diff --git a/.editorconfig b/.editorconfig index e4bd9df2e3e..a6ecef14f19 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,642 @@ -root = true - [*] -end_of_line = lf -insert_final_newline = true - -[*.{js,rb,ts}] charset = utf-8 -indent_style = space +end_of_line = lf indent_size = 2 +ij_continuation_indent_size = 2 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 2 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = 0 +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_value_alignment = 0 + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_brace_placement = 0 +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_value_alignment = 0 + +[*.pp] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_puppet_keep_indents_on_empty_lines = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_value_alignment = 0 + +[*.slim] +indent_size = 2 +ij_slim_keep_indents_on_empty_lines = false + +[*.styl] +indent_size = 2 +ij_stylus_align_closing_brace_with_properties = false +ij_stylus_blank_lines_around_nested_selector = 1 +ij_stylus_blank_lines_between_blocks = 1 +ij_stylus_brace_placement = 0 +ij_stylus_hex_color_long_format = false +ij_stylus_hex_color_lower_case = false +ij_stylus_hex_color_short_format = false +ij_stylus_hex_color_upper_case = false +ij_stylus_keep_blank_lines_in_code = 2 +ij_stylus_keep_indents_on_empty_lines = false +ij_stylus_keep_single_line_blocks = false +ij_stylus_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_stylus_space_after_colon = true +ij_stylus_space_before_opening_brace = true +ij_stylus_value_alignment = 0 + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**/*,@angular/material,@angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = global +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.erb,*.rhtml}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_rhtml_keep_indents_on_empty_lines = false + +[{*.js,*.cjs}] +ij_continuation_indent_size = 2 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**/*,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.sht,*.html,*.shtm,*.shtml,*.htm,*.ng}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_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 +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.xslt,*.rng,*.xsl,*.xml,*.jhm,*.tld,*.xsd,*.fxml,*.wsdl,*.jrxml,*.jnlp,*.ant,*.xul}] +indent_size = 2 +ij_xml_block_comment_at_first_column = true +ij_xml_keep_indents_on_empty_lines = false +ij_xml_line_comment_at_first_column = true + +[{*.yml.example,*.yml,*.yaml}] +indent_size = 2 +ij_continuation_indent_size = 2 +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true + +[{*.zsh,*.bash,*.sh}] +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{.babelrc,.stylelintrc,.eslintrc,jest.config,*.json,*.jsb3,*.jsb2,*.bowerrc}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{rcov,spec,rake,cucumber,rails,spork,capfile,gemfile,rakefile,guardfile,isolate,vagrantfile,Puppetfile,*.jbuilder,*.rbw,*.gemspec,*.thor,*.ru,*.rb,*.rake}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_ruby_align_group_field_declarations = false +ij_ruby_align_multiline_parameters = true +ij_ruby_blank_lines_around_method = 1 +ij_ruby_convert_brace_block_by_enter = true +ij_ruby_force_newlines_around_visibility_mods = true +ij_ruby_indent_private_methods = false +ij_ruby_indent_protected_methods = false +ij_ruby_indent_public_methods = false +ij_ruby_indent_when_cases = false +ij_ruby_keep_blank_lines_in_declarations = 2 +ij_ruby_keep_indents_on_empty_lines = false +ij_ruby_keep_line_breaks = true +ij_ruby_parentheses_around_method_arguments = true +ij_ruby_spaces_around_hashrocket = true +ij_ruby_spaces_around_other_operators = true +ij_ruby_spaces_around_range_operators = false +ij_ruby_spaces_around_relational_operators = true +ij_ruby_spaces_within_array_initializer_braces = true +ij_ruby_spaces_within_braces = false + +[{version-autocompleter.component.ts,*.ats,*.ts}] +ij_continuation_indent_size = 2 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**/*,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = false +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = false +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false diff --git a/Dangerfile b/Dangerfile index ac48bd20bc3..9e4e9173bba 100644 --- a/Dangerfile +++ b/Dangerfile @@ -5,7 +5,9 @@ fail("jasmine fdescribe left in tests") if `grep --include '*.spec.ts' -rP 'fdes git.modified_files .select { |path| path.include?('frontend') && path.end_with?('.ts') } .each do |path| - lines = File.readlines(path) + next unless File.readable?(path) + + lines = File.readlines (path) # Ignore non component files component_line = lines.grep(/@Component/)[0] diff --git a/app/assets/javascripts/onboarding/work_package_tour.js b/app/assets/javascripts/onboarding/work_package_tour.js index 9b3d56ff1dd..93f1a7bacd7 100644 --- a/app/assets/javascripts/onboarding/work_package_tour.js +++ b/app/assets/javascripts/onboarding/work_package_tour.js @@ -6,7 +6,7 @@ 'showSkip': false, 'nextButton': {text: I18n.t('js.onboarding.buttons.next')}, onNext: function () { - $(".wp-table--cell-span.id a ")[0].click(); + $(".inline-edit--display-field.id a ")[0].click(); } }, { diff --git a/app/assets/stylesheets/content/_attributes_group.lsg b/app/assets/stylesheets/content/_attributes_group.lsg index 45181d641cc..f683326af2c 100644 --- a/app/assets/stylesheets/content/_attributes_group.lsg +++ b/app/assets/stylesheets/content/_attributes_group.lsg @@ -67,8 +67,8 @@
Assignee
-
- Jane Doe +
+ Jane Doe
@@ -81,8 +81,8 @@
-
- John Doe +
+ John Doe
@@ -98,8 +98,8 @@
Progress (%)
- -
+ +
- +

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. @@ -100,7 +98,7 @@

- +
``` diff --git a/app/assets/stylesheets/content/_work_packages.sass b/app/assets/stylesheets/content/_work_packages.sass index 33d0de4dd8a..1f1b0bb1f7e 100644 --- a/app/assets/stylesheets/content/_work_packages.sass +++ b/app/assets/stylesheets/content/_work_packages.sass @@ -42,7 +42,6 @@ // Specific field styles @import work_packages/inplace_editing/display_fields @import work_packages/inplace_editing/edit_fields -@import work_packages/inplace_editing/legacy_inplace_styles @import work_packages/inplace_editing/textareas // WP single view styles diff --git a/app/assets/stylesheets/content/work_packages/_table_content.lsg b/app/assets/stylesheets/content/work_packages/_table_content.lsg index 6897fa631ca..6c4310b196c 100644 --- a/app/assets/stylesheets/content/work_packages/_table_content.lsg +++ b/app/assets/stylesheets/content/work_packages/_table_content.lsg @@ -90,20 +90,20 @@
1234 - Lorem ipsum + Lorem ipsum - User Story + User Story
- In Progress + In Progress - Normal + Normal - John Doe + John Doe @@ -121,20 +121,20 @@ 1234 - Lorem ipsum + Lorem ipsum - User Story + User Story
- In Progress + In Progress - Normal + Normal - John Doe + John Doe diff --git a/app/assets/stylesheets/content/work_packages/_table_content.sass b/app/assets/stylesheets/content/work_packages/_table_content.sass index 5984ae92e65..a084582ae91 100644 --- a/app/assets/stylesheets/content/work_packages/_table_content.sass +++ b/app/assets/stylesheets/content/work_packages/_table_content.sass @@ -63,7 +63,7 @@ line-height: 24px // Avoid that the select field gets too small - .wp-inline-edit--field.ng-select + .inline-edit--field.ng-select min-width: 140px // Styles for inline editable attributes @@ -71,7 +71,7 @@ display: table-cell width: auto - &:hover .wp-edit-field.-error:hover + &:hover .inline-edit--active-field.-error:hover border-color: $nm-color-error-border // @@ -125,23 +125,16 @@ html:not(.-browser-mobile) .wp-table--cell-td.-editing & display: block - .inplace-edit + .inline-edit--display-field display: initial - - &.id .inplace-edit -// pointer-events: none - - a - pointer-events: all - // Some padding for the inner cells of the display fields -.wp-table--cell-span +.inline-edit--display-field padding: 2px body.-browser-edge // Ensure height is set in table - .work-package-table .wp-table--cell-span + .work-package-table .inline-edit--display-field height: 22px !important line-height: 22px !important diff --git a/app/assets/stylesheets/content/work_packages/_table_inline_create.sass b/app/assets/stylesheets/content/work_packages/_table_inline_create.sass index fc600fcac27..a482b1a7313 100644 --- a/app/assets/stylesheets/content/work_packages/_table_inline_create.sass +++ b/app/assets/stylesheets/content/work_packages/_table_inline_create.sass @@ -42,7 +42,7 @@ &:hover background: darken(#BEF3CA, 5%) !important - .wp-table--cell-span:hover + .inline-edit--display-field:hover border-color: #35c53f .wp-table--cancel-create-td diff --git a/app/assets/stylesheets/content/work_packages/inplace_editing/_display_fields.sass b/app/assets/stylesheets/content/work_packages/inplace_editing/_display_fields.sass index dddbbd4d19a..a0bb1ce8285 100644 --- a/app/assets/stylesheets/content/work_packages/inplace_editing/_display_fields.sass +++ b/app/assets/stylesheets/content/work_packages/inplace_editing/_display_fields.sass @@ -27,7 +27,7 @@ //++ // READ value of edit fields -.wp-edit-field--display-field +.inline-edit--display-field display: inline-block max-width: 100% @include text-shortener @@ -36,6 +36,11 @@ &.-placeholder font-style: italic + // Always render custom options as inline + // when only one line + .custom-option:not(.-multiple-lines) + display: inline + &.split-time-field white-space: nowrap @@ -58,6 +63,28 @@ font-style: italic font-weight: bold -.wp-edit-field--text, -wp-edit-field +// Editable fields cursor +.inline-edit--display-field.-editable + cursor: text + border-color: transparent + border-style: solid + border-radius: 2px + border-width: 1px + line-height: normal + + &:hover, + &:focus + border-color: $inplace-edit--border-color + + &.-multiline + white-space: inherit + +// Mark focused, non-editable read-values +.inline-edit--display-field:not(.id).-read-only + &:focus, &:hover + color: $inplace-edit--color--disabled + background: $inplace-edit--bg-color--disabled + cursor: not-allowed + +editable-attribute-field width: 100% diff --git a/app/assets/stylesheets/content/work_packages/inplace_editing/_edit_fields.sass b/app/assets/stylesheets/content/work_packages/inplace_editing/_edit_fields.sass index 7d504c2ca75..e73ec890a02 100644 --- a/app/assets/stylesheets/content/work_packages/inplace_editing/_edit_fields.sass +++ b/app/assets/stylesheets/content/work_packages/inplace_editing/_edit_fields.sass @@ -1,8 +1,8 @@ -.wp-edit-field +.inline-edit--container &.-error, .wp-table--cell-td.-error & - .wp-table--cell-span, - .wp-inline-edit--field + .inline-edit--display-field, + .inline-edit--field background: $nm-color-error-background border-color: $nm-color-error-border @@ -27,15 +27,6 @@ border-radius: 2px border-color: darkblue - .inplace-edit--read-value - &:before - vertical-align: middle - .wp-table--cell-span - vertical-align: middle - - &.inplace-edit .custom-option:not(.-multiple-lines) - display: inline - .inline-label .form-label, .icon-context:before @@ -43,11 +34,8 @@ // Style no-label fields (long text, description, ..) with padding -.wp-edit-field--container.-no-label:not(.-active) - .wp-table--cell-span +.inline-edit--container.-no-label:not(.-active) + .inline-edit--display-field display: block - padding: 5px - padding-right: 0 + padding: 5px 0 5px 5px -.wp-edit-field.description.-no-label - margin-left: -0.375rem diff --git a/app/assets/stylesheets/content/work_packages/inplace_editing/_legacy_inplace_styles.sass b/app/assets/stylesheets/content/work_packages/inplace_editing/_legacy_inplace_styles.sass deleted file mode 100644 index ecdf6a11a09..00000000000 --- a/app/assets/stylesheets/content/work_packages/inplace_editing/_legacy_inplace_styles.sass +++ /dev/null @@ -1,160 +0,0 @@ -//-- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2017 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See docs/COPYRIGHT.rdoc for more details. -//++ - -// *** NOTE *** -// Following code handles trigger-links and inplace icons -// It is used for example for the attachements list and comment form - -// need to specify the a explicitly as otherwise -// the default class will win -a.inplace-editing--trigger-link, -.inplace-editing--trigger-link, - &:hover, - &:focus, - &.-focus - text-decoration: none - color: $body-font-color - - .inplace-editing--container, - .wp-edit-field - border-color: $inplace-edit--border-color - - &.-active - border-color: transparent - - .inplace-edit--icon-wrapper - visibility: visible - - .inplace-edit--read-value - display: inline-block - span - line-height: 2 - - .inplace-editing--container - position: relative - -.work-package--details--long-field - .inplace-edit--read .inplace-edit--read-value - // Use the whole space and leave room for the icon on the right - width: calc(100% - 42px) - padding: 3px - line-height: 2 - span.deleting - opacity: 0.5 - - img.avatar-mini - float: inherit - -.inplace-edit--icon-wrapper - position: absolute - height: 100% - right: 0 - text-align: center - width: 32px - background: $gray-light - border-left: 1px solid $inplace-edit--border-color - color: $body-font-color - visibility: hidden - - i - position: relative - // Position the icon in the middle - top: calc(50% - 0.5rem) - - .icon-context:before - // HACK: overriding default padding here - padding-right: 0 - - &:hover - text-decoration: none - - - -// Editable fields cursor -.-editable .wp-table--cell-span, -.wp-table--cell-span.-editable - cursor: text - border-color: transparent - border-style: solid - border-radius: 2px - border-width: 1px - line-height: normal - - &:hover, - &:focus - border-color: $inplace-edit--border-color - - .work-package--placeholder - padding: 0 10px 0 0px - - .inplace-edit--read-value--value-span - width: 100% - white-space: nowrap - - &.-multiline - white-space: inherit - -// Do not hover trigger-link when element is read-only -.-read-only - .inplace-editing--trigger-link:hover .inplace-editing--container - border-color: transparent - -// Mark focused, non-editable read-values -.inplace-edit--read-value.-read-only, -.wp-table--cell-span:not(.id).-read-only - &:focus, &:hover - color: $inplace-edit--color--disabled - background: $inplace-edit--bg-color--disabled - cursor: not-allowed - -// Animations on leaving work packages -.wp--row.-animated-leave - &.ng-leave - -webkit-transition: all 1s ease-in-out - -moz-transition: all 1s ease-in-out - -o-transition: all 1s ease-in-out - transition: all 1s ease-in-out - - &.ng-leave-active - height: 0 - line-height: 0 - opacity: 0 - -// Allow horizontal scrolling in descripton field -.work-packages--details--description .wp-table--cell-span .inplace-edit--read-value--value-span - overflow-x: auto - -// Style the attachment hint label of the description field -.wp-edit-field-attachment-label - // Reduce width due to save|cancel controls - width: 75% - position: absolute - left: 1px - font-size: 0.8rem - padding-top: 5px - font-style: italic diff --git a/app/assets/stylesheets/content/work_packages/new/_split_view.sass b/app/assets/stylesheets/content/work_packages/new/_split_view.sass index 0ad1143fe8e..de5c14e1cdb 100644 --- a/app/assets/stylesheets/content/work_packages/new/_split_view.sass +++ b/app/assets/stylesheets/content/work_packages/new/_split_view.sass @@ -10,7 +10,7 @@ // Capitalize status name .work-packages--status-selector - .wp-edit-field--display-field + .inline-edit--display-field text-transform: capitalize // Full screen toggle indicator on the diff --git a/app/assets/stylesheets/content/work_packages/new/_type_status_row.sass b/app/assets/stylesheets/content/work_packages/new/_type_status_row.sass index 4621687a2f5..db7aa3b7956 100644 --- a/app/assets/stylesheets/content/work_packages/new/_type_status_row.sass +++ b/app/assets/stylesheets/content/work_packages/new/_type_status_row.sass @@ -9,16 +9,16 @@ margin-left: 5px // Fix display left padding of tpye - .wp-edit-field--display-field + .inline-edit--display-field padding-left: 0 !important // Disable text decoration from toolbar span - .wp-edit-field--display-field:hover + .inline-edit--display-field:hover text-decoration: none .wp-new-top-row--status, .wp-new-top-row--type - .wp-inline-edit--field + .inline-edit--field min-width: 125px .work-packages--type-selector, diff --git a/app/assets/stylesheets/content/work_packages/single_view/_inplace_esque_fields.sass b/app/assets/stylesheets/content/work_packages/single_view/_inplace_esque_fields.sass index df14c43e617..f8ee21b1d48 100644 --- a/app/assets/stylesheets/content/work_packages/single_view/_inplace_esque_fields.sass +++ b/app/assets/stylesheets/content/work_packages/single_view/_inplace_esque_fields.sass @@ -1,3 +1,31 @@ +// Add a border at all times to the inplace container +.inplace-editing--container + position: relative + +// need to specify the a explicitly as otherwise +// the default class will win +a.inplace-editing--trigger-link, +.inplace-editing--trigger-link, + &:hover, + &:focus, + &.-focus + text-decoration: none + color: $body-font-color + + &.-active + border-color: transparent + + .inplace-editing--container + border-color: $inplace-edit--border-color + + .inplace-editing--trigger-icon + visibility: visible + +// Do not hover trigger-link when element is read-only +.-read-only + .inplace-editing--trigger-link:hover .inplace-editing--container + border-color: transparent + // Explicit styles for input-esque trigger appearance .work-packages--activity--add-comment, .work-package--watchers-lookup @@ -15,29 +43,34 @@ color: $body-font-color font-style: italic - .inplace-edit--read-value - padding-left: 5px +// The trigger text left to the edit icon +.inplace-editing--trigger-text + width: calc(100% - 42px) + padding: 3px + line-height: 2 + display: inline-block +// The edit icon to the right of the trigger text +.inplace-editing--trigger-icon + position: absolute + height: 100% + top: 0 + right: 0 + text-align: center + width: 32px + background: $gray-light + border-left: 1px solid $inplace-edit--border-color + color: $body-font-color + visibility: hidden -// Editing existing comments -.comments-form - float: left - width: 100% - margin: 10px 0 100px 0 - textarea - border: 1px solid #dddddd - padding: 8px - border-radius: 2px - font-size: 0.8125rem - width: 100% - button - float: right - font-size: 0.8125rem - background: linear-gradient(to bottom, white 0%, #eeeeee 74%, #eeeeee 100%) repeat scroll 0 0 rgba(0, 0, 0, 0) - border: 1px solid #CCCCCC - border-radius: 2px - color: #222222 - margin: 10px 0 0 0 - padding: 4px 10px 2px - cursor: pointer - height: 32px + i + position: relative + // Position the icon in the middle + top: calc(50% - 0.5rem) + + .icon-context:before + // HACK: overriding default padding here + padding-right: 0 + + &:hover + text-decoration: none diff --git a/app/assets/stylesheets/content/work_packages/single_view/_single_view.sass b/app/assets/stylesheets/content/work_packages/single_view/_single_view.sass index 4ac335caa8e..cb1f4711c78 100644 --- a/app/assets/stylesheets/content/work_packages/single_view/_single_view.sass +++ b/app/assets/stylesheets/content/work_packages/single_view/_single_view.sass @@ -33,8 +33,8 @@ // Subject field .subject-header, .wp-new--subject-wrapper - .wp-inline-edit--active-field.subject - .wp-inline-edit--field + .inline-edit--active-field.subject + .inline-edit--field height: 40px font-size: 16px line-height: 1 diff --git a/app/assets/stylesheets/content/work_packages/tabs/_relations.sass b/app/assets/stylesheets/content/work_packages/tabs/_relations.sass index f098781343e..bdf756286fc 100644 --- a/app/assets/stylesheets/content/work_packages/tabs/_relations.sass +++ b/app/assets/stylesheets/content/work_packages/tabs/_relations.sass @@ -61,11 +61,11 @@ .description-section border: 1px dotted lightblue padding: 4px - .wp-edit-field + .inline-edit--container @include text-shortener // Similar to inner span's line-height line-height: 1.6em - .wp-table--cell-span + .inline-edit--display-field vertical-align: middle .controls-container text-align: right diff --git a/app/assets/stylesheets/layout/work_packages/_details_view.sass b/app/assets/stylesheets/layout/work_packages/_details_view.sass index 9774b849cc7..b6be9a4c9fb 100644 --- a/app/assets/stylesheets/layout/work_packages/_details_view.sass +++ b/app/assets/stylesheets/layout/work_packages/_details_view.sass @@ -89,7 +89,7 @@ body.router--work-packages-split-view-new // Fix height of subject row .work-packages--subject-element, - .work-packages--details--subject .wp-inline-edit--field + .work-packages--details--subject .inline-edit--field font-size: 1.125rem font-weight: bold @@ -97,7 +97,7 @@ body.router--work-packages-split-view-new line-height: 24px overflow: hidden - .wp-inline-edit--field + .inline-edit--field height: 38px line-height: 36px diff --git a/app/assets/stylesheets/layout/work_packages/_full_view.sass b/app/assets/stylesheets/layout/work_packages/_full_view.sass index 77536385ad6..aeff0207a27 100644 --- a/app/assets/stylesheets/layout/work_packages/_full_view.sass +++ b/app/assets/stylesheets/layout/work_packages/_full_view.sass @@ -172,26 +172,21 @@ width: 100% margin: 0 - .wp-table--cell-span + .inline-edit--display-field white-space: normal word-break: break-all li.inline-edit width: 100% - .inplace-edit--read-value - margin: 0 - padding-top: 0 - padding-bottom: 0 - .work-packages--subject-element, - .work-packages--details--subject .wp-inline-edit--field + .work-packages--details--subject .inline-edit--field font-size: 20px font-weight: bold line-height: 34px .work-packages--subject-element - .wp-inline-edit--field + .inline-edit--field height: 34px // Style edit field to look like the display field. @@ -211,30 +206,24 @@ position: relative // Fix: align and size hover border like buttons in toolbar. - .wp-edit-field.-no-label:not(.-active) .wp-table--cell-span + .inline-edit--container .inline-edit--display-field padding-top: 3px padding-bottom: 3px margin-top: 2px .work-packages--type-selector:not(.wp-new-top-row--element) - .wp-table--cell-span + .inline-edit--display-field padding-right: 5px !important // Remove left padding from type - .wp-edit-field--display-field + .inline-edit--display-field padding-left: 0 !important @media only screen and (min-width: 679px) - .wp-edit-field--container.-active + .inline-edit--container.-active margin-right: 80px !important width: 95% - .inplace-edit--read-value--value-span - white-space: nowrap - - .inplace-edit--read-value--value-span:after - content: ':' - .edit-all-mode .subject-header .work-packages--details--subject .inplace-edit--text-field padding-left: 0.375rem diff --git a/app/assets/stylesheets/layout/work_packages/_mobile.sass b/app/assets/stylesheets/layout/work_packages/_mobile.sass index d114ac8b46a..95648863e91 100644 --- a/app/assets/stylesheets/layout/work_packages/_mobile.sass +++ b/app/assets/stylesheets/layout/work_packages/_mobile.sass @@ -86,9 +86,6 @@ .attributes-key-value--value-container margin-bottom: 20px - .inplace-edit--read-value - padding: 0 - .inplace-editing--container border: none @@ -101,17 +98,9 @@ border-bottom: none padding-top: 4px - // Reset margin for mobile, since the wiki toolbar is hidden on mobile - .inplace-edit.attribute-description - .inplace-edit--write - margin-top: 0 - div[class*='work-packages--details--'] width: 100% - .inplace-edit--read-value span - white-space: normal - .work-package-details-activities-messages font-size: 0.9rem diff --git a/app/assets/stylesheets/layout/work_packages/_print.sass b/app/assets/stylesheets/layout/work_packages/_print.sass index d51ce10d3e8..5c703074a14 100644 --- a/app/assets/stylesheets/layout/work_packages/_print.sass +++ b/app/assets/stylesheets/layout/work_packages/_print.sass @@ -122,7 +122,7 @@ margin-bottom: 0 // Overwrite "click to add description" placeholder with a simple dash. - .wp-edit-field.description.-placeholder + .inline-edit--active-field.description.-placeholder .read-value--html display: none &:after diff --git a/app/assets/stylesheets/layout/work_packages/_table.sass b/app/assets/stylesheets/layout/work_packages/_table.sass index 4dbadd5074d..39551c40026 100644 --- a/app/assets/stylesheets/layout/work_packages/_table.sass +++ b/app/assets/stylesheets/layout/work_packages/_table.sass @@ -160,9 +160,6 @@ > h4 margin-top: 5px - .inplace-edit--read-value - padding: 0.375rem 0.6rem - i display: inline-block &:before diff --git a/app/assets/stylesheets/layout/work_packages/_table_embedded.sass b/app/assets/stylesheets/layout/work_packages/_table_embedded.sass index 1911208c53c..aafb3ec9180 100644 --- a/app/assets/stylesheets/layout/work_packages/_table_embedded.sass +++ b/app/assets/stylesheets/layout/work_packages/_table_embedded.sass @@ -40,7 +40,7 @@ $table-timeline--compact-row-height: 28px // Align with section header .wp-table--table-header:first-child .generic-table--sort-header-outer, - .wp-table--cell-td:first-child .wp-edit-field--display-field, + .wp-table--cell-td:first-child .inline-edit--display-field, .wp-inline-create--add-link i:before padding-left: 0 diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 959b2be3b63..22b75204a80 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -212,6 +212,14 @@ en: general_text_No: "No" general_text_Yes: "Yes" + hal: + error: + update_conflict_refresh: "Click here to refresh the resource and update to the newest version." + edit_prohibited: "Editing %{attribute} is blocked for this resource. Either this attribute is derived from relations (e.g, children) or otherwise not configurable." + format: + date: "%{attribute} is no valid date - YYYY-MM-DD expected." + general: "An error has occurred." + homescreen: blocks: new_features: @@ -299,6 +307,7 @@ en: label_no_data: "No data to display" label_no_due_date: "no end date" label_no_start_date: "no start date" + label_no_value: "No value" label_none: "none" label_not_contains: "doesn't contain" label_not_equals: "is not" @@ -660,12 +669,6 @@ en: description_enter_text: "Enter text" description_options_hide: "Hide options" description_options_show: "Show options" - error: - update_conflict_refresh: "Click here to refresh the work package and update to the newest version." - edit_prohibited: "Editing %{attribute} is blocked for this work package. Either this attribute is derived from relations (e.g, children) or otherwise not configurable." - format: - date: "%{attribute} is no valid date - YYYY-MM-DD expected." - general: "An error has occurred." edit_attribute: "%{attribute} - Edit" key_value: "%{key}: %{value}" label_enable_multi_select: "Enable multiselect" @@ -680,7 +683,6 @@ en: message_view_spent_time: "Show spent time for this work package" message_work_package_read_only: "Work package is locked in this status. No attribute other than status can be altered." message_work_package_status_blocked: "Work package status is not writable due to closed status and closed version being assigned." - no_value: "No value" placeholder_filter_by_text: "Subject, description, comments, ..." inline_create: title: 'Click here to add a new work package to this list' diff --git a/frontend/doc/PLUGINS.md b/frontend/doc/PLUGINS.md index a571c07b2ed..42543011ed7 100644 --- a/frontend/doc/PLUGINS.md +++ b/frontend/doc/PLUGINS.md @@ -48,8 +48,8 @@ Let's take a look at the file structure of the costs folder `frontend/`: module ├── main.ts └── wp-display - ├── wp-display-costs-by-type-field.module.ts - └── wp-display-currency-field.module.ts + ├── costs-by-type-display-field.module.ts + └── currency-display-field.module.ts ``` The Angular frontend entry point is `frontend/module/main.ts` and should export a `PluginModule` ngModule that looks like the following: diff --git a/frontend/src/app/angular4-modules.ts b/frontend/src/app/angular4-modules.ts index ce5aa2e3bc4..eee0aa80c4f 100644 --- a/frontend/src/app/angular4-modules.ts +++ b/frontend/src/app/angular4-modules.ts @@ -93,6 +93,7 @@ import {WpPreviewModal} from "core-components/modals/preview-modal/wp-preview-mo import {PreviewTriggerService} from "core-app/globals/global-listeners/preview-trigger.service"; import {OpenprojectOverviewModule} from "core-app/modules/overview/openproject-overview.module"; import {OpenprojectMyPageModule} from "core-app/modules/my-page/openproject-my-page.module"; +import {OpenprojectProjectsModule} from "core-app/modules/projects/openproject-projects.module"; @NgModule({ imports: [ @@ -114,6 +115,9 @@ import {OpenprojectMyPageModule} from "core-app/modules/my-page/openproject-my-p OpenprojectGridsModule, OpenprojectAttachmentsModule, + // Project module + OpenprojectProjectsModule, + // Work packages and their routes OpenprojectWorkPackagesModule, OpenprojectWorkPackageRoutesModule, diff --git a/frontend/src/app/components/main-menu/main-menu-toggle.service.ts b/frontend/src/app/components/main-menu/main-menu-toggle.service.ts index 8b670986c03..82c44fd9aa8 100644 --- a/frontend/src/app/components/main-menu/main-menu-toggle.service.ts +++ b/frontend/src/app/components/main-menu/main-menu-toggle.service.ts @@ -86,7 +86,7 @@ export class MainMenuToggleService { } // click on arrow or hamburger icon - public toggleNavigation(event?:JQuery.TriggeredEvent) : void { + public toggleNavigation(event?:JQuery.TriggeredEvent):void { if (event) { event.stopPropagation(); event.preventDefault(); diff --git a/frontend/src/app/components/modals/save-modal/save-query.modal.ts b/frontend/src/app/components/modals/save-modal/save-query.modal.ts index a1ebd30b11b..0a033f83dc1 100644 --- a/frontend/src/app/components/modals/save-modal/save-query.modal.ts +++ b/frontend/src/app/components/modals/save-modal/save-query.modal.ts @@ -27,7 +27,7 @@ //++ import {States} from '../../states.service'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {QueryResource} from 'core-app/modules/hal/resources/query-resource'; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {OpModalComponent} from "core-components/op-modals/op-modal.component"; @@ -67,7 +67,7 @@ export class SaveQueryModal extends OpModalComponent { readonly states:States, readonly querySpace:IsolatedQuerySpace, readonly wpListService:WorkPackagesListService, - readonly wpNotificationsService:WorkPackageNotificationService, + readonly halNotification:HalResourceNotificationService, readonly cdRef:ChangeDetectorRef, readonly notificationsService:NotificationsService) { super(locals, cdRef, elementRef); @@ -105,7 +105,7 @@ export class SaveQueryModal extends OpModalComponent { this.closeMe($event); return Promise.resolve(true); }) - .catch((error:any) => this.wpNotificationsService.handleRawError(error)) + .catch((error:any) => this.halNotification.handleRawError(error)) .then(() => this.isBusy = false); // Same as .finally() } } diff --git a/frontend/src/app/components/modals/share-modal/query-sharing.modal.ts b/frontend/src/app/components/modals/share-modal/query-sharing.modal.ts index da4b8a852e0..965691dd474 100644 --- a/frontend/src/app/components/modals/share-modal/query-sharing.modal.ts +++ b/frontend/src/app/components/modals/share-modal/query-sharing.modal.ts @@ -28,7 +28,7 @@ import {WorkPackagesListService} from '../../wp-list/wp-list.service'; import {States} from '../../states.service'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {QueryResource} from 'core-app/modules/hal/resources/query-resource'; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {OpModalComponent} from "core-components/op-modals/op-modal.component"; @@ -66,7 +66,7 @@ export class QuerySharingModal extends OpModalComponent implements OnInit { readonly querySpace:IsolatedQuerySpace, readonly cdRef:ChangeDetectorRef, readonly wpListService:WorkPackagesListService, - readonly wpNotificationsService:WorkPackageNotificationService, + readonly halNotification:HalResourceNotificationService, readonly notificationsService:NotificationsService) { super(locals, cdRef, elementRef); } diff --git a/frontend/src/app/components/modals/wp-destroy-modal/wp-destroy.modal.ts b/frontend/src/app/components/modals/wp-destroy-modal/wp-destroy.modal.ts index 5eac6660e3f..fc40546a45a 100644 --- a/frontend/src/app/components/modals/wp-destroy-modal/wp-destroy.modal.ts +++ b/frontend/src/app/components/modals/wp-destroy-modal/wp-destroy.modal.ts @@ -28,7 +28,7 @@ import {WorkPackagesListService} from '../../wp-list/wp-list.service'; import {States} from '../../states.service'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {OpModalComponent} from "core-components/op-modals/op-modal.component"; import {ChangeDetectorRef, Component, ElementRef, Inject, OnInit} from "@angular/core"; @@ -40,6 +40,7 @@ import {StateService} from '@uirouter/core'; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {WorkPackageService} from "core-components/work-packages/work-package.service"; import {BackRoutingService} from "core-app/modules/common/back-routing/back-routing.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ templateUrl: './wp-destroy.modal.html' @@ -76,8 +77,7 @@ export class WpDestroyModal extends OpModalComponent implements OnInit { readonly states:States, readonly wpTableFocus:WorkPackageViewFocusService, readonly wpListService:WorkPackagesListService, - readonly wpNotificationsService:WorkPackageNotificationService, - readonly notificationsService:NotificationsService, + readonly notificationService:WorkPackageNotificationService, readonly backRoutingService:BackRoutingService) { super(locals, cdRef, elementRef); } diff --git a/frontend/src/app/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts b/frontend/src/app/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts index e69766f58bf..3543e479e80 100644 --- a/frontend/src/app/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts +++ b/frontend/src/app/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts @@ -29,7 +29,8 @@ import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; import {Directive, ElementRef, Inject} from "@angular/core"; import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {States} from "core-components/states.service"; import {FormResource} from 'core-app/modules/hal/resources/form-resource'; @@ -41,7 +42,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger constructor(readonly elementRef:ElementRef, readonly opContextMenu:OPContextMenuService, readonly states:States, - readonly wpEditing:WorkPackageEditingService) { + readonly halEditing:HalResourceEditingService) { super(elementRef, opContextMenu); } @@ -50,7 +51,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger const wp = this.states.workPackages.get('new').value; if (wp) { - const change = this.wpEditing.changeFor(wp); + const change = this.halEditing.changeFor(wp); change.getForm().then( (loadedForm:FormResource) => { this.buildItems(loadedForm); diff --git a/frontend/src/app/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts b/frontend/src/app/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts index 90bc58aed17..dea00c3943a 100644 --- a/frontend/src/app/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts +++ b/frontend/src/app/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts @@ -30,15 +30,17 @@ import {StateService} from '@uirouter/core'; import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; import {Directive, ElementRef, Inject, Input} from "@angular/core"; import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource'; import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Directive({ selector: '[wpStatusDropdown]' @@ -49,17 +51,17 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger { constructor(readonly elementRef:ElementRef, readonly opContextMenu:OPContextMenuService, readonly $state:StateService, - protected wpNotificationsService:WorkPackageNotificationService, - protected wpEditing:WorkPackageEditingService, + protected workPackageNotificationService:WorkPackageNotificationService, + protected halEditing:HalResourceEditingService, protected notificationService:NotificationsService, protected I18n:I18nService, - protected wpEvents:WorkPackageEventsService) { + protected halEvents:HalEventsService) { super(elementRef, opContextMenu); } protected open(evt:JQuery.TriggeredEvent) { - const change = this.wpEditing.changeFor(this.workPackage); + const change = this.halEditing.changeFor(this.workPackage); change.getForm().then((form:any) => { const statuses = form.schema.status.allowedValues; @@ -82,15 +84,15 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger { } private updateStatus(status:HalResource) { - const change = this.wpEditing.changeFor(this.workPackage); + const change = this.halEditing.changeFor(this.workPackage); change.projectedResource.status = status; if (!this.workPackage.isNew) { - this.wpEditing + this.halEditing .save(change) .then(() => { - this.wpNotificationsService.showSave(this.workPackage); - this.wpEvents.push({ type: 'updated', id: this.workPackage.id! }); + this.workPackageNotificationService.showSave(this.workPackage); + this.halEvents.push(this.workPackage, { eventType: 'updated' }); }); } } diff --git a/frontend/src/app/components/projects/project-cache.service.ts b/frontend/src/app/components/projects/project-cache.service.ts index 3a55e2e910c..10a83cb01ca 100644 --- a/frontend/src/app/components/projects/project-cache.service.ts +++ b/frontend/src/app/components/projects/project-cache.service.ts @@ -32,11 +32,13 @@ import {StateCacheService} from '../states/state-cache.service'; import {Injectable} from '@angular/core'; import {ProjectResource} from 'core-app/modules/hal/resources/project-resource'; import {ProjectDmService} from 'core-app/modules/hal/dm-services/project-dm.service'; +import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; @Injectable() export class ProjectCacheService extends StateCacheService { constructor(readonly states:States, + readonly schemaCacheService:SchemaCacheService, readonly projectDmService:ProjectDmService) { super(); } @@ -47,8 +49,19 @@ export class ProjectCacheService extends StateCacheService { .then(_ => undefined); } + updateValue(id:string, val:ProjectResource) { + this.schemaCacheService.ensureLoaded(val).then(() => { + super.updateValue(id, val); + }); + } + protected load(id:string):Promise { - return this.projectDmService.one(parseInt(id)); + return this + .projectDmService + .one(parseInt(id)) + .then((project) => { + return this.schemaCacheService.ensureLoaded(project).then(() => project); + }); } protected get multiState():MultiInputState { diff --git a/frontend/src/app/components/schemas/schema-cache.service.ts b/frontend/src/app/components/schemas/schema-cache.service.ts index 8e87a0f6e3b..1f5e7245a2a 100644 --- a/frontend/src/app/components/schemas/schema-cache.service.ts +++ b/frontend/src/app/components/schemas/schema-cache.service.ts @@ -1,7 +1,3 @@ -import {Injectable} from '@angular/core'; -import {SchemaResource} from 'core-app/modules/hal/resources/schema-resource'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; // -- copyright // OpenProject is a project management system. // Copyright (C) 2012-2015 the OpenProject Foundation (OPF) @@ -31,6 +27,10 @@ import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.ser // ++ import {InputState, State} from 'reactivestates'; import {States} from '../states.service'; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {Injectable} from '@angular/core'; +import {SchemaResource} from 'core-app/modules/hal/resources/schema-resource'; +import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; @Injectable() export class SchemaCacheService { @@ -44,13 +44,13 @@ export class SchemaCacheService { * @param href The schema's href. * @return A promise with the loaded schema. */ - ensureLoaded(workPackage:WorkPackageResource):Promise { - const state = this.state(workPackage); + ensureLoaded(resource:HalResource):Promise { + const state = this.state(resource); if (state.hasValue()) { return Promise.resolve(state.value); } else { - return this.load(workPackage).valuesPromise() as Promise; + return this.load(resource).valuesPromise() as Promise; } } @@ -58,24 +58,29 @@ export class SchemaCacheService { * Get the associated schema state of the work package * without initializing a new resource. */ - state(workPackage:WorkPackageResource):InputState { - const schema = workPackage.$links.schema; + state(resource:HalResource):InputState { + const schema = resource.$links.schema; + + if (!schema) { + throw `Resource ${resource} has no schema!`; + } + return this.states.schemas.get(schema.href!); } /** * Load the associated schema for the given work package, if needed. */ - load(workPackage:WorkPackageResource, forceUpdate = false):State { - const state = this.state(workPackage); + load(resource:HalResource, forceUpdate = false):State { + const state = this.state(resource); if (forceUpdate) { state.clear(); } state.putFromPromiseIfPristine(() => { - const resource = this.halResourceService.createLinkedResource(workPackage, 'schema', workPackage.$links.schema.$link); - return resource.$load() as any; + const schemaResource = this.halResourceService.createLinkedResource(resource, 'schema', resource.$links.schema.$link); + return schemaResource.$load() as any; }); return state; diff --git a/frontend/src/app/components/states/state-cache.service.ts b/frontend/src/app/components/states/state-cache.service.ts index 2716c62b723..1180d09de11 100644 --- a/frontend/src/app/components/states/state-cache.service.ts +++ b/frontend/src/app/components/states/state-cache.service.ts @@ -66,6 +66,13 @@ export abstract class StateCacheService { return this.state(id).values$(); } + /** + * Observe the changes of the given id + */ + public changes$(id:string):Observable { + return this.state(id).changes$(); + } + /** * Observe the entire set of loaded results */ diff --git a/frontend/src/app/components/work-packages/work-package-cache.service.spec.ts b/frontend/src/app/components/work-packages/work-package-cache.service.spec.ts index 435fc06e6d0..2a68f04e8b0 100644 --- a/frontend/src/app/components/work-packages/work-package-cache.service.spec.ts +++ b/frontend/src/app/components/work-packages/work-package-cache.service.spec.ts @@ -38,10 +38,14 @@ import {OpenProjectFileUploadService} from 'core-components/api/op-file-upload/o import {SchemaCacheService} from 'core-components/schemas/schema-cache.service'; import {States} from 'core-components/states.service'; import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {take, takeWhile} from 'rxjs/operators'; import {WorkPackageCreateService} from '../wp-new/wp-create.service'; import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service"; +import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service"; +import {TimezoneService} from "core-components/datetime/timezone.service"; +import {ConfigurationService} from "core-app/modules/common/config/configuration.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; describe('WorkPackageCacheService', () => { let injector:Injector; @@ -58,14 +62,18 @@ describe('WorkPackageCacheService', () => { providers: [ States, HalResourceService, + TimezoneService, + WorkPackagesActivityService, WorkPackageCacheService, SchemaCacheService, WorkPackageDmService, + {provide: ConfigurationService, useValue: {}}, {provide: PathHelperService, useValue: {}}, {provide: I18nService, useValue: {t: (...args:any[]) => 'translation'}}, {provide: WorkPackageResource, useValue: {}}, {provide: WorkPackageCreateService, useValue: {}}, {provide: NotificationsService, useValue: {}}, + {provide: HalResourceNotificationService, useValue: {handleRawError: () => false}}, {provide: WorkPackageNotificationService, useValue: {}}, {provide: OpenProjectFileUploadService, useValue: {}} ] @@ -99,7 +107,7 @@ describe('WorkPackageCacheService', () => { dummyWorkPackages = [workPackage1 as any]; }); - it('returns a work package after the list has been initialized', function(done:any) { + it('returns a work package after the list has been initialized', function (done:any) { wpCacheService.loadWorkPackage('1').values$() .pipe( take(1) diff --git a/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html b/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html index 464bbd4627e..5752120eb0c 100644 --- a/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html +++ b/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.html @@ -6,7 +6,7 @@
-
+
- + - + diff --git a/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts b/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts index 36ca8046e12..4dceedcad39 100644 --- a/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts +++ b/frontend/src/app/components/work-packages/work-package-comment/work-package-comment.component.ts @@ -28,7 +28,7 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {ErrorResource} from 'core-app/modules/hal/resources/error-resource'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageCacheService} from '../work-package-cache.service'; import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service'; import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service"; @@ -52,6 +52,7 @@ import {NotificationsService} from "core-app/modules/common/notifications/notifi import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {WorkPackageCommentFieldHandler} from "core-components/work-packages/work-package-comment/work-package-comment-field-handler"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ selector: 'work-package-comment', @@ -83,7 +84,7 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler protected ConfigurationService:ConfigurationService, protected loadingIndicator:LoadingIndicatorService, protected wpCacheService:WorkPackageCacheService, - protected wpNotificationsService:WorkPackageNotificationService, + protected workPackageNotificationService:WorkPackageNotificationService, protected NotificationsService:NotificationsService, protected cdRef:ChangeDetectorRef, protected I18n:I18nService) { @@ -164,7 +165,7 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler .catch((error:any) => { this.inFlight = false; if (error instanceof ErrorResource) { - this.wpNotificationsService.showError(error, this.workPackage); + this.workPackageNotificationService.showError(error, this.workPackage); } else { this.NotificationsService.addError(this.I18n.t('js.work_packages.comment_send_failed')); @@ -178,4 +179,8 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler setTimeout(() => { scrollableContainer.scrollTop = scrollableContainer.scrollHeight; }, 400); } } + + setErrors(newErrors:string[]):void { + // interface + } } diff --git a/frontend/src/app/components/work-packages/work-package.service.ts b/frontend/src/app/components/work-packages/work-package.service.ts index e7d719697a2..d9da6a6eaf1 100644 --- a/frontend/src/app/components/work-packages/work-package.service.ts +++ b/frontend/src/app/components/work-packages/work-package.service.ts @@ -33,10 +33,7 @@ import {PathHelperService} from "core-app/modules/common/path-helper/path-helper import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; -import { - WorkPackageDeletedEvent, - WorkPackageEventsService -} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalDeletedEvent, HalEventsService} from "core-app/modules/hal/services/hal-events.service"; @Injectable() export class WorkPackageService { @@ -51,7 +48,7 @@ export class WorkPackageService { private readonly UrlParamsHelper:UrlParamsHelperService, private readonly NotificationsService:NotificationsService, private readonly I18n:I18nService, - private readonly wpEvents:WorkPackageEventsService) { + private readonly halEvents:HalEventsService) { } public performBulkDelete(ids:string[], defaultHandling:boolean) { @@ -67,7 +64,7 @@ export class WorkPackageService { .then(() => { this.NotificationsService.addSuccess(this.text.successful_delete); - ids.forEach(id => this.wpEvents.push({ type: 'deleted', id: id } as WorkPackageDeletedEvent)); + ids.forEach(id => this.halEvents.push({_type:'WorkPackage', id: id}, { eventType: 'deleted'} as HalDeletedEvent)); if (this.$state.includes('**.list.details.**') && ids.indexOf(this.$state.params.workPackageId) > -1) { diff --git a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts index 1e28adca67a..5d17f250f94 100644 --- a/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts +++ b/frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.component.ts @@ -29,8 +29,9 @@ import {Component, Input, EventEmitter, Output} from '@angular/core'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageRelationsHierarchyService} from 'core-app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service'; -import {WorkPackageNotificationService} from 'core-app/components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ templateUrl: './wp-breadcrumb-parent.html', @@ -53,7 +54,7 @@ export class WorkPackageBreadcrumbParentComponent { public constructor( protected readonly I18n:I18nService, protected readonly wpRelationsHierarchy:WorkPackageRelationsHierarchyService, - protected readonly wpNotifications:WorkPackageNotificationService + protected readonly notificationService:WorkPackageNotificationService ) { } @@ -87,7 +88,7 @@ export class WorkPackageBreadcrumbParentComponent { this.isSaving = true; this.wpRelationsHierarchy.changeParent(this.workPackage, newParentId) .catch((error:any) => { - this.wpNotifications.handleRawError(error, this.workPackage); + this.notificationService.handleRawError(error, this.workPackage); }) .then(() => this.isSaving = false); // Behaves as .finally() } diff --git a/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.component.ts b/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.component.ts index 8bcd881c7d6..cb3d9b4b6e5 100644 --- a/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.component.ts +++ b/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.component.ts @@ -44,7 +44,8 @@ import {debugLog} from '../../../helpers/debug_output'; import {CurrentProjectService} from '../../projects/current-project.service'; import {States} from '../../states.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageCacheService} from '../work-package-cache.service'; import {input, InputState} from 'reactivestates'; import {DisplayFieldService} from 'core-app/modules/fields/display/display-field.service'; @@ -142,7 +143,7 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy { protected currentProject:CurrentProjectService, protected PathHelper:PathHelperService, protected states:States, - protected wpEditing:WorkPackageEditingService, + protected halEditing:HalResourceEditingService, protected halResourceService:HalResourceService, protected displayFieldService:DisplayFieldService, protected wpCacheService:WorkPackageCacheService, @@ -157,7 +158,7 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy { public ngOnInit() { this.$element = jQuery(this.elementRef.nativeElement); - const change = this.wpEditing.changeFor(this.workPackage); + const change = this.halEditing.changeFor(this.workPackage); this.resourceContextChange.next(this.contextFrom(change)); this.refresh(change); @@ -167,14 +168,14 @@ export class WorkPackageSingleViewComponent implements OnInit, OnDestroy { .pipe( takeUntil(componentDestroyed(this)), distinctUntilChanged((a, b) => _.isEqual(a, b)), - map(() => this.wpEditing.changeFor(this.workPackage)) + map(() => this.halEditing.changeFor(this.workPackage)) ) .subscribe((change:WorkPackageChangeset) => this.refresh(change)); // Update the resource context on every update to the temporary resource. // This allows detecting a changed type value in a new work package. - this.wpEditing - .state(this.workPackage.id!) + this.halEditing + .typedState(this.workPackage) .values$() .pipe( takeUntil(componentDestroyed(this)) diff --git a/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.html b/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.html index 71395fef205..b02fa67ea9d 100644 --- a/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.html +++ b/frontend/src/app/components/work-packages/wp-single-view/wp-single-view.html @@ -3,9 +3,9 @@ [ngClass]="{'-can-have-columns' : enableTwoColumnLayout }">
- + [fieldName]="'subject'">
@@ -51,8 +51,8 @@ [attributeScope]="'WorkPackage'">
- +
@@ -83,12 +83,12 @@
- - +
diff --git a/frontend/src/app/components/work-packages/wp-subject/wp-subject.html b/frontend/src/app/components/work-packages/wp-subject/wp-subject.html index fef42aa72fc..2c1da733f32 100644 --- a/frontend/src/app/components/work-packages/wp-subject/wp-subject.html +++ b/frontend/src/app/components/work-packages/wp-subject/wp-subject.html @@ -3,15 +3,15 @@ [ngClass]="'__overflowing_' + uniqueElementIdentifier" [attr.data-overflowing-identifier]="'.__overflowing_' + uniqueElementIdentifier">
- - +
- - +
diff --git a/frontend/src/app/components/work-packages/wp-type-status/wp-type-status.html b/frontend/src/app/components/work-packages/wp-type-status/wp-type-status.html index 89c8572faa5..25c7bd3063f 100644 --- a/frontend/src/app/components/work-packages/wp-type-status/wp-type-status.html +++ b/frontend/src/app/components/work-packages/wp-type-status/wp-type-status.html @@ -1,17 +1,16 @@
- - +
- - +
diff --git a/frontend/src/app/components/wp-activity/comment-service.ts b/frontend/src/app/components/wp-activity/comment-service.ts index 84d78b440d9..35a7ef67dc3 100644 --- a/frontend/src/app/components/wp-activity/comment-service.ts +++ b/frontend/src/app/components/wp-activity/comment-service.ts @@ -32,8 +32,9 @@ import {NotificationsService} from 'core-app/modules/common/notifications/notifi import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {input, InputState} from 'reactivestates'; -import {WorkPackageNotificationService} from './../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {Subject} from "rxjs"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Injectable() export class CommentService { @@ -44,7 +45,7 @@ export class CommentService { constructor( readonly I18n:I18nService, - private wpNotificationsService:WorkPackageNotificationService, + private workPackageNotificationService:WorkPackageNotificationService, private NotificationsService:NotificationsService) { } @@ -78,7 +79,7 @@ export class CommentService { } private errorAndReject(error:HalResource, workPackage?:WorkPackageResource) { - this.wpNotificationsService.handleRawError(error, workPackage); + this.workPackageNotificationService.handleRawError(error, workPackage); // returning a reject will enable to correctly work with subsequent then/catch handlers. return Promise.reject(error); diff --git a/frontend/src/app/components/wp-activity/user/user-activity.component.ts b/frontend/src/app/components/wp-activity/user/user-activity.component.ts index 66194ba4deb..ebe5e26836e 100644 --- a/frontend/src/app/components/wp-activity/user/user-activity.component.ts +++ b/frontend/src/app/components/wp-activity/user/user-activity.component.ts @@ -206,6 +206,10 @@ export class UserActivityComponent extends WorkPackageCommentFieldHandler implem return this.focused; } + setErrors(newErrors:string[]):void { + // interface + } + public quotedText(rawComment:string) { let quoted = rawComment.split('\n') .map(function(line:string) { diff --git a/frontend/src/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts b/frontend/src/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts index b7144cae62d..43295c6a5f3 100644 --- a/frontend/src/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts +++ b/frontend/src/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts @@ -27,7 +27,8 @@ // ++ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageEditingService} from 'core-components/wp-edit-form/work-package-editing-service'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {ChangeDetectorRef, Component, Inject, Input, OnDestroy, OnInit} from '@angular/core'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions"; @@ -54,12 +55,12 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy { constructor(readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, readonly wpCacheService:WorkPackageCacheService, - readonly wpEditing:WorkPackageEditingService) { + readonly halEditing:HalResourceEditingService) { } ngOnInit() { - this.wpEditing - .temporaryEditResource(this.workPackage.id!) + this.halEditing + .temporaryEditResource(this.workPackage) .values$() .pipe( untilComponentDestroyed(this) @@ -97,7 +98,7 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy { } public get status():HalResource|undefined { - if (!this.wpEditing) { + if (!this.halEditing) { return; } @@ -115,6 +116,6 @@ export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy { } private get changeset() { - return this.wpEditing.changeFor(this.workPackage); + return this.halEditing.changeFor(this.workPackage); } } diff --git a/frontend/src/app/components/wp-card-view/services/wp-card-drag-and-drop.service.ts b/frontend/src/app/components/wp-card-view/services/wp-card-drag-and-drop.service.ts index cb0c7b38aa7..126e1d3833f 100644 --- a/frontend/src/app/components/wp-card-view/services/wp-card-drag-and-drop.service.ts +++ b/frontend/src/app/components/wp-card-view/services/wp-card-drag-and-drop.service.ts @@ -3,7 +3,7 @@ import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-r import {WorkPackageViewOrderService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-order.service"; import {States} from "core-components/states.service"; import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {CurrentProjectService} from "core-components/projects/current-project.service"; import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service"; import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service"; @@ -11,6 +11,7 @@ import {DragAndDropHelpers} from "core-app/modules/common/drag-and-drop/drag-and import {WorkPackageCardViewComponent} from "core-components/wp-card-view/wp-card-view.component"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Injectable() export class WorkPackageCardDragAndDropService { @@ -29,7 +30,7 @@ export class WorkPackageCardDragAndDropService { readonly injector:Injector, readonly reorderService:WorkPackageViewOrderService, readonly wpCreate:WorkPackageCreateService, - readonly wpNotifications:WorkPackageNotificationService, + readonly notificationService:WorkPackageNotificationService, readonly wpCacheService:WorkPackageCacheService, readonly currentProject:CurrentProjectService, readonly wpInlineCreate:WorkPackageInlineCreateService) { @@ -168,7 +169,7 @@ export class WorkPackageCardDragAndDropService { this.updateOrder(newOrder); return true; } catch (e) { - this.wpNotifications.handleRawError(e, workPackage); + this.notificationService.handleRawError(e, workPackage); } return false; diff --git a/frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass b/frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass index b53b7048dc9..9804b1b8ddc 100644 --- a/frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass +++ b/frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass @@ -59,9 +59,6 @@ left: 0 border-radius: 2px 0 0 2px -wp-edit-field - width: initial - .wp-inline-create-button font-size: 0.9rem padding-top: 1rem diff --git a/frontend/src/app/components/wp-card-view/wp-card-view.component.html b/frontend/src/app/components/wp-card-view/wp-card-view.component.html index fc55434ef46..cdf5385b9bd 100644 --- a/frontend/src/app/components/wp-card-view/wp-card-view.component.html +++ b/frontend/src/app/components/wp-card-view/wp-card-view.component.html @@ -36,21 +36,21 @@
-
- - - + - +
-
+ diff --git a/frontend/src/app/components/wp-card-view/wp-card-view.component.ts b/frontend/src/app/components/wp-card-view/wp-card-view.component.ts index 8c4711210c1..38f2c7a166d 100644 --- a/frontend/src/app/components/wp-card-view/wp-card-view.component.ts +++ b/frontend/src/app/components/wp-card-view/wp-card-view.component.ts @@ -21,7 +21,7 @@ import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service"; import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; import {AngularTrackingHelpers} from "core-components/angular/tracking-functions"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions"; import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const"; import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service"; @@ -36,6 +36,7 @@ import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handle import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service"; import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service"; import {checkedClassName, uiStateLinkClass} from "core-components/wp-fast-table/builders/ui-state-link-builder"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; export type CardViewOrientation = 'horizontal'|'vertical'; @@ -99,7 +100,7 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit { readonly I18n:I18nService, readonly wpCreate:WorkPackageCreateService, readonly wpInlineCreate:WorkPackageInlineCreateService, - readonly wpNotifications:WorkPackageNotificationService, + readonly notificationService:WorkPackageNotificationService, readonly authorisationService:AuthorisationService, readonly causedUpdates:CausedUpdatesService, readonly cdRef:ChangeDetectorRef, diff --git a/frontend/src/app/components/wp-copy/wp-copy.controller.ts b/frontend/src/app/components/wp-copy/wp-copy.controller.ts index 83cdfd6e4ba..599548b5af6 100644 --- a/frontend/src/app/components/wp-copy/wp-copy.controller.ts +++ b/frontend/src/app/components/wp-copy/wp-copy.controller.ts @@ -31,9 +31,11 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r import {WorkPackageCreateController} from 'core-components/wp-new/wp-create.controller'; import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {ChangeDetectionStrategy} from "@angular/core"; +import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; export class WorkPackageCopyController extends WorkPackageCreateController { private __initialized_at:Number; @@ -43,7 +45,8 @@ export class WorkPackageCopyController extends WorkPackageCreateController { public copying = true; private wpRelations:WorkPackageRelationsService = this.injector.get(WorkPackageRelationsService); - protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + protected halEditing:HalResourceEditingService = this.injector.get(HalResourceEditingService); + protected wpCreate:WorkPackageCreateService = this.injector.get(WorkPackageCreateService); ngOnInit() { super.ngOnInit(); @@ -78,15 +81,15 @@ export class WorkPackageCopyController extends WorkPackageCreateController { } private createCopyFrom(wp:WorkPackageResource) { - let sourceChangeset = this.wpEditing.changeFor(wp); + let sourceChangeset = this.halEditing.changeFor(wp) as WorkPackageChangeset; return this.wpCreate .copyWorkPackage(sourceChangeset) - .then((copyChangeset) => { + .then((copyChangeset:WorkPackageChangeset) => { this.__initialized_at = copyChangeset.pristineResource.__initialized_at; this.wpCacheService.updateWorkPackage(copyChangeset.pristineResource); - this.wpEditing.updateValue('new', copyChangeset); + this.halEditing.updateValue('new', copyChangeset); return copyChangeset; }); diff --git a/frontend/src/app/components/wp-custom-actions/wp-custom-actions/wp-custom-action.component.ts b/frontend/src/app/components/wp-custom-actions/wp-custom-actions/wp-custom-action.component.ts index ccd4befe8e9..63b2cb5dfbb 100644 --- a/frontend/src/app/components/wp-custom-actions/wp-custom-actions/wp-custom-action.component.ts +++ b/frontend/src/app/components/wp-custom-actions/wp-custom-actions/wp-custom-action.component.ts @@ -30,13 +30,15 @@ import {Component, HostListener, Input, Inject} from '@angular/core'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; import {CustomActionResource} from 'core-app/modules/hal/resources/custom-action-resource'; import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service'; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ selector: 'wp-custom-action', @@ -51,9 +53,9 @@ export class WpCustomActionComponent { private wpCacheService:WorkPackageCacheService, private wpSchemaCacheService:SchemaCacheService, private wpActivity:WorkPackagesActivityService, - private wpNotificationsService:WorkPackageNotificationService, - private wpEditing:WorkPackageEditingService, - private wpEvents:WorkPackageEventsService) { + private notificationService:WorkPackageNotificationService, + private halEditing:HalResourceEditingService, + private halEvents:HalEventsService) { } private fetchAction() { @@ -77,18 +79,18 @@ export class WpCustomActionComponent { this.halResourceService.post(this.action.href + '/execute', payload) .toPromise() .then((savedWp:WorkPackageResource) => { - this.wpNotificationsService.showSave(savedWp, false); + this.notificationService.showSave(savedWp, false); this.workPackage = savedWp; this.wpActivity.clear(this.workPackage.id!); // Loading the schema might be necessary in cases where the button switches // project or type. this.wpSchemaCacheService.ensureLoaded(savedWp).then(() => { this.wpCacheService.updateWorkPackage(savedWp, true); - this.wpEditing.stopEditing(savedWp.id!); - this.wpEvents.push({ type: "updated", id: savedWp.id! }); + this.halEditing.stopEditing(savedWp); + this.halEvents.push(savedWp, { eventType: "updated" }); }); }).catch((errorResource:any) => { - this.wpNotificationsService.handleRawError(errorResource, this.workPackage); + this.notificationService.handleRawError(errorResource, this.workPackage); }); } diff --git a/frontend/src/app/components/wp-details/wp-details-toolbar.component.ts b/frontend/src/app/components/wp-details/wp-details-toolbar.component.ts index 14f4361b677..398f51adac4 100644 --- a/frontend/src/app/components/wp-details/wp-details-toolbar.component.ts +++ b/frontend/src/app/components/wp-details/wp-details-toolbar.component.ts @@ -25,7 +25,8 @@ // // See doc/COPYRIGHT.rdoc for more details. //++ -import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-service'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {Component, Inject, Input} from '@angular/core'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; @@ -42,5 +43,5 @@ export class WorkPackageSplitViewToolbarComponent { } constructor(readonly I18n:I18nService, - readonly wpEditing:WorkPackageEditingService) {} + readonly halEditing:HalResourceEditingService) {} } diff --git a/frontend/src/app/components/wp-edit-form/display-field-renderer.ts b/frontend/src/app/components/wp-edit-form/display-field-renderer.ts index 6979c4e0b2c..105df88d2c6 100644 --- a/frontend/src/app/components/wp-edit-form/display-field-renderer.ts +++ b/frontend/src/app/components/wp-edit-form/display-field-renderer.ts @@ -1,24 +1,23 @@ -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {Injector} from '@angular/core'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {IFieldSchema} from "core-app/modules/fields/field.base"; import {DisplayFieldContext, DisplayFieldService} from "core-app/modules/fields/display/display-field.service"; import {DisplayField} from "core-app/modules/fields/display/display-field.module"; -import {MultipleLinesStringObjectsDisplayField} from "core-app/modules/fields/display/field-types/wp-display-multiple-lines-string-objects-field.module"; -import {ProgressTextDisplayField} from "core-app/modules/fields/display/field-types/wp-display-progress-text-field.module"; -import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; -import {MultipleLinesUserFieldModule} from "core-app/modules/fields/display/field-types/wp-display-multiple-lines-user-field.module"; +import {MultipleLinesStringObjectsDisplayField} from "core-app/modules/fields/display/field-types/multiple-lines-string-objects-display-field.module"; +import {ProgressTextDisplayField} from "core-app/modules/fields/display/field-types/progress-text-display-field.module"; +import {MultipleLinesUserFieldModule} from "core-app/modules/fields/display/field-types/multiple-lines-user-display-field.module"; +import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; export const editableClassName = '-editable'; export const requiredClassName = '-required'; export const readOnlyClassName = '-read-only'; export const placeholderClassName = '-placeholder'; -export const cellClassName = 'wp-table--cell-span'; -export const displayClassName = 'wp-edit-field--display-field'; -export const editFieldContainerClass = 'wp-edit-field--container'; +export const displayClassName = 'inline-edit--display-field'; +export const editFieldContainerClass = 'inline-edit--container'; export const cellEmptyPlaceholder = '-'; -export class DisplayFieldRenderer { +export class DisplayFieldRenderer { readonly displayFieldService:DisplayFieldService = this.injector.get(DisplayFieldService); readonly I18n:I18nService = this.injector.get(I18nService); @@ -27,70 +26,70 @@ export class DisplayFieldRenderer { private fieldCache:{ [key:string]:DisplayField } = {}; constructor(public readonly injector:Injector, - public readonly container:'table' | 'single-view' | 'timeline', - public readonly options:{ [key:string]: any } = {}) { + public readonly container:'table'|'single-view'|'timeline', + public readonly options:{ [key:string]:any } = {}) { } - public render(workPackage:WorkPackageResource, + public render(resource:T, name:string, - change:WorkPackageChangeset|null, + change:ResourceChangeset|null, placeholder = cellEmptyPlaceholder):HTMLSpanElement { - const [field, span] = this.renderFieldValue(workPackage, name, change, placeholder); + const [field, span] = this.renderFieldValue(resource, name, change, placeholder); if (field === null) { return span; } - this.setSpanAttributes(span, field, name, workPackage); + this.setSpanAttributes(span, field, name, resource); return span; } - public renderFieldValue(workPackage:WorkPackageResource, + public renderFieldValue(resource:T, name:string, - change:WorkPackageChangeset|null, + change:ResourceChangeset|null, placeholder = cellEmptyPlaceholder):[DisplayField|null, HTMLSpanElement] { const span = document.createElement('span'); - const schemaName = workPackage.getSchemaName(name); - const fieldSchema = workPackage.schema[schemaName]; + const schemaName = this.getSchemaName(resource, change, name); + const fieldSchema = resource.schema[schemaName]; - // If the work package does not have that field, return an empty + // If the resource does not have that field, return an empty // span (e.g., for the table). if (!fieldSchema) { return [null, span]; } - const field = this.getField(workPackage, fieldSchema, schemaName, change); + const field = this.getField(resource, fieldSchema, schemaName, change); field.render(span, this.getText(field, placeholder)); const title = field.title; if (title) { span.setAttribute('title', title); } - span.setAttribute('aria-label', this.getAriaLabel(field, workPackage)); + span.setAttribute('aria-label', this.getAriaLabel(field, resource)); return [field, span]; } - public getField(workPackage:WorkPackageResource, + public getField(resource:T, fieldSchema:IFieldSchema, name:string, - change:WorkPackageChangeset|null):DisplayField { + change:ResourceChangeset|null):DisplayField { let field = this.fieldCache[name]; if (!field) { - field = this.fieldCache[name] = this.getFieldForCurrentContext(workPackage, name, fieldSchema); + field = this.fieldCache[name] = this.getFieldForCurrentContext(resource, name, fieldSchema); } - field.apply(workPackage, fieldSchema); + field.apply(resource, fieldSchema); field.activeChange = change; return field; } - private getFieldForCurrentContext(workPackage:WorkPackageResource, name:string, fieldSchema:IFieldSchema):DisplayField { - const context:DisplayFieldContext = { container: this.container, injector: this.injector, options: this.options }; + private getFieldForCurrentContext(resource:T, name:string, fieldSchema:IFieldSchema):DisplayField { + const context:DisplayFieldContext = {container: this.container, injector: this.injector, options: this.options}; // We handle multi value fields differently in the single view context const isCustomMultiLinesField = ['[]CustomOption'].indexOf(fieldSchema.type) >= 0; @@ -107,7 +106,7 @@ export class DisplayFieldRenderer { return new ProgressTextDisplayField(name, context); } - return this.displayFieldService.getField(workPackage, name, fieldSchema, context); + return this.displayFieldService.getField(resource, name, fieldSchema, context); } private getText(field:DisplayField, placeholder:string):string { @@ -118,8 +117,8 @@ export class DisplayFieldRenderer { } } - private setSpanAttributes(span:HTMLElement, field:DisplayField, name:string, workPackage:WorkPackageResource):void { - span.classList.add(cellClassName, displayClassName, 'inplace-edit', 'wp-edit-field', name); + private setSpanAttributes(span:HTMLElement, field:DisplayField, name:string, resource:T):void { + span.classList.add(displayClassName, name); span.dataset['fieldName'] = name; // Make span tabbable unless it's an id field @@ -133,7 +132,7 @@ export class DisplayFieldRenderer { span.classList.add(placeholderClassName); } - if (field.writable && workPackage.isAttributeEditable(field.name)) { + if (field.writable) { span.classList.add(editableClassName); span.setAttribute('role', 'button'); } else { @@ -141,7 +140,7 @@ export class DisplayFieldRenderer { } } - private getAriaLabel(field:DisplayField, workPackage:WorkPackageResource):string { + private getAriaLabel(field:DisplayField, resource:T):string { let titleContent; let labelContent = this.getLabelContent(field); @@ -157,8 +156,8 @@ export class DisplayFieldRenderer { titleContent = labelContent; } - if (field.writable && workPackage.isAttributeEditable(field.name)) { - return this.I18n.t('js.inplace.button_edit', { attribute: `${field.displayName} ${titleContent}` }); + if (field.writable && resource.isAttributeEditable(field.name)) { + return this.I18n.t('js.inplace.button_edit', {attribute: `${field.displayName} ${titleContent}`}); } else { return `${field.displayName} ${titleContent}`; } @@ -171,4 +170,25 @@ export class DisplayFieldRenderer { return field.valueString; } } + + /** + * Get the schema name from either the changeset, the resource (if available) or + * return the attribute itself. + * + * @param resource + * @param change + * @param name + */ + private getSchemaName(resource:T, change:ResourceChangeset|null, name:string) { + if (change) { + return change.getSchemaName(name); + } + + if (!!resource.getSchemaName) { + return resource.getSchemaName(name); + } + + return name; + + } } diff --git a/frontend/src/app/components/wp-edit-form/single-view-edit-context.ts b/frontend/src/app/components/wp-edit-form/single-view-edit-context.ts deleted file mode 100644 index cf518efa633..00000000000 --- a/frontend/src/app/components/wp-edit-form/single-view-edit-context.ts +++ /dev/null @@ -1,106 +0,0 @@ -// -- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See doc/COPYRIGHT.rdoc for more details. -// ++ - -import {StateService} from '@uirouter/core'; -import {WorkPackageEditFieldGroupComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; -import {WorkPackageEditFieldComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field.component'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {States} from '../states.service'; -import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service'; -import {Injector} from '@angular/core'; -import {WorkPackageEditContext} from 'core-components/wp-edit-form/work-package-edit-context'; -import {WorkPackageEditForm} from 'core-components/wp-edit-form/work-package-edit-form'; -import {WorkPackageEditFieldHandler} from 'core-components/wp-edit-form/work-package-edit-field-handler'; -import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper'; -import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; -import {IFieldSchema} from "core-app/modules/fields/field.base"; -import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service"; - -export class SingleViewEditContext implements WorkPackageEditContext { - - // Injections - public states:States = this.injector.get(States); - public FocusHelper:FocusHelperService = this.injector.get(FocusHelperService); - public $state:StateService = this.injector.get(StateService); - public wpNotificationsService:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService); - public wpEditingPortalService:WorkPackageEditingPortalService = this.injector.get(WorkPackageEditingPortalService); - protected wpTableSelection:WorkPackageViewSelectionService = this.injector.get(WorkPackageViewSelectionService); - - // other fields - public successState:string; - - constructor(readonly injector:Injector, - readonly fieldGroup:WorkPackageEditFieldGroupComponent) { - } - - public async activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise { - return this.fieldCtrl(fieldName).then((ctrl) => { - ctrl.setActive(true); - const container = ctrl.editContainer.nativeElement; - return this.wpEditingPortalService.create( - container, - this.injector, - form, - schema, - fieldName, - errors - ); - }); - } - - public async reset(workPackage:WorkPackageResource, fieldName:string, focus:boolean = false) { - const ctrl = await this.fieldCtrl(fieldName); - ctrl.reset(); - ctrl.deactivate(focus); - } - - public onSaved(isInitial:boolean, savedWorkPackage:WorkPackageResource) { - this.fieldGroup.stopEditingAndLeave(savedWorkPackage, isInitial); - } - - public requireVisible(fieldName:string):Promise { - return new Promise((resolve,) => { - const interval = setInterval(() => { - const field = this.fieldGroup.fields[fieldName]; - - if (field !== undefined) { - clearInterval(interval); - resolve(); - } - }, 50); - }); - } - - public firstField(names:string[]) { - return 'subject'; - } - - private fieldCtrl(name:string):Promise { - return this.fieldGroup.waitForField(name); - } -} diff --git a/frontend/src/app/components/wp-edit-form/table-row-edit-context.ts b/frontend/src/app/components/wp-edit-form/table-edit-form.ts similarity index 50% rename from frontend/src/app/components/wp-edit-form/table-row-edit-context.ts rename to frontend/src/app/components/wp-edit-form/table-edit-form.ts index 6a0e787ebc1..8819877c80b 100644 --- a/frontend/src/app/components/wp-edit-form/table-row-edit-context.ts +++ b/frontend/src/app/components/wp-edit-form/table-edit-form.ts @@ -27,38 +27,57 @@ // ++ import {Injector} from '@angular/core'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {States} from '../states.service'; -import {CellBuilder, editCellContainer, tdClassName} from '../wp-fast-table/builders/cell-builder'; -import {WorkPackageEditContext} from './work-package-edit-context'; -import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler'; -import {WorkPackageEditForm} from './work-package-edit-form'; -import {FocusHelperService} from 'core-app/modules/common/focus/focus-helper'; -import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table'; -import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; +import {ErrorResource} from 'core-app/modules/hal/resources/error-resource'; +import {Observable, Subscription} from 'rxjs'; +import {States} from 'core-components/states.service'; import {IFieldSchema} from "core-app/modules/fields/field.base"; -import {editModeClassName} from "core-app/modules/fields/edit/edit-field.component"; -import {WorkPackageViewColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service"; -export class TableRowEditContext implements WorkPackageEditContext { +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageViewColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service"; +import {FocusHelperService} from "core-app/modules/common/focus/focus-helper"; +import {EditingPortalService} from "core-app/modules/fields/edit/editing-portal/editing-portal-service"; +import {CellBuilder, editCellContainer, tdClassName} from "core-components/wp-fast-table/builders/cell-builder"; +import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table"; +import {EditForm} from "core-app/modules/fields/edit/edit-form/edit-form"; +import {editModeClassName} from "core-app/modules/fields/edit/edit-field.component"; +import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; +import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; + +export const activeFieldContainerClassName = 'inline-edit--active-field'; +export const activeFieldClassName = 'inline-edit--field'; + +export class TableEditForm extends EditForm { // Injections public wpTableColumns:WorkPackageViewColumnsService = this.injector.get(WorkPackageViewColumnsService); + public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); public states:States = this.injector.get(States); public FocusHelper:FocusHelperService = this.injector.get(FocusHelperService); - public wpEditingPortalService:WorkPackageEditingPortalService = this.injector.get(WorkPackageEditingPortalService); - - // other fields - public successState:string; + public editingPortalService:EditingPortalService = this.injector.get(EditingPortalService); // Use cell builder to reset edit fields private cellBuilder = new CellBuilder(this.injector); - constructor(readonly table:WorkPackageTable, - readonly injector:Injector, + // Subscription + private resourceSubscription:Subscription = this.wpCacheService + .requireAndStream(this.workPackageId) + .subscribe((wp) => this.resource = wp); + + constructor(protected readonly injector:Injector, + protected readonly table:WorkPackageTable, public workPackageId:string, public classIdentifier:string) { - // injectorBridge(this); + super(injector); + } + + destroy() { + this.resourceSubscription.unsubscribe(); + super.destroy(); } public findContainer(fieldName:string):JQuery { @@ -69,39 +88,39 @@ export class TableRowEditContext implements WorkPackageEditContext { return this.rowContainer.find(`.${tdClassName}.${fieldName}`).first(); } - public activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise { + public activateField(form:EditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise { return this.waitForContainer(fieldName) - .then((cell) => { + .then((cell) => { - // Forcibly set the width since the edit field may otherwise - // be given more width. Thereby preserve a minimum width of 150. - // To avoid flickering content, the padding is removed, too. - const td = this.findCell(fieldName); - td.addClass(editModeClassName); - var width = parseInt(td.css('width')); - width = width > 150 ? width - 10 : 150; - td.css('max-width', width + 'px'); - td.css('width', width + 'px'); + // Forcibly set the width since the edit field may otherwise + // be given more width. Thereby preserve a minimum width of 150. + // To avoid flickering content, the padding is removed, too. + const td = this.findCell(fieldName); + td.addClass(editModeClassName); + let width = parseInt(td.css('width')); + width = width > 150 ? width - 10 : 150; + td.css('max-width', width + 'px'); + td.css('width', width + 'px'); - return this.wpEditingPortalService.create( - cell, - this.injector, - form, - schema, - fieldName, - errors - ); - }); + return this.editingPortalService.create( + cell, + this.injector, + form, + schema, + fieldName, + errors + ); + }); } - public reset(workPackage:WorkPackageResource, fieldName:string, focus?:boolean) { + public reset(fieldName:string, focus?:boolean) { const cell = this.findContainer(fieldName); const td = this.findCell(fieldName); if (cell.length) { this.findCell(fieldName).css('width', ''); this.findCell(fieldName).css('max-width', ''); - this.cellBuilder.refresh(cell[0], workPackage, fieldName); + this.cellBuilder.refresh(cell[0], this.resource, fieldName); td.removeClass(editModeClassName); if (focus) { @@ -115,11 +134,12 @@ export class TableRowEditContext implements WorkPackageEditContext { return this.waitForContainer(fieldName); } - public firstField(names:string[]) { - return 'subject'; - } - - public onSaved(isInitial:boolean, savedWorkPackage:WorkPackageResource) { + protected focusOnFirstError():void { + // Focus the first field that is erroneous + jQuery(this.table.tableAndTimelineContainer) + .find(`.${activeFieldContainerClassName}.-error .${activeFieldClassName}`) + .first() + .trigger('focus'); } // Ensure the given field is visible. @@ -140,4 +160,5 @@ export class TableRowEditContext implements WorkPackageEditContext { private get rowContainer() { return jQuery(this.table.tableAndTimelineContainer).find(`.${this.classIdentifier}-table`); } + } diff --git a/frontend/src/app/components/wp-edit-form/work-package-edit-context.ts b/frontend/src/app/components/wp-edit-form/work-package-edit-context.ts deleted file mode 100644 index 616aef51933..00000000000 --- a/frontend/src/app/components/wp-edit-form/work-package-edit-context.ts +++ /dev/null @@ -1,59 +0,0 @@ -// -- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2017 the OpenProject Foundation (OPF) -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2017 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See doc/COPYRIGHT.rdoc for more details. -// ++ - -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageEditForm} from './work-package-edit-form'; -import {WorkPackageEditFieldHandler} from './work-package-edit-field-handler'; -import {IFieldSchema} from "core-app/modules/fields/field.base"; - -export interface WorkPackageEditContext { - /** - * Activate the field, returning the element and associated field handler - */ - activateField(form:WorkPackageEditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise; - - /** - * Show this required field. E.g., add the necessary column - */ - requireVisible(fieldName:string):Promise; - - /** - * Reset the field and re-render the current WPs value. - */ - reset(workPackage:WorkPackageResource, fieldName:string, focus?:boolean):void; - - /** - * Return the first relevant field from the given list of attributes. - */ - firstField(names:string[]):string; - - /** - * Optional callback when the form is being saved - */ - onSaved(isInitial:boolean, savedWorkPackage:WorkPackageResource):void; -} diff --git a/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts b/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts index ad16b164a30..eb07d40f41f 100644 --- a/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts +++ b/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts @@ -33,10 +33,11 @@ import {Injector} from "@angular/core"; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service"; import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; import {TypeResource} from "core-app/modules/hal/resources/type-resource"; import {HttpClientModule} from "@angular/common/http"; @@ -51,9 +52,10 @@ import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/op-file-upload.service"; import {HookService} from "core-app/modules/plugins/hook-service"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; import {TimezoneService} from "core-components/datetime/timezone.service"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; describe('WorkPackageFilterValues', () => { let resource:WorkPackageResource; @@ -76,7 +78,7 @@ describe('WorkPackageFilterValues', () => { I18nService, States, IsolatedQuerySpace, - WorkPackageEventsService, + HalEventsService, TimezoneService, PathHelperService, ConfigurationService, @@ -87,11 +89,12 @@ describe('WorkPackageFilterValues', () => { WorkPackageDmService, HalResourceService, NotificationsService, - WorkPackageNotificationService, + HalResourceNotificationService, SchemaCacheService, + WorkPackageNotificationService, WorkPackageCacheService, WorkPackageCreateService, - WorkPackageEditingService, + HalResourceEditingService, WorkPackagesActivityService, ] }).compileComponents(); diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts deleted file mode 100644 index 4fa0169d22a..00000000000 --- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts +++ /dev/null @@ -1,218 +0,0 @@ -// -- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See doc/COPYRIGHT.rdoc for more details. -// ++ - -import {Component, Injector, Input, OnDestroy, OnInit} from '@angular/core'; -import {StateService, Transition, TransitionService} from '@uirouter/core'; -import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; -import {WorkPackageEditFieldComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field.component'; -import {WorkPackageViewFocusService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service'; -import {componentDestroyed} from 'ng2-rx-componentdestroyed'; -import {input} from 'reactivestates'; -import {filter, map, take, takeUntil} from 'rxjs/operators'; -import {States} from '../../states.service'; -import {SingleViewEditContext} from '../../wp-edit-form/single-view-edit-context'; -import {WorkPackageEditForm} from '../../wp-edit-form/work-package-edit-form'; -import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageNotificationService} from '../wp-notification.service'; -import {WorkPackageCreateService} from './../../wp-new/wp-create.service'; -import {I18nService} from "core-app/modules/common/i18n/i18n.service"; -import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service"; - -@Component({ - selector: 'wp-edit-field-group,[wp-edit-field-group]', - template: '' -}) -export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy { - @Input('workPackage') workPackage:WorkPackageResource; - @Input('successState') successState?:string; - @Input('inEditMode') initializeEditMode:boolean = false; - - public form:WorkPackageEditForm; - public fields:{ [attribute:string]:WorkPackageEditFieldComponent } = {}; - private registeredFields = input(); - private unregisterListener:Function; - - constructor(protected states:States, - protected injector:Injector, - protected wpCreate:WorkPackageCreateService, - protected wpEditing:WorkPackageEditingService, - protected wpNotificationsService:WorkPackageNotificationService, - protected wpTableSelection:WorkPackageViewSelectionService, - protected wpTableFocus:WorkPackageViewFocusService, - protected $transitions:TransitionService, - protected ConfigurationService:ConfigurationService, - readonly $state:StateService, - readonly I18n:I18nService) { - - const confirmText = I18n.t('js.work_packages.confirm_edit_cancel'); - const requiresConfirmation = ConfigurationService.warnOnLeavingUnsaved(); - - this.unregisterListener = $transitions.onBefore({}, (transition:Transition) => { - if (!this.editing) { - return undefined; - } - - // Show confirmation message when transitioning to a new state - // that's not withing the edit mode. - const toState = transition.to(); - const fromState = transition.from(); - const fromParams = transition.params('from'); - const toParams = transition.params('to'); - if (!this.allowedStateChange(toState, toParams, fromState, fromParams)) { - if (requiresConfirmation && !window.confirm(confirmText)) { - return false; - } - - this.stop(); - } - - return true; - }); - } - - ngOnDestroy() { - this.unregisterListener(); - this.form.destroy(); - } - - ngOnInit() { - const context = new SingleViewEditContext(this.injector, this); - this.form = WorkPackageEditForm.createInContext(this.injector, context, this.workPackage, this.initializeEditMode); - - if (this.initializeEditMode) { - this.start(); - } - - // Stop editing whenever a work package was saved - if (this.initializeEditMode && this.workPackage.isNew) { - this.wpCreate.onNewWorkPackage() - .pipe( - takeUntil(componentDestroyed(this)) - ) - .subscribe((wp:WorkPackageResource) => { - this.form.editMode = false; - this.stopEditingAndLeave(wp, true); - }); - } - } - - public get editing():boolean { - return this.editMode || this.form.hasActiveFields(); - } - - public get editMode() { - return this.form.editMode; - } - - public register(field:WorkPackageEditFieldComponent) { - this.fields[field.fieldName] = field; - this.registeredFields.putValue(_.keys(this.fields)); - - const shouldActivate = - (this.editMode && !this.skipField(field) || this.form.activeFields[field.fieldName]); - - if (shouldActivate) { - field.activateOnForm(true); - } - } - - public waitForField(name:string):Promise { - return this.registeredFields - .values$() - .pipe( - filter(keys => keys.indexOf(name) >= 0), - take(1), - map(() => this.fields[name]) - ) - .toPromise(); - } - - public start() { - _.each(this.fields, ctrl => this.form.activate(ctrl.fieldName)); - } - - public stop() { - this.form.editMode = false; - this.wpEditing.stopEditing(this.workPackage.id!); - this.form.destroy(); - - if (this.workPackage.isNew) { - this.wpCreate.cancelCreation(); - } - } - - public saveWorkPackage() { - const isInitial = this.workPackage.isNew; - return this.form - .submit() - .then((savedWorkPackage) => { - this.stopEditingAndLeave(savedWorkPackage, isInitial); - }); - } - - public stopEditingAndLeave(savedWorkPackage:WorkPackageResource, isInitial:boolean) { - this.stop(); - - if (this.successState) { - this.$state.go(this.successState, {workPackageId: savedWorkPackage.id}) - .then(() => { - this.wpTableFocus.updateFocus(savedWorkPackage.id!); - this.wpNotificationsService.showSave(savedWorkPackage, isInitial); - }); - } - } - - private skipField(field:WorkPackageEditFieldComponent) { - const fieldName = field.fieldName; - - const isSkipField = fieldName === 'status' || fieldName === 'type'; - - // Only skip status or type - if (!isSkipField) { - return false; - } - - // Only skip if value present and not changed in changeset - const hasDefault = this.workPackage[fieldName]; - const changed = this.form.change.changes[fieldName]; - - return hasDefault && !changed; - } - - private allowedStateChange(toState:any, toParams:any, fromState:any, fromParams:any) { - - // In new/copy mode, transitions to the same controller are allowed - if (fromState.name.match(/\.(new|copy)$/)) { - return toState.data && toState.data.allowMovingInEditMode; - } - - // When editing an existing WP, transitions on the same WP id are allowed - return toParams.workPackageId !== undefined && toParams.workPackageId === fromParams.workPackageId; - } -} diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts b/frontend/src/app/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts index 4a7fe9a401e..99726e2d12c 100644 --- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts +++ b/frontend/src/app/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts @@ -27,8 +27,8 @@ // ++ -import {WorkPackageEditFieldGroupComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; import {Component, ElementRef, Input, OnInit} from '@angular/core'; +import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component"; @Component({ selector: 'wp-replacement-label', @@ -38,7 +38,7 @@ export class WorkPackageReplacementLabelComponent implements OnInit { @Input('fieldName') public fieldName:string; private $element:JQuery; - constructor(protected wpEditFieldGroup:WorkPackageEditFieldGroupComponent, + constructor(protected wpeditForm:EditFormComponent, protected elementRef:ElementRef) { } @@ -53,7 +53,7 @@ export class WorkPackageReplacementLabelComponent implements OnInit { return true; } - const field = this.wpEditFieldGroup.fields[this.fieldName]; + const field = this.wpeditForm.fields[this.fieldName]; field && field.handleUserActivate(null); return false; diff --git a/frontend/src/app/components/wp-fast-table/builders/cell-builder.ts b/frontend/src/app/components/wp-fast-table/builders/cell-builder.ts index e9102c4c2ba..9d0fc943522 100644 --- a/frontend/src/app/components/wp-fast-table/builders/cell-builder.ts +++ b/frontend/src/app/components/wp-fast-table/builders/cell-builder.ts @@ -7,7 +7,6 @@ import {Injector} from '@angular/core'; import {QueryColumn} from "core-components/wp-query/query-column"; export const tdClassName = 'wp-table--cell-td'; export const editCellContainer = 'wp-table--cell-container'; -export const wpCellTdClassName = 'wp-table--cell-td'; export class CellBuilder { @@ -19,7 +18,7 @@ export class CellBuilder { public build(workPackage:WorkPackageResource, column:QueryColumn) { const td = document.createElement('td'); const attribute = column.id; - td.classList.add(tdClassName, wpCellTdClassName, attribute); + td.classList.add(tdClassName, attribute); if (attribute === 'subject') { td.classList.add('-max'); diff --git a/frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder.ts b/frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder.ts index 7e6575f6b97..da7f5f4999a 100644 --- a/frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder.ts +++ b/frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder.ts @@ -1,5 +1,5 @@ import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; -import {wpCellTdClassName} from "core-components/wp-fast-table/builders/cell-builder"; +import {tdClassName} from "core-components/wp-fast-table/builders/cell-builder"; import {Injector} from "@angular/core"; import {TableDragActionsRegistryService} from "core-components/wp-table/drag-and-drop/actions/table-drag-actions-registry.service"; import {TableDragActionService} from "core-components/wp-table/drag-and-drop/actions/table-drag-action.service"; @@ -29,7 +29,7 @@ export class DragDropHandleBuilder { return td; } - td.classList.add(wpCellTdClassName, 'wp-table--sort-td', internalSortColumn.id, 'hide-when-print'); + td.classList.add(tdClassName, 'wp-table--sort-td', internalSortColumn.id, 'hide-when-print'); // Wrap handle as span let span = document.createElement('span'); diff --git a/frontend/src/app/components/wp-fast-table/builders/primary-render-pass.ts b/frontend/src/app/components/wp-fast-table/builders/primary-render-pass.ts index 483222b9a50..81766538357 100644 --- a/frontend/src/app/components/wp-fast-table/builders/primary-render-pass.ts +++ b/frontend/src/app/components/wp-fast-table/builders/primary-render-pass.ts @@ -3,7 +3,8 @@ import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {timeOutput} from '../../../helpers/debug_output'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {States} from '../../states.service'; -import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageTable} from '../wp-fast-table'; import {RelationRenderInfo, RelationsRenderPass} from './relations/relations-render-pass'; import {SingleRowBuilder} from './rows/single-row-builder'; @@ -36,7 +37,7 @@ export interface RowRenderInfo { export abstract class PrimaryRenderPass { - protected readonly wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + protected readonly halEditing:HalResourceEditingService = this.injector.get(HalResourceEditingService); protected readonly states:States = this.injector.get(States); protected readonly I18n:I18nService = this.injector.get(I18nService); diff --git a/frontend/src/app/components/wp-fast-table/builders/relations/relation-row-builder.ts b/frontend/src/app/components/wp-fast-table/builders/relations/relation-row-builder.ts index 55c9f967e14..b536654a2b2 100644 --- a/frontend/src/app/components/wp-fast-table/builders/relations/relation-row-builder.ts +++ b/frontend/src/app/components/wp-fast-table/builders/relations/relation-row-builder.ts @@ -4,7 +4,7 @@ import {RelationResource} from 'core-app/modules/hal/resources/relation-resource import {States} from '../../../states.service'; import {isRelationColumn, QueryColumn} from '../../../wp-query/query-column'; import {WorkPackageTable} from '../../wp-fast-table'; -import {wpCellTdClassName} from '../cell-builder'; +import {tdClassName} from '../cell-builder'; import {commonRowClassName, SingleRowBuilder, tableRowClassName} from '../rows/single-row-builder'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {RelationColumnType} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-relation-columns.service"; @@ -120,7 +120,7 @@ export class RelationRowBuilder extends SingleRowBuilder { protected emptyRelationCell(column:QueryColumn) { const cell = document.createElement('td'); - cell.classList.add(relationCellClassName, wpCellTdClassName, column.id); + cell.classList.add(relationCellClassName, tdClassName, column.id); return cell; } diff --git a/frontend/src/app/components/wp-fast-table/builders/rows/single-row-builder.ts b/frontend/src/app/components/wp-fast-table/builders/rows/single-row-builder.ts index 7bec3f1e408..84bc19c7d7c 100644 --- a/frontend/src/app/components/wp-fast-table/builders/rows/single-row-builder.ts +++ b/frontend/src/app/components/wp-fast-table/builders/rows/single-row-builder.ts @@ -6,7 +6,7 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r import {isRelationColumn, QueryColumn} from '../../../wp-query/query-column'; import {WorkPackageViewColumnsService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service'; import {WorkPackageTable} from '../../wp-fast-table'; -import {CellBuilder, wpCellTdClassName} from '../cell-builder'; +import {CellBuilder, tdClassName} from '../cell-builder'; import {RelationCellbuilder} from '../relation-cell-builder'; import {checkedClassName} from '../ui-state-link-builder'; import {TableActionRenderer} from 'core-components/wp-fast-table/builders/table-action-renderer'; @@ -141,7 +141,7 @@ export class SingleRowBuilder { */ public refreshRow(workPackage:WorkPackageResource, jRow:JQuery):JQuery { // Detach all current edit cells - const cells = jRow.find(`.${wpCellTdClassName}`).detach(); + const cells = jRow.find(`.${tdClassName}`).detach(); // Remember the order of all new edit cells const newCells:HTMLElement[] = []; @@ -175,7 +175,7 @@ export class SingleRowBuilder { } protected buildEmptyRow(workPackage:WorkPackageResource, row:HTMLTableRowElement):[HTMLTableRowElement, boolean] { - const change = this.workPackageTable.editing.change(workPackage.id!); + const change = this.workPackageTable.editing.change(workPackage); let cells:{ [attribute:string]:JQuery } = {}; if (change && !change.isEmpty()) { @@ -183,7 +183,7 @@ export class SingleRowBuilder { const oldRow = locateTableRowByIdentifier(this.classIdentifier(workPackage)); change.changedAttributes.forEach((attribute:string) => { - cells[attribute] = oldRow.find(`.${wpCellTdClassName}.${attribute}`); + cells[attribute] = oldRow.find(`.${tdClassName}.${attribute}`); }); } diff --git a/frontend/src/app/components/wp-fast-table/builders/table-action-renderer.ts b/frontend/src/app/components/wp-fast-table/builders/table-action-renderer.ts index 3c165a64902..40b960c7c1c 100644 --- a/frontend/src/app/components/wp-fast-table/builders/table-action-renderer.ts +++ b/frontend/src/app/components/wp-fast-table/builders/table-action-renderer.ts @@ -1,5 +1,5 @@ import {Injector} from '@angular/core'; -import {wpCellTdClassName} from './cell-builder'; +import {tdClassName} from './cell-builder'; import {OpTableActionsService} from 'core-components/wp-table/table-actions/table-actions.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {contextMenuSpanClassName, contextMenuTdClassName} from "core-components/wp-table/table-actions/table-action"; @@ -16,7 +16,7 @@ export class TableActionRenderer { public build(workPackage:WorkPackageResource):HTMLElement { // Append details button let td = document.createElement('td'); - td.classList.add(wpCellTdClassName, contextMenuTdClassName, internalContextMenuColumn.id, 'hide-when-print'); + td.classList.add(tdClassName, contextMenuTdClassName, internalContextMenuColumn.id, 'hide-when-print'); // Wrap any actions in a span let span = document.createElement('span'); diff --git a/frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts b/frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts index 4c7d31ca5d2..dc213b9b5d0 100644 --- a/frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts +++ b/frontend/src/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts @@ -1,8 +1,9 @@ import {Injector} from '@angular/core'; import {debugLog} from '../../../../helpers/debug_output'; import {States} from '../../../states.service'; -import {cellClassName, editableClassName, readOnlyClassName} from '../../../wp-edit-form/display-field-renderer'; -import {WorkPackageEditingService} from '../../../wp-edit-form/work-package-editing-service'; +import {displayClassName, editableClassName, readOnlyClassName} from '../../../wp-edit-form/display-field-renderer'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {WorkPackageTable} from '../../wp-fast-table'; import {ClickOrEnterHandler} from '../click-or-enter-handler'; @@ -13,7 +14,7 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa // Injections public states:States = this.injector.get(States); - public wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + public halEditing:HalResourceEditingService = this.injector.get(HalResourceEditingService); // Keep a reference to all @@ -22,7 +23,7 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa } public get SELECTOR() { - return `.${cellClassName}.${editableClassName}`; + return `.${displayClassName}.${editableClassName}`; } public eventScope(table:WorkPackageTable) { @@ -38,7 +39,7 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa evt.preventDefault(); // Locate the cell from event - let target = jQuery(evt.target).closest(`.${cellClassName}`); + let target = jQuery(evt.target).closest(`.${displayClassName}`); // Get the target field name let fieldName = target.data('fieldName'); diff --git a/frontend/src/app/components/wp-fast-table/handlers/row/double-click-handler.ts b/frontend/src/app/components/wp-fast-table/handlers/row/double-click-handler.ts index 89e9bf2c038..eb0b4f7f2fe 100644 --- a/frontend/src/app/components/wp-fast-table/handlers/row/double-click-handler.ts +++ b/frontend/src/app/components/wp-fast-table/handlers/row/double-click-handler.ts @@ -3,7 +3,7 @@ import {StateService} from '@uirouter/core'; import {WorkPackageViewFocusService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service'; import {debugLog} from '../../../../helpers/debug_output'; import {States} from '../../../states.service'; -import {tdClassName, wpCellTdClassName} from '../../builders/cell-builder'; +import {tdClassName} from '../../builders/cell-builder'; import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {WorkPackageTable} from '../../wp-fast-table'; import {TableEventHandler} from '../table-handler-registry'; @@ -27,7 +27,7 @@ export class RowDoubleClickHandler implements TableEventHandler { } public get SELECTOR() { - return `.${wpCellTdClassName}`; + return `.${tdClassName}`; } public eventScope(table:WorkPackageTable) { diff --git a/frontend/src/app/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts b/frontend/src/app/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts index b310c2dfe50..21909a454f9 100644 --- a/frontend/src/app/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts +++ b/frontend/src/app/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts @@ -3,7 +3,7 @@ import {WorkPackageTable} from '../../wp-fast-table'; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {take, takeUntil} from "rxjs/operators"; import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageViewSortByService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-sort-by.service"; import {TableDragActionsRegistryService} from "core-components/wp-table/drag-and-drop/actions/table-drag-actions-registry.service"; import {TableDragActionService} from "core-components/wp-table/drag-and-drop/actions/table-drag-action.service"; @@ -24,7 +24,7 @@ export class DragAndDropTransformer { private readonly querySpace:IsolatedQuerySpace = this.injector.get(IsolatedQuerySpace); private readonly dragService:DragAndDropService|null = this.injector.get(DragAndDropService, null); private readonly inlineCreateService = this.injector.get(WorkPackageInlineCreateService); - private readonly wpNotifications = this.injector.get(WorkPackageNotificationService); + private readonly halNotification = this.injector.get(HalResourceNotificationService); private readonly wpTableSortBy = this.injector.get(WorkPackageViewSortByService); private readonly wpTableOrder = this.injector.get(WorkPackageViewOrderService); private readonly browserDetector = this.injector.get(BrowserDetector); @@ -85,7 +85,7 @@ export class DragAndDropTransformer { await this.wpListService.save(query); } } catch (e) { - this.wpNotifications.handleRawError(e); + this.halNotification.handleRawError(e); // Restore element in from container DragAndDropHelpers.reinsert(el, el.dataset.sourceIndex || -1, source); diff --git a/frontend/src/app/components/wp-fast-table/wp-table-editing.ts b/frontend/src/app/components/wp-fast-table/wp-table-editing.ts index c27f86a1a3d..fb93b66629d 100644 --- a/frontend/src/app/components/wp-fast-table/wp-table-editing.ts +++ b/frontend/src/app/components/wp-fast-table/wp-table-editing.ts @@ -1,41 +1,42 @@ import {Injector} from '@angular/core'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {TableRowEditContext} from '../wp-edit-form/table-row-edit-context'; -import {WorkPackageEditForm} from '../wp-edit-form/work-package-edit-form'; -import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-service'; +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table'; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; +import {EditForm} from "core-app/modules/fields/edit/edit-form/edit-form"; +import {TableEditForm} from "core-components/wp-edit-form/table-edit-form"; export class WorkPackageTableEditingContext { - public wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + public halEditing:HalResourceEditingService = this.injector.get(HalResourceEditingService); constructor(readonly table:WorkPackageTable, readonly injector:Injector) { } - public forms:{ [wpId:string]:WorkPackageEditForm } = {}; + public forms:{ [wpId:string]:TableEditForm } = {}; public reset() { _.each(this.forms, (form) => form.destroy()); this.forms = {}; } - public change(workPackageId:string):WorkPackageChangeset | undefined { - return this.wpEditing.state(workPackageId).value; + public change(workPackage:WorkPackageResource):WorkPackageChangeset|undefined { + return this.halEditing.typedState(workPackage).value; } - public stopEditing(workPackageId:string) { - this.wpEditing.stopEditing(workPackageId); + // TODO + public stopEditing(workPackage:WorkPackageResource) { + this.halEditing.stopEditing(workPackage); - const existing = this.forms[workPackageId]; + const existing = this.forms[workPackage.id!]; if (existing) { existing.destroy(); - delete this.forms[workPackageId]; + delete this.forms[workPackage.id!]; } } - public startEditing(workPackage:WorkPackageResource, classIdentifier:string):WorkPackageEditForm { + public startEditing(workPackage:WorkPackageResource, classIdentifier:string):EditForm { const wpId = workPackage.id!; const existing = this.forms[wpId]; if (existing) { @@ -43,8 +44,7 @@ export class WorkPackageTableEditingContext { } // Get any existing edit state for this work package - const editContext = new TableRowEditContext(this.table, this.injector, wpId, classIdentifier); - return this.forms[wpId] = WorkPackageEditForm.createInContext(this.injector, editContext, workPackage, false); + return this.forms[wpId] = new TableEditForm(this.injector, this.table, wpId, classIdentifier); } } diff --git a/frontend/src/app/components/wp-form-group/wp-attribute-group.component.ts b/frontend/src/app/components/wp-form-group/wp-attribute-group.component.ts index 1531536218b..af8c00135fc 100644 --- a/frontend/src/app/components/wp-form-group/wp-attribute-group.component.ts +++ b/frontend/src/app/components/wp-form-group/wp-attribute-group.component.ts @@ -28,9 +28,9 @@ import {Component, Injector, Input} from '@angular/core'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; -import {WorkPackageEditFieldGroupComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; import {FieldDescriptor, GroupDescriptor} from 'core-components/work-packages/wp-single-view/wp-single-view.component'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; +import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component"; @Component({ selector: 'wp-attribute-group', @@ -48,7 +48,7 @@ export class WorkPackageFormAttributeGroupComponent { }; constructor(readonly I18n:I18nService, - public wpEditFieldGroup:WorkPackageEditFieldGroupComponent, + public wpeditForm:EditFormComponent, protected injector:Injector) { } @@ -62,6 +62,6 @@ export class WorkPackageFormAttributeGroupComponent { */ public shouldHideField(descriptor:FieldDescriptor) { const field = descriptor.field || descriptor.fields![0]; - return this.wpEditFieldGroup.editMode && !field.writable; + return this.wpeditForm.editMode && !field.writable; } } diff --git a/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html b/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html index 7eeac4c7e12..57b6baea201 100644 --- a/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html +++ b/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html @@ -16,17 +16,16 @@
- - - - + + +
- - + - - +
diff --git a/frontend/src/app/components/wp-inline-create/inline-create-row-builder.ts b/frontend/src/app/components/wp-inline-create/inline-create-row-builder.ts index d1adf37d403..cfcd69a71aa 100644 --- a/frontend/src/app/components/wp-inline-create/inline-create-row-builder.ts +++ b/frontend/src/app/components/wp-inline-create/inline-create-row-builder.ts @@ -2,7 +2,6 @@ import {Injector} from '@angular/core'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {States} from '../states.service'; -import {WorkPackageEditForm} from '../wp-edit-form/work-package-edit-form'; import { commonRowClassName, SingleRowBuilder, @@ -13,8 +12,9 @@ import {WorkPackageTable} from '../wp-fast-table/wp-fast-table'; import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service"; import {WorkPackageViewColumnsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-columns.service"; import {QueryColumn} from "core-components/wp-query/query-column"; -import {wpCellTdClassName} from "core-components/wp-fast-table/builders/cell-builder"; +import {tdClassName} from "core-components/wp-fast-table/builders/cell-builder"; import {internalContextMenuColumn} from "core-components/wp-fast-table/builders/internal-sort-columns"; +import {EditForm} from "core-app/modules/fields/edit/edit-form/edit-form"; export const inlineCreateRowClassName = 'wp-inline-create-row'; export const inlineCreateCancelClassName = 'wp-table--cancel-create-link'; @@ -48,7 +48,7 @@ export class InlineCreateRowBuilder extends SingleRowBuilder { } } - public buildNew(workPackage:WorkPackageResource, form:WorkPackageEditForm):[HTMLElement, boolean] { + public buildNew(workPackage:WorkPackageResource, form:EditForm):[HTMLElement, boolean] { // Get any existing edit state for this work package const [row, hidden] = this.buildEmpty(workPackage); @@ -78,7 +78,7 @@ export class InlineCreateRowBuilder extends SingleRowBuilder { protected buildCancelButton() { const td = document.createElement('td'); - td.classList.add(wpCellTdClassName, 'wp-table--cancel-create-td'); + td.classList.add(tdClassName, 'wp-table--cancel-create-td'); td.innerHTML = ` { @@ -97,7 +97,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { if (this.stateParams['parent_id']) { this.newWorkPackage.parent = - { href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path }; + {href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path}; } // Load the parent simply to display the type name :-/ @@ -123,7 +123,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { window.location.href = url.toString(); } }); - this.wpNotificationsService.handleRawError(error); + this.notificationService.handleRawError(error); } }); } @@ -136,6 +136,24 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { this.$state.go('work-packages.new', this.$state.params); } + public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) { + let {savedResource, isInitial} = params; + + // Shouldn't this always be true in create controller? + // Close all edit fields when saving + if (isInitial && this.editForm && this.editForm.editMode) { + this.editForm.stop(); + } + + if (this.successState) { + this.$state.go(this.successState, {workPackageId: savedResource.id}) + .then(() => { + this.wpViewFocus.updateFocus(savedResource.id!); + this.notificationService.showSave(savedResource, isInitial); + }); + } + } + protected setTitle() { this.titleService.setFirstPart(this.I18n.t('js.work_packages.create.title')); } @@ -151,4 +169,13 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { return this.wpCreate.createOrContinueWorkPackage(project, type); } + + private closeEditFormWhenNewWorkPackageSaved() { + this.wpCreate + .onNewWorkPackage() + .pipe(untilComponentDestroyed(this)) + .subscribe((wp:WorkPackageResource) => { + this.onSaved({savedResource: wp, isInitial: true}); + }); + } } diff --git a/frontend/src/app/components/wp-new/wp-create.service.ts b/frontend/src/app/components/wp-new/wp-create.service.ts index e782d346e5c..2a477bde956 100644 --- a/frontend/src/app/components/wp-new/wp-create.service.ts +++ b/frontend/src/app/components/wp-new/wp-create.service.ts @@ -33,14 +33,21 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; import {HookService} from 'core-app/modules/plugins/hook-service'; import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import { + HalResourceEditingService, + ResourceChangesetCommit +} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {filter} from "rxjs/operators"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service"; import {FormResource} from "core-app/modules/hal/resources/form-resource"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; + +export const newWorkPackageHref = '/api/v3/work_packages/new'; @Injectable() export class WorkPackageCreateService implements OnDestroy { @@ -54,17 +61,29 @@ export class WorkPackageCreateService implements OnDestroy { protected wpCacheService:WorkPackageCacheService, protected halResourceService:HalResourceService, protected readonly querySpace:IsolatedQuerySpace, - protected wpEditing:WorkPackageEditingService, + protected halEditing:HalResourceEditingService, protected workPackageDmService:WorkPackageDmService, - protected readonly wpEvents:WorkPackageEventsService) { + protected readonly halEvents:HalEventsService) { - this.wpEditing + this.halEditing .comittedChanges .pipe( untilComponentDestroyed(this), - filter(commit => commit.wasNew) + filter(commit => commit.resource._type === 'WorkPackage' && commit.wasNew) ) - .subscribe(commit => this.newWorkPackageCreated(commit.workPackage)); + .subscribe((commit:ResourceChangesetCommit) => { + this.newWorkPackageCreated(commit.resource); + }); + + this.halEditing + .changes$(newWorkPackageHref) + .pipe( + untilComponentDestroyed(this), + filter(changeset => !changeset) + ) + .subscribe(() => { + this.reset(); + }); } ngOnDestroy() { @@ -72,8 +91,8 @@ export class WorkPackageCreateService implements OnDestroy { } protected newWorkPackageCreated(wp:WorkPackageResource) { - this.form = undefined; - this.wpEvents.push({ type: 'created', id: wp.id! }); + this.halEvents.push(wp, { eventType: 'created' }); + this.reset(); this.newWorkPackageCreatedSubject.next(wp); } @@ -93,11 +112,11 @@ export class WorkPackageCreateService implements OnDestroy { }); } - public fromCreateForm(form:FormResource) { + public fromCreateForm(form:FormResource):WorkPackageChangeset { let wp = this.halResourceService.createHalResourceOfType('WorkPackage', form.payload.$plain()); wp.initializeNewResource(form); - const change = this.wpEditing.edit(wp, form); + const change = this.halEditing.edit(wp, form); // Call work package initialization hook this.hooks.call('workPackageNewInitialization', change); @@ -128,7 +147,7 @@ export class WorkPackageCreateService implements OnDestroy { wp.initializeNewResource(form); - return this.wpEditing.edit(wp, form); + return this.halEditing.edit(wp, form); } @@ -141,15 +160,14 @@ export class WorkPackageCreateService implements OnDestroy { } public cancelCreation() { - this.wpEditing.stopEditing('new'); - this.wpCacheService.clearSome('new'); - this.form = undefined; + this.halEditing.stopEditing({ href: newWorkPackageHref }); + this.reset(); } public changesetUpdates$() { return this - .wpEditing - .state('new') + .halEditing + .state(newWorkPackageHref) .values$(); } @@ -160,16 +178,21 @@ export class WorkPackageCreateService implements OnDestroy { changePromise = this.createNewWithDefaults(projectIdentifier, type); } - return changePromise.then((change) => { - this.wpEditing.updateValue('new', change); + return changePromise.then((change:WorkPackageChangeset) => { + this.halEditing.updateValue(newWorkPackageHref, change); this.wpCacheService.updateWorkPackage(change.pristineResource); return change; }); } + protected reset() { + this.wpCacheService.clearSome('new'); + this.form = undefined; + } + protected continueExistingEdit(type?:number) { - const change = this.wpEditing.state('new').value; + const change = this.halEditing.state(newWorkPackageHref).value as WorkPackageChangeset; if (change !== undefined) { const changeType = change.projectedResource.type; diff --git a/frontend/src/app/components/wp-new/wp-new-full-view.html b/frontend/src/app/components/wp-new/wp-new-full-view.html index ef1b5d2cf5a..028235c47b5 100644 --- a/frontend/src/app/components/wp-new/wp-new-full-view.html +++ b/frontend/src/app/components/wp-new/wp-new-full-view.html @@ -1,9 +1,10 @@
- + class="work-package--new-state work-packages--new work-packages--show-view toolbar-container" + *ngIf="newWorkPackage"> +
@@ -26,6 +27,7 @@ - - + + +
diff --git a/frontend/src/app/components/wp-new/wp-new-split-view.html b/frontend/src/app/components/wp-new/wp-new-split-view.html index 4bfe45ae4a5..f32620511f6 100644 --- a/frontend/src/app/components/wp-new/wp-new-split-view.html +++ b/frontend/src/app/components/wp-new/wp-new-split-view.html @@ -2,10 +2,11 @@ class="work-packages--details work-packages--new" *ngIf="newWorkPackage" > - -
+ +
@@ -21,12 +22,14 @@
- + +
- +
diff --git a/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts b/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts index 96b0804dd25..91628ed349e 100644 --- a/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts +++ b/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts @@ -43,7 +43,7 @@ import {WorkPackageCacheService} from "core-components/work-packages/work-packag import {filter} from "rxjs/operators"; import {QueryResource} from "core-app/modules/hal/resources/query-resource"; import {GroupDescriptor} from "core-components/work-packages/wp-single-view/wp-single-view.component"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; @Component({ selector: 'wp-children-query', @@ -74,7 +74,7 @@ export class WorkPackageChildrenQueryComponent extends WorkPackageRelationQueryB constructor(protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService, protected PathHelper:PathHelperService, protected wpInlineCreate:WorkPackageInlineCreateService, - protected wpEvents:WorkPackageEventsService, + protected halEvents:HalEventsService, protected wpCacheService:WorkPackageCacheService, protected queryUrlParamsHelper:UrlParamsHelperService, readonly I18n:I18nService) { @@ -92,9 +92,8 @@ export class WorkPackageChildrenQueryComponent extends WorkPackageRelationQueryB this.wpInlineCreate.newInlineWorkPackageCreated .pipe(untilComponentDestroyed(this)) .subscribe((toId:string) => { - this.wpEvents.push({ - type: 'association', - id: this.workPackage.id!, + this.halEvents.push(this.workPackage, { + eventType: 'association', relatedWorkPackage: toId, relationType: 'child' }); diff --git a/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts b/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts index 15b36c85255..45d8efca88e 100644 --- a/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts +++ b/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts @@ -31,7 +31,7 @@ import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service"; import {WorkPackageInlineCreateComponent} from "core-components/wp-inline-create/wp-inline-create.component"; import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {WpRelationInlineCreateServiceInterface} from "core-components/wp-relations/embedded/wp-relation-inline-create.service.interface"; import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; @@ -39,7 +39,8 @@ import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/iso import {ApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder"; import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper"; import {RelationResource} from "core-app/modules/hal/resources/relation-resource"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ templateUrl: './wp-relation-inline-add-existing.component.html' @@ -58,8 +59,8 @@ export class WpRelationInlineAddExistingComponent { @Inject(WorkPackageInlineCreateService) protected readonly wpInlineCreate:WpRelationInlineCreateServiceInterface, protected wpCacheService:WorkPackageCacheService, protected wpRelations:WorkPackageRelationsService, - protected wpNotificationsService:WorkPackageNotificationService, - protected wpEvents:WorkPackageEventsService, + protected notificationService:WorkPackageNotificationService, + protected halEvents:HalEventsService, protected urlParamsHelper:UrlParamsHelperService, protected querySpace:IsolatedQuerySpace, protected readonly I18n:I18nService) { @@ -77,9 +78,8 @@ export class WpRelationInlineAddExistingComponent { .then(() => { this.wpCacheService.loadWorkPackage(this.workPackage.id!, true); - this.wpEvents.push({ - type: 'association', - id: this.workPackage.id!, + this.halEvents.push(this.workPackage, { + eventType: 'association', relatedWorkPackage: newRelationId, relationType: this.relationType, }); @@ -89,7 +89,7 @@ export class WpRelationInlineAddExistingComponent { this.cancel(); }) .catch((err:any) => { - this.wpNotificationsService.handleRawError(err, this.workPackage); + this.notificationService.handleRawError(err, this.workPackage); this.isDisabled = false; this.cancel(); }); diff --git a/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts b/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts index a32f75746c7..14d1db9412c 100644 --- a/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts +++ b/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts @@ -37,12 +37,12 @@ import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/w import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {WorkPackageRelationQueryBase} from "core-components/wp-relations/embedded/wp-relation-query.base"; import {WpRelationInlineCreateService} from "core-components/wp-relations/embedded/relations/wp-relation-inline-create.service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service"; import {filter} from "rxjs/operators"; import {QueryResource} from "core-app/modules/hal/resources/query-resource"; import {GroupDescriptor} from "core-components/work-packages/wp-single-view/wp-single-view.component"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ selector: 'wp-relation-query', @@ -65,7 +65,7 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB this.embeddedTable.loadingIndicator = this.wpRelations.require(relatedTo.id!) .then(() => this.wpInlineCreate.remove(this.workPackage, relatedTo)) .then(() => this.refreshTable()) - .catch((error) => this.wpNotifications.handleRawError(error, this.workPackage)); + .catch((error) => this.notificationService.handleRawError(error, this.workPackage)); }, (child:WorkPackageResource) => !!child.changeParent ) @@ -74,9 +74,9 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB constructor(protected readonly PathHelper:PathHelperService, @Inject(WorkPackageInlineCreateService) protected readonly wpInlineCreate:WpRelationInlineCreateService, protected readonly wpRelations:WorkPackageRelationsService, - protected readonly wpEvents:WorkPackageEventsService, + protected readonly halEvents:HalEventsService, protected readonly queryUrlParamsHelper:UrlParamsHelperService, - protected readonly wpNotifications:WorkPackageNotificationService, + protected readonly notificationService:WorkPackageNotificationService, protected readonly I18n:I18nService) { super(queryUrlParamsHelper); } @@ -113,14 +113,13 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB this.wpInlineCreate .add(this.workPackage, toId) .then(() => { - this.wpEvents.push({ - type: 'association', - id: this.workPackage.id!, + this.halEvents.push(this.workPackage, { + eventType: 'association', relatedWorkPackage: toId, relationType: this.getRelationTypeFromQuery() }); }) - .catch(error => this.wpNotifications.handleRawError(error, this.workPackage)); + .catch(error => this.notificationService.handleRawError(error, this.workPackage)); } private getRelationTypeFromQuery() { diff --git a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts index b17933d05f3..c7e135181ad 100644 --- a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts @@ -1,5 +1,5 @@ import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageRelationsService} from '../wp-relations.service'; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; @@ -7,7 +7,8 @@ import {RelationResource} from 'core-app/modules/hal/resources/relation-resource import {ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from "@angular/core"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ @@ -56,9 +57,9 @@ export class WorkPackageRelationRowComponent implements OnInit, OnDestroy { }; constructor(protected wpCacheService:WorkPackageCacheService, - protected wpNotificationsService:WorkPackageNotificationService, + protected notificationService:WorkPackageNotificationService, protected wpRelations:WorkPackageRelationsService, - protected wpEvents:WorkPackageEventsService, + protected halEvents:HalEventsService, protected I18n:I18nService, protected cdRef:ChangeDetectorRef, protected PathHelper:PathHelperService) { @@ -130,7 +131,7 @@ export class WorkPackageRelationRowComponent implements OnInit, OnDestroy { this.relation = savedRelation; this.relatedWorkPackage.relatedBy = savedRelation; this.userInputs.showDescriptionEditForm = false; - this.wpNotificationsService.showSave(this.relatedWorkPackage); + this.notificationService.showSave(this.relatedWorkPackage); this.cdRef.detectChanges(); }); } @@ -156,14 +157,14 @@ export class WorkPackageRelationRowComponent implements OnInit, OnDestroy { this.relation, this.selectedRelationType.name) .then((savedRelation:RelationResource) => { - this.wpNotificationsService.showSave(this.relatedWorkPackage); + this.notificationService.showSave(this.relatedWorkPackage); this.relatedWorkPackage.relatedBy = savedRelation; this.relation = savedRelation; this.userInputs.showRelationTypesForm = false; this.cdRef.detectChanges(); }) - .catch((error:any) => this.wpNotificationsService.handleRawError(error, this.workPackage)); + .catch((error:any) => this.notificationService.handleRawError(error, this.workPackage)); } public toggleUserDescriptionForm() { @@ -173,17 +174,16 @@ export class WorkPackageRelationRowComponent implements OnInit, OnDestroy { public removeRelation() { this.wpRelations.removeRelation(this.relation) .then(() => { - this.wpEvents.push({ - type: 'association', - id: this.workPackage.id!, + this.halEvents.push(this.workPackage, { + eventType: 'association', relatedWorkPackage: null, relationType: this.relation.normalizedType(this.workPackage) }); this.wpCacheService.updateWorkPackage(this.relatedWorkPackage); - this.wpNotificationsService.showSave(this.relatedWorkPackage); + this.notificationService.showSave(this.relatedWorkPackage); }) - .catch((err:any) => this.wpNotificationsService.handleRawError(err, + .catch((err:any) => this.notificationService.handleRawError(err, this.relatedWorkPackage)); } } diff --git a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html index aeb1eba9e64..8b45382f745 100644 --- a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html +++ b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html @@ -16,9 +16,9 @@ [textContent]="relatedWorkPackage.type.name"> -
- { + this.ngSelectComponent.close(); + }); } public onClose() { - jQuery(this.hiddenOverflowContainer).removeClass('-hidden-overflow'); + // Nothing to do } public repositionDropdown() { diff --git a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.html b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.html index 919750a95af..ec6eff107b2 100644 --- a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.html +++ b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.html @@ -7,6 +7,6 @@ id: handler.htmlId, createAllowed: false, finishedLoading: true, - classes: 'wp-inline-edit--field ' + handler.fieldName }" + classes: 'inline-edit--field ' + handler.fieldName }" [ndcDynamicOutputs]="referenceOutputs"> diff --git a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts index 5b93411420f..4fa19648f99 100644 --- a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts @@ -39,7 +39,7 @@ import {SelectAutocompleterRegisterService} from "app/modules/fields/edit/field- export interface ValueOption { name:string; - $href:string | null; + $href:string|null; } @Component({ @@ -50,7 +50,6 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn public options:any[]; public valueOptions:ValueOption[]; - public template:string = '/components/wp-edit/field-types/wp-edit-select-field.directive.html'; public text:{ requiredPlaceholder:string, placeholder:string }; public appendTo:any = null; @@ -153,7 +152,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn public onCreate(newElement:HalResource) { this.addValue(newElement); - this.selectedOption = { name: newElement.name, $href: newElement.$href }; + this.selectedOption = {name: newElement.name, $href: newElement.$href}; this.handler.handleUserSubmit(); } @@ -167,9 +166,19 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn // Nothing to do } - public onChange(value:HalResource) { - this.selectedOption = { name: value.name, $href: value.$href }; - this.handler.handleUserSubmit(); + public onChange(value:HalResource|undefined) { + if (value !== undefined) { + this.selectedOption = {name: value.name, $href: value.$href}; + this.handler.handleUserSubmit(); + return; + } + + const emptyOption = this.getEmptyOption(); + + if (emptyOption) { + this.selectedOption = emptyOption; + this.handler.handleUserSubmit(); + } } private addEmptyOption() { @@ -180,7 +189,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn // Since we use the original schema values, avoid adding // the option if one is returned / exists already. - const emptyOption = _.find(this.options, el => el.name === this.text.placeholder); + const emptyOption = this.getEmptyOption(); if (emptyOption === undefined) { this.options.unshift({ name: this.text.placeholder, @@ -188,4 +197,8 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn }); } } + + private getEmptyOption():ValueOption|undefined { + return _.find(this.options, el => el.name === this.text.placeholder); + } } diff --git a/frontend/src/app/modules/fields/edit/field-types/text-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/text-edit-field.component.ts index 9d2eafd0b05..16dd2c80feb 100644 --- a/frontend/src/app/modules/fields/edit/field-types/text-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/text-edit-field.component.ts @@ -32,7 +32,7 @@ import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.compon @Component({ template: ` diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html b/frontend/src/app/modules/fields/edit/field/editable-attribute-field.component.html similarity index 100% rename from frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html rename to frontend/src/app/modules/fields/edit/field/editable-attribute-field.component.html diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field/editable-attribute-field.component.ts similarity index 76% rename from frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts rename to frontend/src/app/modules/fields/edit/field/editable-attribute-field.component.ts index 5776fc88e42..ad77fdc4134 100644 --- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field/editable-attribute-field.component.ts @@ -26,29 +26,29 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; -import {WorkPackageNotificationService} from '../wp-notification.service'; -import {States} from '../../states.service'; +import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {States} from 'core-components/states.service'; import { displayClassName, DisplayFieldRenderer, editFieldContainerClass -} from '../../wp-edit-form/display-field-renderer'; -import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; -import {SelectionHelpers} from '../../../helpers/selection-helpers'; -import {debugLog} from '../../../helpers/debug_output'; +} from 'core-components/wp-edit-form/display-field-renderer'; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; +import {SelectionHelpers} from '../../../../helpers/selection-helpers'; +import {debugLog} from '../../../../helpers/debug_output'; import { - ChangeDetectionStrategy, ChangeDetectorRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, - Inject, Injector, - Input, OnDestroy, + Input, + OnDestroy, OnInit, ViewChild } from '@angular/core'; -import {WorkPackageEditFieldGroupComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; @@ -56,15 +56,17 @@ import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {IFieldSchema} from "core-app/modules/fields/field.base"; import {ClickPositionMapper} from "core-app/modules/common/set-click-position/set-click-position"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; +import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; @Component({ - selector: 'wp-edit-field', + selector: 'editable-attribute-field', changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './wp-edit-field.html' + templateUrl: './editable-attribute-field.component.html' }) -export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { +export class EditableAttributeFieldComponent implements OnInit, OnDestroy { @Input('fieldName') public fieldName:string; - @Input('workPackageId') public workPackageId:string; + @Input('resource') public resource:HalResource; @Input('wrapperClasses') public wrapperClasses?:string; @Input('displayFieldOptions') public displayFieldOptions:any = {}; @Input('displayPlaceholder') public displayPlaceholder?:string; @@ -73,7 +75,6 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { @ViewChild('displayContainer', { static: true }) readonly displayContainer:ElementRef; @ViewChild('editContainer', { static: true }) readonly editContainer:ElementRef; - public workPackage:WorkPackageResource; public fieldRenderer:DisplayFieldRenderer; public editFieldContainerClass = editFieldContainerClass; public active = false; @@ -84,13 +85,13 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { constructor(protected states:States, protected injector:Injector, protected elementRef:ElementRef, - protected wpNotificationsService:WorkPackageNotificationService, + protected halNotification:HalResourceNotificationService, protected ConfigurationService:ConfigurationService, protected opContextMenu:OPContextMenuService, - protected wpEditing:WorkPackageEditingService, + protected halEditing:HalResourceEditingService, protected wpCacheService:WorkPackageCacheService, // Get parent field group from injector - protected wpEditFieldGroup:WorkPackageEditFieldGroupComponent, + protected editForm:EditFormComponent, protected NotificationsService:NotificationsService, protected cdRef:ChangeDetectorRef, protected I18n:I18nService) { @@ -107,16 +108,16 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { public ngOnInit() { this.fieldRenderer = new DisplayFieldRenderer(this.injector, 'single-view', this.displayFieldOptions); this.$element = jQuery(this.elementRef.nativeElement); - this.wpEditFieldGroup.register(this); + this.editForm.register(this); - this.wpEditing - .temporaryEditResource(this.workPackageId) + this.halEditing + .temporaryEditResource(this.resource) .values$() .pipe( untilComponentDestroyed(this) ) .subscribe(workPackage => { - this.workPackage = workPackage; + this.resource = workPackage; this.render(); }); } @@ -137,7 +138,7 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { } public render() { - const el = this.fieldRenderer.render(this.workPackage, this.fieldName, null, this.displayPlaceholder); + const el = this.fieldRenderer.render(this.resource, this.fieldName, null, this.displayPlaceholder); this.displayContainer.nativeElement.innerHTML = ''; this.displayContainer.nativeElement.appendChild(el); } @@ -153,8 +154,8 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { } public get isEditable() { - const fieldSchema = this.workPackage.schema[this.fieldName] as IFieldSchema; - return this.workPackage.isAttributeEditable(this.fieldName) && fieldSchema && fieldSchema.writable; + const fieldSchema = this.resource.schema[this.fieldName] as IFieldSchema; + return this.resource.isAttributeEditable(this.fieldName) && fieldSchema && fieldSchema.writable; } public activateIfEditable(event:JQuery.TriggeredEvent) { @@ -181,17 +182,11 @@ export class WorkPackageEditFieldComponent implements OnInit, OnDestroy { return false; } - public overflowingSelector() { - return this.$element - .closest('.attributes-group') - .data ('groupIdentifier'); - } - public activateOnForm(noWarnings:boolean = false) { // Activate the field this.setActive(true); - return this.wpEditFieldGroup.form + return this.editForm .activate(this.fieldName, noWarnings) .catch(() => this.deactivate(true)); } diff --git a/frontend/src/app/components/wp-edit-form/work-package-editing-service.ts b/frontend/src/app/modules/fields/edit/services/hal-resource-editing.service.ts similarity index 52% rename from frontend/src/app/components/wp-edit-form/work-package-editing-service.ts rename to frontend/src/app/modules/fields/edit/services/hal-resource-editing.service.ts index e17ee3596cb..17c4592d15b 100644 --- a/frontend/src/app/components/wp-edit-form/work-package-editing-service.ts +++ b/frontend/src/app/modules/fields/edit/services/hal-resource-editing.service.ts @@ -27,22 +27,23 @@ // ++ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {combine, deriveRaw, multiInput, MultiInputState, State, StatesGroup} from 'reactivestates'; +import {combine, deriveRaw, InputState, multiInput, MultiInputState, State, StatesGroup} from 'reactivestates'; import {map} from 'rxjs/operators'; -import {StateCacheService} from '../states/state-cache.service'; -import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; import {Injectable, Injector} from '@angular/core'; -import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; import {Subject} from "rxjs"; import {FormResource} from "core-app/modules/hal/resources/form-resource"; import {ChangeMap} from "core-app/modules/fields/changeset/changeset"; +import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {StateCacheService} from "core-components/states/state-cache.service"; +import {HookService} from "core-app/modules/plugins/hook-service"; class ChangesetStates extends StatesGroup { name = 'Changesets'; - changesets = multiInput(); + changesets = multiInput(); constructor() { super(); @@ -55,7 +56,7 @@ class ChangesetStates extends StatesGroup { * used to access the previous save and or previous state * of the work package (e.g., whether it was new). */ -export class WorkPackageChangesetCommit { +export class ResourceChangesetCommit { /** * The work package id of the change * (This is the new work package ID if +wasNew+ is true. @@ -65,7 +66,7 @@ export class WorkPackageChangesetCommit { /** * The resulting, saved work package. */ - public readonly workPackage:WorkPackageResource; + public readonly resource:T; /** Whether the commit saved an initial work package */ public readonly wasNew:boolean = false; @@ -78,36 +79,35 @@ export class WorkPackageChangesetCommit { * @param change The change object that resulted in the save * @param saved The returned work package */ - constructor(change:WorkPackageChangeset, saved:WorkPackageResource) { + constructor(change:ResourceChangeset, saved:T) { this.id = saved.id!.toString(); this.wasNew = change.pristineResource.isNew; - this.workPackage = saved; + this.resource = saved; this.changes = change.changes; } } +export interface ResourceChangesetClass { + new(...args:any[]):ResourceChangeset; +} @Injectable() -export class WorkPackageEditingService extends StateCacheService { +export class HalResourceEditingService extends StateCacheService { /** Committed / saved changes to work packages observable */ - public comittedChanges = new Subject(); + public comittedChanges = new Subject(); /** State group of changes to wrap */ private stateGroup = new ChangesetStates(); - constructor(readonly injector:Injector, - readonly wpActivity:WorkPackagesActivityService, - readonly schemaCache:SchemaCacheService, - readonly wpCacheService:WorkPackageCacheService) { + constructor(protected readonly injector:Injector, + protected readonly hook:HookService) { super(); } - public async save(change:WorkPackageChangeset):Promise { + public async save>(change:T):Promise> { change.inFlight = true; - // TODO remove? const wasNew = change.pristineResource.isNew; - // Form the payload we're going to save const [form, payload] = await change.buildRequestPayload(); // Reject errors when occurring in form validation @@ -117,28 +117,25 @@ export class WorkPackageEditingService extends StateCacheService>(change:T, saved:V):ResourceChangesetCommit { + const commit = new ResourceChangesetCommit(change, saved); this.comittedChanges.next(commit); this.reset(change); @@ -149,44 +146,69 @@ export class WorkPackageEditingService extends StateCacheService>(change:T) { change.clear(); - this.clearSome(change.id); + this.clearSome(change.href); } /** - * Create a new changeset for the given work package, discarding any previous changeset that might exist - * @param workPackage - * @param form + * Returns the typed state value. Use this to get a changeset + * for a subtype of ResourceChangeset. + * @param resource */ - public edit(workPackage:WorkPackageResource, form?:FormResource):WorkPackageChangeset { - const state = this.multiState.get(workPackage.id!); - const changeset = new WorkPackageChangeset(workPackage, state, form); + public typedState>(resource:V):State { + return this.multiState.get(resource.href!) as InputState; + } + + /** + * Create a new changeset for the given work package, discarding any previous changeset that might exist. + * + * @param resource + * @param form + * + * @return The state for the created changeset + */ + public edit>(resource:V, form?:FormResource):T { + const state = this.multiState.get(resource.href!) as InputState; + const changeset = this.newChangeset(resource, state, form); state.putValue(changeset); + return changeset; } + protected newChangeset>(resource:V, state:InputState, form?:FormResource):T { + // we take the last registered group component which means that + // plugins will have their say if they register for it. + const cls = this.hook.call('halResourceChangesetClass', resource).pop() || ResourceChangeset; + return new cls(resource, state, form) as T; + } + /** * Start or continue editing the work package with a given edit context - * @param {workPackage} Work package to edit + * @param {resource} Hal resource to edit * @param {form:FormResource} Initialize with an existing form - * @return {WorkPackageChangeset} Change object to work on + * @return {ResourceChangeset} Change object to work on */ - public changeFor(fallback:WorkPackageResource):WorkPackageChangeset { - const state = this.multiState.get(fallback.id!); - const workPackage = this.wpCacheService.state(fallback.id!).getValueOr(fallback); + public changeFor>(fallback:V):T { + const state = this.multiState.get(fallback.href!) as InputState; + let resource = fallback; + if (fallback.state) { + resource = fallback.state.getValueOr(fallback); + } let changeset = state.value; // If there is no changeset, or // If there is an empty one for a older work package reference // build a new changeset - if (!changeset || (changeset.isEmpty() && changeset.pristineResource.lockVersion < workPackage.lockVersion)) { - return this.edit(workPackage); + if (changeset && !changeset.isEmpty()) { + return changeset; + } + if (!changeset || resource.hasOwnProperty('lockVersion') && changeset.pristineResource.lockVersion < resource.lockVersion) { + return this.edit(resource); } - const change = state.value!; - return change; + return changeset; } /** @@ -199,51 +221,44 @@ export class WorkPackageEditingService extends StateCacheService} + * @return {State} */ - public temporaryEditResource(id:string):State { - const combined = combine(this.wpCacheService.state(id), this.state(id)); + public temporaryEditResource>(resource:V):State { + const combined = combine(resource.state! as State, this.typedState(resource) as State); return deriveRaw(combined, ($) => $ .pipe( - map(([wp, change]) => { - if (wp && change && !change.isEmpty()) { - return change.projectedResource; + map(([resource, change]) => { + if (resource && change && !change.isEmpty()) { + return change.projectedResource as V; } else { - return wp; + return resource; } }) ) ); } - public stopEditing(workPackageId:string) { - this.multiState.get(workPackageId).clear(); + public stopEditing(resource:HalResource|{ href:string }) { + this.multiState.get(resource.href!).clear(); } - protected load(id:string):Promise { - return this.wpCacheService.require(id) - .then((wp:WorkPackageResource) => { - return this.changeFor(wp); - }); + protected load(href:string):Promise { + return Promise.reject('Loading not applicable for changesets.') as any; } - protected onSaved(saved:WorkPackageResource) { - this.wpActivity.clear(saved.id); - - // If there is a parent, its view has to be updated as well - if (saved.parent) { - this.wpCacheService.loadWorkPackage(saved.parent.id.toString(), true); + protected onSaved(saved:HalResource) { + if (saved.state) { + saved.push(saved); } - this.wpCacheService.updateWorkPackage(saved); } - protected loadAll(ids:string[]) { - return Promise.all(ids.map(id => this.load(id))) as any; + protected loadAll(hrefs:string[]) { + return Promise.all(hrefs.map(href => this.load(href))) as any; } - protected get multiState():MultiInputState { + protected get multiState():MultiInputState { return this.stateGroup.changesets; } } diff --git a/frontend/src/app/modules/fields/field.base.ts b/frontend/src/app/modules/fields/field.base.ts index 27b468e03d7..cb5e7b1743b 100644 --- a/frontend/src/app/modules/fields/field.base.ts +++ b/frontend/src/app/modules/fields/field.base.ts @@ -62,7 +62,7 @@ export class Field { } public get writable():boolean { - return !!this.schema.writable; + return this.schema.writable && this.resource.isAttributeEditable(this.name); } public get hasDefault():boolean { diff --git a/frontend/src/app/modules/fields/field.service.ts b/frontend/src/app/modules/fields/field.service.ts index 50ff1cadb51..bccc36b742b 100644 --- a/frontend/src/app/modules/fields/field.service.ts +++ b/frontend/src/app/modules/fields/field.service.ts @@ -67,6 +67,17 @@ export abstract class AbstractFieldService `${resourceType}-${attribute}`); + this.register(fieldClass, attributes); + + return this; + } + /** * Register new schema attribute names for an existing field type * diff --git a/frontend/src/app/modules/fields/openproject-fields.module.ts b/frontend/src/app/modules/fields/openproject-fields.module.ts index 6d152af20ba..c656c90ac99 100644 --- a/frontend/src/app/modules/fields/openproject-fields.module.ts +++ b/frontend/src/app/modules/fields/openproject-fields.module.ts @@ -41,15 +41,17 @@ import {SelectEditFieldComponent} from "core-app/modules/fields/edit/field-types import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component"; import {TextEditFieldComponent} from "core-app/modules/fields/edit/field-types/text-edit-field.component"; import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module"; -import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service"; +import {EditingPortalService} from "core-app/modules/fields/edit/editing-portal/editing-portal-service"; import {EditFormPortalComponent} from "core-app/modules/fields/edit/editing-portal/edit-form-portal.component"; import {EditFieldControlsComponent,} from "core-app/modules/fields/edit/field-controls/edit-field-controls.component"; import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-a11y.module"; -import {WorkPackageEditFieldComponent} from "core-app/modules/fields/edit/field-types/work-package-edit-field.component"; import {OpenprojectEditorModule} from 'core-app/modules/editor/openproject-editor.module'; import {UserFieldPortalComponent} from "core-app/modules/fields/display/display-portal/display-user-field-portal/user-field-portal.component"; import {UserFieldPortalService} from "core-app/modules/fields/display/display-portal/display-user-field-portal/user-field-portal-service"; import {SelectAutocompleterRegisterService} from "core-app/modules/fields/edit/field-types/select-autocompleter-register.service"; +import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component"; +import {WorkPackageEditFieldComponent} from "core-app/modules/fields/edit/field-types/work-package-edit-field.component"; +import {EditableAttributeFieldComponent} from "core-app/modules/fields/edit/field/editable-attribute-field.component"; @NgModule({ imports: [ @@ -61,9 +63,11 @@ import {SelectAutocompleterRegisterService} from "core-app/modules/fields/edit/f EditFieldControlsComponent, EditFormPortalComponent, UserFieldPortalComponent, + EditFormComponent, + EditableAttributeFieldComponent, ], providers: [ - WorkPackageEditingPortalService, + EditingPortalService, UserFieldPortalService, DisplayFieldService, EditFieldService, @@ -85,6 +89,8 @@ import {SelectAutocompleterRegisterService} from "core-app/modules/fields/edit/f TextEditFieldComponent, EditFieldControlsComponent, WorkPackageEditFieldComponent, + EditFormComponent, + EditableAttributeFieldComponent, ], entryComponents: [ EditFormPortalComponent, @@ -99,6 +105,7 @@ import {SelectAutocompleterRegisterService} from "core-app/modules/fields/edit/f SelectEditFieldComponent, TextEditFieldComponent, WorkPackageEditFieldComponent, + EditableAttributeFieldComponent, ] }) export class OpenprojectFieldsModule { } diff --git a/frontend/src/app/modules/global_search/input/global-search-input.component.ts b/frontend/src/app/modules/global_search/input/global-search-input.component.ts index a8be2849619..060008048f6 100644 --- a/frontend/src/app/modules/global_search/input/global-search-input.component.ts +++ b/frontend/src/app/modules/global_search/input/global-search-input.component.ts @@ -51,7 +51,7 @@ import {NgSelectComponent} from "@ng-select/ng-select"; import {Observable, of} from "rxjs"; import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions"; import {map} from "rxjs/internal/operators"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {DebouncedRequestSwitchmap, errorNotificationHandler} from "core-app/helpers/rxjs/debounced-input-switchmap"; import {LinkHandling} from "core-app/modules/common/link-handling/link-handling"; @@ -83,7 +83,7 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy { /** Keep a switchmap for search term and loading state */ public requests = new DebouncedRequestSwitchmap( (searchTerm:string) => this.autocompleteWorkPackages(searchTerm), - errorNotificationHandler(this.wpNotification) + errorNotificationHandler(this.halNotification) ); /** Remember the current value */ @@ -108,7 +108,7 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy { readonly currentProjectService:CurrentProjectService, readonly deviceService:DeviceService, readonly cdRef:ChangeDetectorRef, - readonly wpNotification:WorkPackageNotificationService) { + readonly halNotification:HalResourceNotificationService) { } ngOnInit() { diff --git a/frontend/src/app/modules/grids/openproject-grids.module.ts b/frontend/src/app/modules/grids/openproject-grids.module.ts index f0dd7f06e78..bf70ec3f5d4 100644 --- a/frontend/src/app/modules/grids/openproject-grids.module.ts +++ b/frontend/src/app/modules/grids/openproject-grids.module.ts @@ -63,6 +63,7 @@ import {WidgetTimeEntriesProjectComponent} from "core-app/modules/grids/widgets/ import {WidgetSubprojectsComponent} from "core-app/modules/grids/widgets/subprojects/subprojects.component"; import {OpenprojectAttachmentsModule} from "core-app/modules/attachments/openproject-attachments.module"; import {WidgetMembersComponent} from "core-app/modules/grids/widgets/members/members.component"; +import {OpenprojectProjectsModule} from "core-app/modules/projects/openproject-projects.module"; @NgModule({ imports: [ diff --git a/frontend/src/app/modules/grids/widgets/custom-text/custom-text-edit-field.service.ts b/frontend/src/app/modules/grids/widgets/custom-text/custom-text-edit-field.service.ts index bce7413f985..295b8b2dcae 100644 --- a/frontend/src/app/modules/grids/widgets/custom-text/custom-text-edit-field.service.ts +++ b/frontend/src/app/modules/grids/widgets/custom-text/custom-text-edit-field.service.ts @@ -105,6 +105,10 @@ export class CustomTextEditFieldService extends EditFieldHandler { trigger && trigger.focus(); } + setErrors(newErrors:string[]):void { + // interface + } + handleUserKeydown(event:JQuery.TriggeredEvent, onlyCancel?:boolean):void { // interface } diff --git a/frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html b/frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html index 561621a5c31..4b9f9c06761 100644 --- a/frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html +++ b/frontend/src/app/modules/grids/widgets/custom-text/custom-text.component.html @@ -10,7 +10,7 @@ + class="inline-edit--formattable-display-text -default"> @@ -21,7 +21,7 @@
-
+
+ [ngClass]="{'inline-edit--container': isTextEditable}">
{ @@ -42,11 +43,17 @@ export interface HalResourceClass { } export class HalResource { + // TODO this is the source of many issues in the frontend + // because it no longer properly type checks stuff [attribute:string]:any; // The API type reported from API public _type:string; + // Internal initialization time for objects + // created in the frontend + public __initialized_at:Number; + // The HalResource that this type maps to // This will almost always be equal to _type, however may be different for dynamic types // e.g., { _type: 'StatusFilterInstance', $halType: 'QueryFilterInstance' }. @@ -79,8 +86,8 @@ export class HalResource { this.$initialize($source); } - public static getEmptyResource(self:{ href:string|null } = { href: null }):any { - return { _links: { self: self } }; + public static getEmptyResource(self:{ href:string | null } = {href: null}):any { + return {_links: {self: self}}; } public $links:any = {}; @@ -112,7 +119,7 @@ export class HalResource { * - The embedded ID is actually set * - The self link is terminated by a number. */ - public get id():string|null { + public get id():string | null { if (this.$source.id) { return this.$source.id.toString(); } @@ -125,14 +132,41 @@ export class HalResource { return null; } - public set id(val:string|null) { + public set id(val:string | null) { this.$source.id = val; } + public get isNew():boolean { + return this.id === 'new'; + } + public get persisted() { return !!(this.id && this.id !== 'new'); } + /** + * Return whether the work package is editable with the user's permission + * on the given work package attribute. + * + * @param property + */ + public isAttributeEditable(property:string):boolean { + const fieldSchema = this.schema[property]; + return fieldSchema && fieldSchema.writable; + } + + /** + * Retain the internal tracking identifier from the given other work package. + * This is due to us needing to identify a work package beyond its actual ID, + * because that changes upon saving. + * + * @param other + */ + public retainFrom(other:HalResource) { + this.__initialized_at = other.__initialized_at; + } + + /** * Create a HalResource from the copied source of the given, other HalResource. * @@ -140,7 +174,7 @@ export class HalResource { * @returns A HalResource with the identitical copied source of other. */ public $copy(source:Object = {}):T { - let clone:HalResourceClass = this.constructor as any; + let clone:HalResourceClass = this.constructor as any; return new clone(this.injector, _.merge(this.$plain(), source), this.$loaded, this.halInitializer, this.$halType); } @@ -168,21 +202,38 @@ export class HalResource { /** * Alias for $href. */ - public get href():string|null { + public get href():string | null { return this.$link.href; } - public get $href():string|null { + public get $href():string | null { return this.$link.href; } /** * Return the associated state to this HAL resource, if any. */ - public get state():InputState|null { + public get state():InputState | null { return null; } + /** + * Update the state + */ + public push(newValue:this):void { + if (this.state) { + this.state.putValue(newValue); + } + } + + public previewPath():string|undefined { + if (this.isNew && this.project) { + return this.project.href; + } + + return undefined; + } + public $load(force = false):Promise { if (!this.state) { return this.$loadResource(force); @@ -198,7 +249,7 @@ export class HalResource { // Otherwise, we risk returning a promise, that will never be resolved. state.putFromPromiseIfPristine(() => this.$loadResource(force)); - return > state.valuesPromise().then((source:any) => { + return >state.valuesPromise().then((source:any) => { this.$initialize(source); this.$loaded = true; return this; diff --git a/frontend/src/app/modules/hal/resources/mixins/attachable-mixin.ts b/frontend/src/app/modules/hal/resources/mixins/attachable-mixin.ts index 7adbeb2c425..105ea5082c9 100644 --- a/frontend/src/app/modules/hal/resources/mixins/attachable-mixin.ts +++ b/frontend/src/app/modules/hal/resources/mixins/attachable-mixin.ts @@ -29,7 +29,7 @@ import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; import {AttachmentCollectionResource} from 'core-app/modules/hal/resources/attachment-collection-resource'; import {OpenProjectFileUploadService, UploadFile} from 'core-components/api/op-file-upload/op-file-upload.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; import {HttpErrorResponse} from "@angular/common/http"; @@ -42,7 +42,7 @@ export function Attachable>(Base:TBase) { public attachments:AttachmentCollectionResource; private NotificationsService:NotificationsService; - private wpNotificationsService:WorkPackageNotificationService; + private halNotification:HalResourceNotificationService; private opFileUpload:OpenProjectFileUploadService; private pathHelper:PathHelperService; @@ -102,7 +102,7 @@ export function Attachable>(Base:TBase) { } }) .catch((error:any) => { - this.wpNotificationsService.handleRawError(error, this as any); + this.halNotification.handleRawError(error, this as any); this.attachments.elements.push(attachment); }); } @@ -161,7 +161,7 @@ export function Attachable>(Base:TBase) { message = error.error; } - this.wpNotificationsService.handleRawError(message); + this.halNotification.handleRawError(message); return message || I18n.t('js.error.internal'); }); } @@ -186,7 +186,7 @@ export function Attachable>(Base:TBase) { public $initialize(source:any) { this.NotificationsService = this.injector.get(NotificationsService); - this.wpNotificationsService = this.injector.get( WorkPackageNotificationService); + this.halNotification = this.injector.get( HalResourceNotificationService); this.opFileUpload = this.injector.get(OpenProjectFileUploadService); this.pathHelper = this.injector.get(PathHelperService); diff --git a/frontend/src/app/modules/hal/resources/project-resource.ts b/frontend/src/app/modules/hal/resources/project-resource.ts index 3b68b73b90d..3d4d3563d64 100644 --- a/frontend/src/app/modules/hal/resources/project-resource.ts +++ b/frontend/src/app/modules/hal/resources/project-resource.ts @@ -27,6 +27,37 @@ //++ import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; +import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; +import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; export class ProjectResource extends HalResource { + + private schemaCacheService = this.injector.get(SchemaCacheService); + + public get state() { + return this.states.projects.get(this.id!) as any; + } + + /** + * Get the schema of the project + * ensure that it's loaded + * + * TODO this is duplicating the WorkPackageResource#schema getter + */ + public get schema():SchemaResource { + const state = this.schemaCacheService.state(this as any); + + if (!state.hasValue()) { + throw `Accessing schema of ${this.id} without it being loaded.`; + } + + return state.value!; + } + + /** + * Exclude the schema _link from the linkable Resources. + */ + public $linkableKeys():string[] { + return _.without(super.$linkableKeys(), 'schema'); + } } diff --git a/frontend/src/app/modules/hal/resources/work-package-resource.spec.ts b/frontend/src/app/modules/hal/resources/work-package-resource.spec.ts index 5fc3a20a2a1..f6de065c044 100644 --- a/frontend/src/app/modules/hal/resources/work-package-resource.spec.ts +++ b/frontend/src/app/modules/hal/resources/work-package-resource.spec.ts @@ -34,7 +34,7 @@ import {States} from 'core-components/states.service'; import {TypeDmService} from 'core-app/modules/hal/dm-services/type-dm.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {SchemaCacheService} from 'core-components/schemas/schema-cache.service'; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; @@ -46,13 +46,16 @@ import {StateService} from "@uirouter/core"; import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/op-file-upload.service"; import {WorkPackageCreateService} from 'core-app/components/wp-new/wp-create.service'; import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; +import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service"; +import {TimezoneService} from "core-components/datetime/timezone.service"; describe('WorkPackage', () => { let halResourceService:HalResourceService; let injector:Injector; let wpCacheService:WorkPackageCacheService; let notificationsService:NotificationsService; - let wpNotificationsService:WorkPackageNotificationService; + let halResourceNotification:HalResourceNotificationService; let source:any; let workPackage:WorkPackageResource; @@ -73,13 +76,16 @@ describe('WorkPackage', () => { States, TypeDmService, WorkPackageCacheService, + TimezoneService, + WorkPackagesActivityService, NotificationsService, ConfigurationService, - WorkPackageNotificationService, OpenProjectFileUploadService, LoadingIndicatorService, PathHelperService, I18nService, + { provide: HalResourceNotificationService, useValue: { handleRawError: () => false } }, + { provide: WorkPackageNotificationService, useValue: {} }, { provide: WorkPackageDmService, useValue: {} }, { provide: WorkPackageCreateService, useValue: {} }, { provide: StateService, useValue: {} }, @@ -92,7 +98,7 @@ describe('WorkPackage', () => { injector = TestBed.get(Injector); wpCacheService = injector.get(WorkPackageCacheService); notificationsService = injector.get(NotificationsService); - wpNotificationsService = injector.get(WorkPackageNotificationService); + halResourceNotification = injector.get(HalResourceNotificationService); halResourceService.registerResource('WorkPackage', { cls: WorkPackageResource }); }); @@ -193,7 +199,7 @@ describe('WorkPackage', () => { attachment.delete = jasmine.createSpy('delete') .and.returnValue(Promise.reject({ foo: 'bar'})); - errorStub = spyOn(wpNotificationsService, 'handleRawError'); + errorStub = spyOn(halResourceNotification, 'handleRawError'); }); it('should call the handleRawError notification', (done) => { diff --git a/frontend/src/app/modules/hal/resources/work-package-resource.ts b/frontend/src/app/modules/hal/resources/work-package-resource.ts index 55079327402..d5235c2fd72 100644 --- a/frontend/src/app/modules/hal/resources/work-package-resource.ts +++ b/frontend/src/app/modules/hal/resources/work-package-resource.ts @@ -40,13 +40,15 @@ import {SchemaResource} from 'core-app/modules/hal/resources/schema-resource'; import {States} from 'core-components/states.service'; import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; import {SchemaCacheService} from 'core-components/schemas/schema-cache.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; import {Attachable} from 'core-app/modules/hal/resources/mixins/attachable-mixin'; import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service"; import {FormResource} from "core-app/modules/hal/resources/form-resource"; import {InputState} from "reactivestates"; +import {WorkPackagesActivityService} from "core-components/wp-single-view-tabs/activity-panel/wp-activity.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; export interface WorkPackageResourceEmbedded { activities:CollectionResource; @@ -123,15 +125,14 @@ export class WorkPackageBaseResource extends HalResource { public attachments:AttachmentCollectionResource; public overriddenSchema:SchemaResource|undefined = undefined; - public __initialized_at:Number; - readonly I18n:I18nService = this.injector.get(I18nService); readonly states:States = this.injector.get(States); + readonly wpActivity = this.injector.get(WorkPackagesActivityService); readonly workPackageDmService = this.injector.get(WorkPackageDmService); readonly wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); readonly schemaCacheService:SchemaCacheService = this.injector.get(SchemaCacheService); readonly NotificationsService:NotificationsService = this.injector.get(NotificationsService); - readonly wpNotificationsService:WorkPackageNotificationService = this.injector.get( + readonly workPackageNotificationService:WorkPackageNotificationService = this.injector.get( WorkPackageNotificationService); readonly pathHelper:PathHelperService = this.injector.get(PathHelperService); readonly opFileUpload:OpenProjectFileUploadService = this.injector.get(OpenProjectFileUploadService); @@ -170,10 +171,6 @@ export class WorkPackageBaseResource extends HalResource { return `${subject}${id}`; } - public get isNew():boolean { - return this.id === 'new'; - } - public get isMilestone():boolean { return this.schema.hasOwnProperty('date'); } @@ -193,6 +190,14 @@ export class WorkPackageBaseResource extends HalResource { return this.isNew || !!this.$links.update; } + public previewPath() { + if (!this.isNew) { + return this.pathHelper.api.v3.work_packages.id(this.id!).path; + } else { + return super.previewPath(); + } + } + /** * Return whether the work package is editable with the user's permission * on the given work package attribute. @@ -278,17 +283,6 @@ export class WorkPackageBaseResource extends HalResource { }; } - /** - * Retain the internal tracking identifier from the given other work package. - * This is due to us needing to identify a work package beyond its actual ID, - * because that changes upon saving. - * - * @param other - */ - public retainFrom(other:WorkPackageResource) { - this.__initialized_at = other.__initialized_at; - } - public $initialize(source:any) { super.$initialize(source); @@ -338,6 +332,20 @@ export class WorkPackageBaseResource extends HalResource { return this.states.workPackages.get(this.id!) as any; } + /** + * Update the state + */ + public push(newValue:this):void { + this.wpActivity.clear(newValue.id!); + + // If there is a parent, its view has to be updated as well + if (newValue.parent) { + this.wpCacheService.require(newValue.parent.id!, true); + } + + this.wpCacheService.updateWorkPackage(newValue as any); + } + public get hasOverriddenSchema():boolean { return this.overriddenSchema != null; } diff --git a/frontend/src/app/modules/hal/services/hal-events.service.ts b/frontend/src/app/modules/hal/services/hal-events.service.ts new file mode 100644 index 00000000000..272a4e5b23b --- /dev/null +++ b/frontend/src/app/modules/hal/services/hal-events.service.ts @@ -0,0 +1,57 @@ +import {Injectable} from "@angular/core"; +import {Observable, Subject} from "rxjs"; +import {buffer, debounceTime, filter, map, scan} from "rxjs/operators"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; + +export interface HalEvent { + id:string; + eventType:string; + resourceType:string; +} + +export interface HalCreatedEvent extends HalEvent { + eventType:'created'; +} + +export interface HalUpdatedEvent extends HalEvent { + eventType:'updated'; +} + +export interface RelatedWorkPackageEvent extends HalEvent { + eventType:'association'; + relatedWorkPackage:string|null; + relationType:string; +} + +export interface HalDeletedEvent extends HalEvent { + eventType:'deleted'; +} + +export type HalEventTypes = + HalCreatedEvent|HalUpdatedEvent|RelatedWorkPackageEvent|HalDeletedEvent; + +@Injectable() +export class HalEventsService { + private _events = new Subject(); + + /** Entire event stream */ + public events$ = this._events.asObservable(); + + /** Aggregated events */ + public aggregated$(resourceType:string, debounceTimeInMs = 500):Observable { + return this + .events$ + .pipe( + filter((event:HalEvent) => event.resourceType === resourceType), + buffer(this.events$.pipe(debounceTime(debounceTimeInMs))), + scan((acc, curr) => acc.concat(curr)) + ); + } + + public push(resourceReference:HalResource|{id:string, _type:string}, event:Partial) { + event.id = resourceReference.id!; + event.resourceType = resourceReference._type!; + + this._events.next(event as HalEvent); + } +} diff --git a/frontend/src/app/components/wp-edit/wp-notification.service.ts b/frontend/src/app/modules/hal/services/hal-resource-notification.service.ts similarity index 61% rename from frontend/src/app/components/wp-edit/wp-notification.service.ts rename to frontend/src/app/modules/hal/services/hal-resource-notification.service.ts index 75247f75c34..f0d0e63a032 100644 --- a/frontend/src/app/components/wp-edit/wp-notification.service.ts +++ b/frontend/src/app/modules/hal/services/hal-resource-notification.service.ts @@ -26,38 +26,33 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {ErrorResource} from 'core-app/modules/hal/resources/error-resource'; import {StateService} from '@uirouter/core'; import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; -import {Injectable} from '@angular/core'; +import {Injectable, Injector} from '@angular/core'; import {LoadingIndicatorService} from 'core-app/modules/common/loading-indicator/loading-indicator.service'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {HttpErrorResponse} from "@angular/common/http"; -import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {HalResource} from "core-app/modules/hal/resources/hal-resource"; @Injectable() -export class WorkPackageNotificationService { - constructor(readonly I18n:I18nService, - protected $state:StateService, - protected wpCacheService:WorkPackageCacheService, - protected halResourceService:HalResourceService, - protected NotificationsService:NotificationsService, - protected loadingIndicator:LoadingIndicatorService) { +export class HalResourceNotificationService { + + protected I18n = this.injector.get(I18nService); + protected $state = this.injector.get(StateService); + protected halResourceService = this.injector.get(HalResourceService); + protected NotificationsService = this.injector.get(NotificationsService); + protected loadingIndicator = this.injector.get(LoadingIndicatorService); + + constructor(protected injector:Injector) { } - public showSave(workPackage:WorkPackageResource, isCreate:boolean = false) { - var message:any = { + public showSave(resource:HalResource, isCreate:boolean = false) { + let message:any = { message: this.I18n.t('js.notice_successful_' + (isCreate ? 'create' : 'update')), }; - // Don't show the 'Show in full screen' link if we're there already - if (!this.$state.includes('work-packages.show')) { - message.link = this.showInFullScreenLink(workPackage); - } - this.NotificationsService.addSuccess(message); } @@ -69,24 +64,24 @@ export class WorkPackageNotificationService { * - String error messages * * @param response - * @param workPackage + * @param resource */ - public handleRawError(response:unknown, workPackage?:WorkPackageResource) { - console.error("Handling error message %O for work package %O", response, workPackage); + public handleRawError(response:unknown, resource?:HalResource) { + console.error("Handling error message %O for work package %O", response, resource); // Some transformation may already have returned the error as a HAL resource, // which we will forward to handleErrorResponse if (response instanceof ErrorResource) { - return this.handleErrorResponse(response, workPackage); + return this.handleErrorResponse(response, resource); } const errorBody = this.retrieveError(response); if (errorBody instanceof HalResource) { - return this.handleErrorResponse(errorBody, workPackage); + return this.handleErrorResponse(errorBody, resource); } - if (typeof(response) === 'string') { + if (typeof (response) === 'string') { this.NotificationsService.addError(response); return; } @@ -105,7 +100,7 @@ export class WorkPackageNotificationService { return error.message; } - if (typeof(error) === 'string') { + if (typeof (error) === 'string') { return error; } @@ -134,26 +129,26 @@ export class WorkPackageNotificationService { return errorBody; } - protected handleErrorResponse(errorResource:any, workPackage?:WorkPackageResource) { + protected handleErrorResponse(errorResource:any, resource?:HalResource) { if (!(errorResource instanceof ErrorResource)) { return this.showGeneralError(errorResource); } - if (workPackage) { - return this.showError(errorResource, workPackage); + if (resource) { + return this.showError(errorResource, resource); } this.showApiErrorMessages(errorResource); } - public showError(errorResource:any, workPackage:WorkPackageResource) { - this.showCustomError(errorResource, workPackage) || this.showApiErrorMessages(errorResource); + public showError(errorResource:any, resource:HalResource) { + this.showCustomError(errorResource, resource) || this.showApiErrorMessages(errorResource); } public showGeneralError(message?:unknown) { let error = this.I18n.t('js.error.internal'); - if (typeof(message) === 'string' || _.has(message, 'toString')) { + if (typeof (message) === 'string' || _.has(message, 'toString')) { error += ' ' + (message as any).toString(); } @@ -162,65 +157,40 @@ export class WorkPackageNotificationService { public showEditingBlockedError(attribute:string) { this.NotificationsService.addError(this.I18n.t( - 'js.work_packages.error.edit_prohibited', - { attribute: attribute } + 'js.hal.error.edit_prohibited', + {attribute: attribute} )); } - private showCustomError(errorResource:any, workPackage:WorkPackageResource) { - if (errorResource.errorIdentifier === 'urn:openproject-org:api:v3:errors:UpdateConflict') { - this.NotificationsService.addError({ - message: errorResource.message, - type: 'error', - link: { - text: this.I18n.t('js.work_packages.error.update_conflict_refresh'), - target: () => this.wpCacheService.require(workPackage.id!, true) - } - }); - - - return true; - } - + protected showCustomError(errorResource:any, resource:HalResource) { if (errorResource.errorIdentifier === 'urn:openproject-org:api:v3:errors:PropertyFormatError') { - let attributeName = workPackage.schema[errorResource.details.attribute].name; - let attributeType = workPackage.schema[errorResource.details.attribute].type.toLowerCase(); - let i18nString = 'js.work_packages.error.format.' + attributeType; + let attributeName = resource.schema[errorResource.details.attribute].name; + let attributeType = resource.schema[errorResource.details.attribute].type.toLowerCase(); + let i18nString = 'js.hal.error.format.' + attributeType; if (this.I18n.lookup(i18nString) === undefined) { return false; } this.NotificationsService.addError(this.I18n.t(i18nString, - { attribute: attributeName })); + {attribute: attributeName})); return true; } return false; } - private showApiErrorMessages(errorResource:any) { - var messages = errorResource.errorMessages; + protected showApiErrorMessages(errorResource:any) { + let messages = errorResource.errorMessages; if (messages.length > 1) { this.NotificationsService.addError('', messages); - } - else { + } else { this.NotificationsService.addError(messages[0]); } return true; } - - private showInFullScreenLink(workPackage:WorkPackageResource) { - return { - target: () => { - this.loadingIndicator.table.promise = - this.$state.go('work-packages.show.activity', { workPackageId: workPackage.id }); - }, - text: this.I18n.t('js.work_packages.message_successful_show_in_fullscreen') - }; - } } diff --git a/frontend/src/app/modules/hal/services/hal-resource.config.ts b/frontend/src/app/modules/hal/services/hal-resource.config.ts index 0691b4e6c23..92b738d6dc8 100644 --- a/frontend/src/app/modules/hal/services/hal-resource.config.ts +++ b/frontend/src/app/modules/hal/services/hal-resource.config.ts @@ -62,6 +62,7 @@ import {NewsResource} from "core-app/modules/hal/resources/news-resource"; import {VersionResource} from "core-app/modules/hal/resources/version-resource"; import {MembershipResource} from "core-app/modules/hal/resources/membership-resource"; import {RoleResource} from "core-app/modules/hal/resources/role-resource"; +import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInterface } = { WorkPackage: { @@ -181,6 +182,9 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter Post: { cls: PostResource }, + Project: { + cls: ProjectResource + }, Role: { cls: RoleResource }, diff --git a/frontend/src/app/modules/plugins/plugin-context.ts b/frontend/src/app/modules/plugins/plugin-context.ts index 526fc311a21..994a4993974 100644 --- a/frontend/src/app/modules/plugins/plugin-context.ts +++ b/frontend/src/app/modules/plugins/plugin-context.ts @@ -16,7 +16,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {DisplayFieldService} from "core-app/modules/fields/display/display-field.service"; import {EditFieldService} from "core-app/modules/fields/edit/edit-field.service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/op-file-upload.service"; import {EditorMacrosService} from "core-components/modals/editor/editor-macros.service"; import {HTMLSanitizeService} from "../common/html-sanitize/html-sanitize.service"; @@ -47,7 +47,6 @@ export class OpenProjectPluginContext { hooks: this.injector.get(HookService), i18n: this.injector.get(I18nService), notifications: this.injector.get(NotificationsService), - wpNotifications: this.injector.get(WorkPackageNotificationService), opModalService: this.injector.get(OpModalService), opFileUpload: this.injector.get(OpenProjectFileUploadService), helpTextDm: this.injector.get(HelpTextDmService), diff --git a/frontend/src/app/modules/projects/openproject-projects.module.ts b/frontend/src/app/modules/projects/openproject-projects.module.ts new file mode 100644 index 00000000000..2313028482c --- /dev/null +++ b/frontend/src/app/modules/projects/openproject-projects.module.ts @@ -0,0 +1,54 @@ +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import {OpenprojectCommonModule} from 'core-app/modules/common/openproject-common.module'; +import {OpenprojectFieldsModule} from 'core-app/modules/fields/openproject-fields.module'; +import {NgModule} from '@angular/core'; +import {OpenprojectHalModule} from "core-app/modules/hal/openproject-hal.module"; + + +@NgModule({ + imports: [ + // Commons + OpenprojectCommonModule, + + OpenprojectHalModule, + OpenprojectFieldsModule, + ], + providers: [ + + ], + declarations: [ + ], + entryComponents: [ + ], + exports: [ + ] +}) +export class OpenprojectProjectsModule { +} diff --git a/frontend/src/app/modules/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts b/frontend/src/app/modules/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts index 71b926bc325..904529eaf59 100644 --- a/frontend/src/app/modules/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts +++ b/frontend/src/app/modules/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts @@ -1,29 +1,33 @@ import { - ApplicationRef, ChangeDetectorRef, + ApplicationRef, + ChangeDetectorRef, Component, ComponentFactoryResolver, - ElementRef, Inject, InjectionToken, + ElementRef, + Inject, + InjectionToken, Injector, OnDestroy, - OnInit, Optional, + OnInit, + Optional, ViewChild } from '@angular/core'; import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; import { - ActiveTabInterface, TabComponent, + ActiveTabInterface, + TabComponent, TabInterface, TabPortalOutlet } from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; -import {QueryFormDmService} from 'core-app/modules/hal/dm-services/query-form-dm.service'; import {LoadingIndicatorService} from 'core-app/modules/common/loading-indicator/loading-indicator.service'; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service"; import {ComponentType} from "@angular/cdk/portal"; -import { WpGraphConfigurationService } from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration.service"; +import {WpGraphConfigurationService} from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration.service"; import {WpGraphConfiguration} from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; export const WpTableConfigurationModalPrependToken = new InjectionToken>('WpTableConfigurationModalPrependComponent'); @@ -62,8 +66,7 @@ export class WpGraphConfigurationModalComponent extends OpModalComponent impleme readonly appRef:ApplicationRef, readonly componentFactoryResolver:ComponentFactoryResolver, readonly loadingIndicator:LoadingIndicatorService, - readonly queryFormDm:QueryFormDmService, - readonly wpNotificationsService:WorkPackageNotificationService, + readonly notificationService:WorkPackageNotificationService, readonly cdRef:ChangeDetectorRef, readonly ConfigurationService:ConfigurationService, readonly elementRef:ElementRef, diff --git a/frontend/src/app/modules/work-package-graphs/configuration/wp-graph-configuration.service.ts b/frontend/src/app/modules/work-package-graphs/configuration/wp-graph-configuration.service.ts index c32bdaa2522..f29d28a93bd 100644 --- a/frontend/src/app/modules/work-package-graphs/configuration/wp-graph-configuration.service.ts +++ b/frontend/src/app/modules/work-package-graphs/configuration/wp-graph-configuration.service.ts @@ -7,7 +7,7 @@ import {WpGraphConfigurationFiltersTab} from "core-app/modules/work-package-grap import {ChartType} from 'chart.js'; import {QueryFormDmService} from "core-app/modules/hal/dm-services/query-form-dm.service"; import {QueryFormResource} from "core-app/modules/hal/resources/query-form-resource"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {StateService} from '@uirouter/core'; import {QueryDmService} from "core-app/modules/hal/dm-services/query-dm.service"; import { @@ -15,6 +15,7 @@ import { WpGraphQueryParams } from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration"; import {CurrentProjectService} from "core-components/projects/current-project.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Injectable() export class WpGraphConfigurationService { @@ -26,7 +27,7 @@ export class WpGraphConfigurationService { constructor(readonly I18n:I18nService, readonly queryFormDm:QueryFormDmService, protected readonly queryDm:QueryDmService, - readonly wpNotificationsService:WorkPackageNotificationService, + readonly notificationService:WorkPackageNotificationService, readonly currentProject:CurrentProjectService) { } @@ -168,7 +169,7 @@ export class WpGraphConfigurationService { .then((form:QueryFormResource) => { this._forms[query.id as string] = form; }) - .catch((error) => this.wpNotificationsService.handleRawError(error)); + .catch((error) => this.notificationService.handleRawError(error)); }); this._formsPromise = Promise.all(formPromises); diff --git a/frontend/src/app/modules/work_packages/events/work-package-events.service.ts b/frontend/src/app/modules/work_packages/events/work-package-events.service.ts deleted file mode 100644 index 85516d94088..00000000000 --- a/frontend/src/app/modules/work_packages/events/work-package-events.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {Injectable} from "@angular/core"; -import {Observable, Subject} from "rxjs"; -import {buffer, debounceTime, scan} from "rxjs/operators"; - -export interface WorkPackageEvent { - id:string; - type:string; -} - -export interface WorkPackageCreatedEvent extends WorkPackageEvent { - type:'created'; -} - -export interface WorkPackageUpdatedEvent extends WorkPackageEvent { - type:'updated'; -} - -export interface RelatedWorkPackageEvent extends WorkPackageEvent { - type:'association'; - relatedWorkPackage:string|null; - relationType:string; -} - -export interface WorkPackageDeletedEvent extends WorkPackageEvent { - type:'deleted'; -} - -export type WorkPackageEventTypes = - WorkPackageCreatedEvent|WorkPackageUpdatedEvent|RelatedWorkPackageEvent|WorkPackageDeletedEvent; - -@Injectable() -export class WorkPackageEventsService { - private _events = new Subject(); - - /** Entire event stream */ - public events$ = this._events.asObservable(); - - /** Aggregated events */ - public aggregated$(debounceTimeInMs = 500):Observable { - return this - .events$ - .pipe( - buffer(this.events$.pipe(debounceTime(debounceTimeInMs))), - scan((acc, curr) => acc.concat(curr)) - ); - } - - public push(event:WorkPackageEventTypes) { - this._events.next(event); - } -} diff --git a/frontend/src/app/modules/work_packages/notifications/work-package-notification.service.ts b/frontend/src/app/modules/work_packages/notifications/work-package-notification.service.ts new file mode 100644 index 00000000000..3ba5fc543dd --- /dev/null +++ b/frontend/src/app/modules/work_packages/notifications/work-package-notification.service.ts @@ -0,0 +1,81 @@ +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import {StateService} from '@uirouter/core'; +import {Injectable, Injector} from '@angular/core'; +import {INotification} from 'core-app/modules/common/notifications/notifications.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; +import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; + +@Injectable() +export class WorkPackageNotificationService extends HalResourceNotificationService { + + constructor(protected injector:Injector, + protected $state:StateService, + protected wpCacheService:WorkPackageCacheService) { + super(injector); + } + + public showSave(resource:WorkPackageResource, isCreate:boolean = false) { + let message:any = { + message: this.I18n.t('js.notice_successful_' + (isCreate ? 'create' : 'update')), + }; + + this.addWorkPackageFullscreenLink(message, resource as any); + + this.NotificationsService.addSuccess(message); + } + + protected showCustomError(errorResource:any, resource:WorkPackageResource):boolean { + if (errorResource.errorIdentifier === 'urn:openproject-org:api:v3:errors:UpdateConflict') { + this.NotificationsService.addError({ + message: errorResource.message, + type: 'error', + link: { + text: this.I18n.t('js.hal.error.update_conflict_refresh'), + target: () => this.wpCacheService.require(resource.id!, true) + } + }); + + return true; + } + + return super.showCustomError(errorResource, resource); + } + + private addWorkPackageFullscreenLink(message:INotification, resource:WorkPackageResource) { + // Don't show the 'Show in full screen' link if we're there already + if (!this.$state.includes('work-packages.show')) { + message.link = { + target: () => this.$state.go('work-packages.show.activity', {workPackageId: resource.id}), + text: this.I18n.t('js.work_packages.message_successful_show_in_fullscreen') + }; + } + } +} diff --git a/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts b/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts index 4cdcd71c0f0..03c7422cc14 100644 --- a/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts +++ b/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts @@ -67,13 +67,11 @@ import {RevisionActivityComponent} from 'core-components/wp-activity/revision/re import {ActivityLinkComponent} from 'core-components/wp-activity/activity-link.component'; import {WorkPackageActivityTabComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-tab.component'; import {OpenprojectAttachmentsModule} from 'core-app/modules/attachments/openproject-attachments.module'; -import {WorkPackageEditFieldComponent} from 'core-app/components/wp-edit/wp-edit-field/wp-edit-field.component'; import {WpCustomActionComponent} from 'core-components/wp-custom-actions/wp-custom-actions/wp-custom-action.component'; import {WpCustomActionsComponent} from 'core-components/wp-custom-actions/wp-custom-actions.component'; import {WorkPackageRelationsCountComponent} from 'core-components/work-packages/wp-relations-count/wp-relations-count.component'; import {WorkPackageWatchersCountComponent} from 'core-components/work-packages/wp-relations-count/wp-watchers-count.component'; import {WorkPackageBreadcrumbComponent} from 'core-components/work-packages/wp-breadcrumb/wp-breadcrumb.component'; -import {WorkPackageEditFieldGroupComponent} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive'; import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/wp-details-toolbar.component'; import {WorkPackageWatcherButtonComponent} from 'core-components/work-packages/wp-watcher-button/wp-watcher-button.component'; import {WorkPackageSubjectComponent} from 'core-components/work-packages/wp-subject/wp-subject.component'; @@ -132,7 +130,6 @@ import {WorkPackageCacheService} from 'core-components/work-packages/work-packag import {SchemaCacheService} from 'core-components/schemas/schema-cache.service'; import {WorkPackageWatchersService} from 'core-components/wp-single-view-tabs/watchers-tab/wp-watchers.service'; import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service'; import {QueryFormDmService} from 'core-app/modules/hal/dm-services/query-form-dm.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; @@ -159,9 +156,12 @@ import {WorkPackageIsolatedGraphQuerySpaceDirective} from "core-app/modules/work import {WorkPackageViewToggleButton} from "core-components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component"; import {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.component"; import {WorkPackageViewDropdownMenuDirective} from "core-components/op-context-menu/handlers/wp-view-dropdown-menu.directive"; -import {WorkPackageEventsService} from "core-app/modules/work_packages/events/work-package-events.service"; -import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; +import {HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {OpenprojectProjectsModule} from "core-app/modules/projects/openproject-projects.module"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; +import {WorkPackageEditActionsBarComponent} from "core-app/modules/common/edit-actions-bar/wp-edit-actions-bar.component"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; @NgModule({ @@ -177,6 +177,8 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa OpenprojectBcfModule, + OpenprojectProjectsModule, + // Work package custom actions //WpCustomActionsModule, ], @@ -188,6 +190,9 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa multi: true }, + // Notification service + WorkPackageNotificationService, + // External query configuration ExternalQueryConfigurationService, ExternalRelationQueryConfigurationService, @@ -210,7 +215,6 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa SchemaCacheService, KeepTabService, - WorkPackageNotificationService, WorkPackageDmService, WorkPackagesActivityService, @@ -218,7 +222,7 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa WorkPackageWatchersService, QueryFormDmService, - WorkPackageEventsService, + HalEventsService, ], declarations: [ // Routing @@ -233,6 +237,7 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa WorkPackageNewFullViewComponent, WorkPackageNewSplitViewComponent, WorkPackageTypeStatusComponent, + WorkPackageEditActionsBarComponent, // WP Copy WorkPackageCopyFullViewComponent, @@ -260,9 +265,6 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa WorkPackageTableSumsRowController, - // WP Edit Fields - WorkPackageEditFieldComponent, - // Filters QueryFiltersComponent, QueryFilterComponent, @@ -339,7 +341,6 @@ import {WorkPackageEditingService} from "core-components/wp-edit-form/work-packa WorkPackageRelationsCountComponent, WorkPackageWatchersCountComponent, WorkPackageBreadcrumbComponent, - WorkPackageEditFieldGroupComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, WorkPackageSubjectComponent, @@ -507,6 +508,15 @@ export class OpenprojectWorkPackagesModule { hookService.register('workPackageAttachmentListComponent', (workPackage:WorkPackageResource) => { return AttachmentListComponent; }); + + /** Return specialized work package changeset for editing service */ + hookService.register('halResourceChangesetClass', (resource:HalResource) => { + if (resource._type === 'WorkPackage') { + return WorkPackageChangeset; + } + + return null; + }); }; } } diff --git a/frontend/src/app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive.ts b/frontend/src/app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive.ts index 8f672309a38..af0b9278cd7 100644 --- a/frontend/src/app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive.ts +++ b/frontend/src/app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive.ts @@ -44,7 +44,8 @@ import {WorkPackageViewHighlightingService} from "core-app/modules/work_packages import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service"; import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackagesListService} from "core-components/wp-list/wp-list.service"; import {WorkPackageService} from "core-components/work-packages/work-package.service"; import {WorkPackageRelationsHierarchyService} from "core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service"; @@ -93,8 +94,8 @@ export const WpIsolatedGraphQuerySpaceProviders = [ WpChildrenInlineCreateService, WpRelationInlineCreateService, + HalResourceEditingService, WorkPackageCreateService, - WorkPackageEditingService, WorkPackageStatesInitializationService, diff --git a/frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts b/frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts index c701b6be643..f5580cadcb0 100644 --- a/frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts +++ b/frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts @@ -44,7 +44,8 @@ import {WorkPackageViewHighlightingService} from "core-app/modules/work_packages import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service"; import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service"; import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; import {WorkPackagesListService} from "core-components/wp-list/wp-list.service"; import {WorkPackageService} from "core-components/work-packages/work-package.service"; import {WorkPackageRelationsHierarchyService} from "core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service"; @@ -62,6 +63,8 @@ import {CausedUpdatesService} from "core-app/modules/boards/board/caused-updates import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service"; import {WorkPackageViewDisplayRepresentationService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; import {WorkPackageViewHierarchyIdentationService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-hierarchy-indentation.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; /** * Directive to open a work package query 'space', an isolated injector hierarchy @@ -73,52 +76,55 @@ import {WorkPackageViewHierarchyIdentationService} from "core-app/modules/work_p @Directive({ selector: '[wp-isolated-query-space]', providers: [ - // Open the isolated space first, order is important here - IsolatedQuerySpace, - OpTableActionsService, + // Override the hal notification service + { provide: HalResourceNotificationService, useExisting: WorkPackageNotificationService }, - // Work package table services - WorkPackagesListChecksumService, - WorkPackagesListService, - WorkPackageViewRelationColumnsService, - WorkPackageViewPaginationService, - WorkPackageViewGroupByService, - WorkPackageViewHierarchiesService, - WorkPackageViewSortByService, - WorkPackageViewColumnsService, - WorkPackageViewFiltersService, - WorkPackageViewTimelineService, - WorkPackageViewSelectionService, - WorkPackageViewSumService, - WorkPackageViewAdditionalElementsService, - WorkPackageViewFocusService, - WorkPackageViewHighlightingService, - WorkPackageViewDisplayRepresentationService, - WorkPackageViewOrderService, - WorkPackageViewHierarchyIdentationService, - CausedUpdatesService, + // Open the isolated space first, order is important here + IsolatedQuerySpace, + OpTableActionsService, - WorkPackageService, - WorkPackageRelationsHierarchyService, - WorkPackageFiltersService, - WorkPackageContextMenuHelperService, + // Work package table services + WorkPackagesListChecksumService, + WorkPackagesListService, + WorkPackageViewRelationColumnsService, + WorkPackageViewPaginationService, + WorkPackageViewGroupByService, + WorkPackageViewHierarchiesService, + WorkPackageViewSortByService, + WorkPackageViewColumnsService, + WorkPackageViewFiltersService, + WorkPackageViewTimelineService, + WorkPackageViewSelectionService, + WorkPackageViewSumService, + WorkPackageViewAdditionalElementsService, + WorkPackageViewFocusService, + WorkPackageViewHighlightingService, + WorkPackageViewDisplayRepresentationService, + WorkPackageViewOrderService, + WorkPackageViewHierarchyIdentationService, + CausedUpdatesService, - // Provide a separate service for creation events of WP Inline create - // This can be hierarchically injected to provide isolated events on an embedded table - WorkPackageInlineCreateService, - WpChildrenInlineCreateService, - WpRelationInlineCreateService, + WorkPackageService, + WorkPackageRelationsHierarchyService, + WorkPackageFiltersService, + WorkPackageContextMenuHelperService, - WorkPackageCardViewService, + // Provide a separate service for creation events of WP Inline create + // This can be hierarchically injected to provide isolated events on an embedded table + WorkPackageInlineCreateService, + WpChildrenInlineCreateService, + WpRelationInlineCreateService, - WorkPackageCreateService, - WorkPackageEditingService, + WorkPackageCardViewService, - WorkPackageStatesInitializationService, - PortalCleanupService, + HalResourceEditingService, + WorkPackageCreateService, - // Table Drag & Drop actions - TableDragActionsRegistryService, + WorkPackageStatesInitializationService, + PortalCleanupService, + + // Table Drag & Drop actions + TableDragActionsRegistryService, ] }) export class WorkPackageIsolatedQuerySpaceDirective { diff --git a/frontend/src/app/modules/work_packages/routing/wp-base/wp--base.component.ts b/frontend/src/app/modules/work_packages/routing/wp-base/wp--base.component.ts index c5ba43513fb..d8bc3c4eefd 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-base/wp--base.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-base/wp--base.component.ts @@ -26,8 +26,12 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {Component, Injector} from "@angular/core"; +import {Component, Injector, OnDestroy, OnInit} from "@angular/core"; import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; +import {EditFormRoutingService} from "core-app/modules/fields/edit/edit-form/edit-form-routing.service"; +import {WorkPackageEditFormRoutingService} from "core-app/modules/work_packages/routing/wp-edit-form/wp-edit-form-routing.service"; export const wpBaseSelector = 'work-packages-base'; @@ -37,7 +41,10 @@ export const wpBaseSelector = 'work-packages-base';
- ` + `, + providers: [ + { provide: EditFormRoutingService, useClass: WorkPackageEditFormRoutingService } + ] }) export class WorkPackagesBaseComponent { } diff --git a/frontend/src/app/modules/work_packages/routing/wp-edit-form/wp-edit-form-routing.service.ts b/frontend/src/app/modules/work_packages/routing/wp-edit-form/wp-edit-form-routing.service.ts new file mode 100644 index 00000000000..a2b332a6c8d --- /dev/null +++ b/frontend/src/app/modules/work_packages/routing/wp-edit-form/wp-edit-form-routing.service.ts @@ -0,0 +1,56 @@ +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import {Transition} from "@uirouter/core"; +import {Injectable} from "@angular/core"; +import {EditFormRoutingService} from "core-app/modules/fields/edit/edit-form/edit-form-routing.service"; + +@Injectable() +export class WorkPackageEditFormRoutingService extends EditFormRoutingService { + /** + * Return whether the given transition is cancelled during the editing of this form + * + * @param transition The transition that is underway. + * @return A boolean marking whether the transition should be blocked. + */ + public blockedTransition(transition:Transition):boolean { + const toState = transition.to(); + const fromState = transition.from(); + const fromParams = transition.params('from'); + const toParams = transition.params('to'); + + // In new/copy mode, transitions to the same controller are allowed + if (fromState.name && fromState.name.match(/\.(new|copy)$/)) { + return !(toState.data && toState.data.allowMovingInEditMode); + } + + // When editing an existing WP, transitions on the same WP id are allowed + return toParams.workPackageId === undefined || toParams.workPackageId !== fromParams.workPackageId; + } +} + diff --git a/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.html b/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.html index 769b845df5c..19601832f39 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.html +++ b/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.html @@ -1,5 +1,5 @@ -
diff --git a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts index 3aaabf03bea..a231e6946ad 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts @@ -37,6 +37,8 @@ import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and import {BcfDetectorService} from "core-app/modules/bcf/helper/bcf-detector.service"; import {wpDisplayCardRepresentation} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-table-configuration"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ selector: 'wp-list', @@ -44,6 +46,8 @@ import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-t styleUrls: ['./wp-list.component.sass'], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ + /** We need to provide the wpNotification service here to get correct save notifications for WP resources */ + { provide: HalResourceNotificationService, useClass: WorkPackageNotificationService }, DragAndDropService, CausedUpdatesService ] diff --git a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts index ce809a3f90f..424aade5e2a 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts @@ -36,11 +36,16 @@ import {FirstRouteService} from "core-app/modules/router/first-route-service"; import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service"; import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service"; import {WorkPackageSingleViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-package-single-view.base"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Component({ templateUrl: './wp-split-view.html', changeDetection: ChangeDetectionStrategy.OnPush, selector: 'wp-split-view-entry', + providers: [ + { provide: HalResourceNotificationService, useClass: WorkPackageNotificationService } + ] }) export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase implements OnInit { diff --git a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.html b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.html index ee653cd2a10..ec66767ce31 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.html +++ b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.html @@ -48,10 +48,10 @@ - +
-
+
diff --git a/frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service.ts index 228020e58e1..93ce6a864a4 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-additional-elements.service.ts @@ -30,12 +30,13 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r import {WorkPackageViewColumnsService} from './wp-view-columns.service'; import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; import {WorkPackageViewHierarchiesService} from './wp-view-hierarchy.service'; -import {WorkPackageNotificationService} from 'core-components/wp-edit/wp-notification.service'; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {Injectable} from '@angular/core'; import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {RelationsStateValue, WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; @Injectable() export class WorkPackageViewAdditionalElementsService { @@ -43,7 +44,7 @@ export class WorkPackageViewAdditionalElementsService { constructor(readonly querySpace:IsolatedQuerySpace, readonly wpTableHierarchies:WorkPackageViewHierarchiesService, readonly wpTableColumns:WorkPackageViewColumnsService, - readonly wpNotificationsService:WorkPackageNotificationService, + readonly notificationService:WorkPackageNotificationService, readonly halResourceService:HalResourceService, readonly wpCacheService:WorkPackageCacheService, readonly wpRelations:WorkPackageRelationsService) { @@ -66,7 +67,7 @@ export class WorkPackageViewAdditionalElementsService { }) .catch((e) => { this.querySpace.additionalRequiredWorkPackages.putValue(null, 'Failure loading required work packages'); - this.wpNotificationsService.handleRawError(e); + this.notificationService.handleRawError(e); }); } diff --git a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts index 99cadc0cd00..896195f45f3 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts @@ -39,8 +39,11 @@ import {AuthorisationService} from "core-app/modules/common/model-auth/model-aut import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; import {States} from "core-components/states.service"; import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service"; -import {WorkPackageEditingService} from "core-components/wp-edit-form/work-package-editing-service"; -import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; + +import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; + export class WorkPackageSingleViewBase implements OnDestroy { public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); @@ -48,9 +51,9 @@ export class WorkPackageSingleViewBase implements OnDestroy { public I18n:I18nService = this.injector.get(I18nService); public keepTab:KeepTabService = this.injector.get(KeepTabService); public PathHelper:PathHelperService = this.injector.get(PathHelperService); - protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + protected halEditing:HalResourceEditingService = this.injector.get(HalResourceEditingService); protected wpTableFocus:WorkPackageViewFocusService = this.injector.get(WorkPackageViewFocusService); - protected wpNotifications:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService); + protected notificationService:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService); protected projectCacheService:ProjectCacheService = this.injector.get(ProjectCacheService); protected authorisationService:AuthorisationService = this.injector.get(AuthorisationService); protected cdRef:ChangeDetectorRef = this.injector.get(ChangeDetectorRef); @@ -82,7 +85,7 @@ export class WorkPackageSingleViewBase implements OnDestroy { protected observeWorkPackage() { /** Require the work package once to ensure we're displaying errors */ this.wpCacheService.require(this.workPackageId) - .catch((error) => this.wpNotifications.handleRawError(error)); + .catch((error) => this.notificationService.handleRawError(error)); /** Stream updates of the work package */ this.wpCacheService.state(this.workPackageId) diff --git a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts index e07f05ecfe5..c03ac6d8f82 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts @@ -31,7 +31,7 @@ import {StateService, TransitionService} from '@uirouter/core'; import {AuthorisationService} from 'core-app/modules/common/model-auth/model-auth.service'; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed'; -import {filter, withLatestFrom} from 'rxjs/operators'; +import {filter, map, withLatestFrom} from 'rxjs/operators'; import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {WorkPackageStaticQueriesService} from 'core-components/wp-query-select/wp-static-queries.service'; @@ -52,10 +52,9 @@ import {QueryDmService} from "core-app/modules/hal/dm-services/query-dm.service" import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service"; import {WorkPackageViewOrderService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-order.service"; import {WorkPackageViewDisplayRepresentationService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; -import { - WorkPackageEvent, - WorkPackageEventsService -} from "core-app/modules/work_packages/events/work-package-events.service"; +import {HalEvent, HalEventsService} from "core-app/modules/hal/services/hal-events.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { @@ -83,7 +82,8 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { readonly wpStatesInitialization:WorkPackageStatesInitializationService = this.injector.get(WorkPackageStatesInitializationService); readonly cdRef:ChangeDetectorRef = this.injector.get(ChangeDetectorRef); readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService = this.injector.get(WorkPackageViewDisplayRepresentationService); - readonly wpEvents:WorkPackageEventsService = this.injector.get(WorkPackageEventsService); + readonly halEvents:HalEventsService = this.injector.get(HalEventsService); + constructor(protected injector:Injector) { } @@ -163,14 +163,13 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { * through the refresh service. */ protected setupRefreshObserver() { - (window as any).wpEvents = this.wpEvents; - this.wpEvents - .aggregated$() + this.halEvents + .aggregated$('WorkPackage') .pipe( untilComponentDestroyed(this), - filter((events:WorkPackageEvent[]) => this.filterRefreshEvents(events)) + filter((events:HalEvent[]) => this.filterRefreshEvents(events)) ) - .subscribe((events:WorkPackageEvent[]) => { + .subscribe((events:HalEvent[]) => { this.refresh(false, false); }); } @@ -193,16 +192,16 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { /** * Filter the given work package events for something interesting - * @param events WorkPackageEvent[] + * @param events HalEvent[] * * @return {boolean} whether any of these events should trigger the view reloading */ - protected filterRefreshEvents(events:WorkPackageEvent[]):boolean { + protected filterRefreshEvents(events:HalEvent[]):boolean { let rendered = new Set(this.querySpace.renderedWorkPackageIds.getValueOr([])); for (let i = 0; i < events.length; i++) { const item = events[i]; - if (rendered.has(item.id) || item.type === 'created') { + if (rendered.has(item.id) || item.eventType === 'created') { return true; } } diff --git a/lib/api/v3/projects/project_representer.rb b/lib/api/v3/projects/project_representer.rb index d3a9efe26ee..ba502c80f89 100644 --- a/lib/api/v3/projects/project_representer.rb +++ b/lib/api/v3/projects/project_representer.rb @@ -122,6 +122,12 @@ module API } end + link :schema do + { + href: api_v3_paths.projects_schema + } + end + associated_resource :parent, v3_path: :project, representer: ::API::V3::Projects::ProjectRepresenter, diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index b584027f38b..fa3087b0d28 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -215,6 +215,10 @@ module API "#{projects}/available_parent_projects" end + def self.projects_schema + "#{projects}/schema" + end + resources :query def self.query_default diff --git a/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts b/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts index cd689067035..abec45096b1 100644 --- a/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts +++ b/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts @@ -28,12 +28,12 @@ import {Injectable} from "@angular/core"; import {HttpClient} from '@angular/common/http'; -import {WorkPackageNotificationService} from "core-app/components/wp-edit/wp-notification.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; @Injectable() export class CostBudgetSubformAugmentService { - constructor(private wpNotifications:WorkPackageNotificationService, + constructor(private halNotification:HalResourceNotificationService, private http:HttpClient) { } @@ -87,7 +87,7 @@ export class CostBudgetSubformAugmentService { jQuery('#' + selector).html(val); }); }, - (error:any) => this.wpNotifications.handleRawError(error) + (error:any) => this.halNotification.handleRawError(error) ); } diff --git a/modules/costs/frontend/module/main.ts b/modules/costs/frontend/module/main.ts index 8ee1ca095c4..b363678f1f3 100644 --- a/modules/costs/frontend/module/main.ts +++ b/modules/costs/frontend/module/main.ts @@ -26,8 +26,8 @@ import {APP_INITIALIZER, Injector, NgModule} from '@angular/core'; import {OpenProjectPluginContext} from 'core-app/modules/plugins/plugin-context'; -import {CostsByTypeDisplayField} from './wp-display/wp-display-costs-by-type-field.module'; -import {CurrencyDisplayField} from './wp-display/wp-display-currency-field.module'; +import {CostsByTypeDisplayField} from './wp-display/costs-by-type-display-field.module'; +import {CurrencyDisplayField} from './wp-display/currency-display-field.module'; import {BudgetResource} from './hal/resources/budget-resource'; import {multiInput} from 'reactivestates'; import {CostSubformAugmentService} from "./augment/cost-subform.augment.service"; diff --git a/modules/costs/frontend/module/wp-display/wp-display-costs-by-type-field.module.ts b/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts similarity index 95% rename from modules/costs/frontend/module/wp-display/wp-display-costs-by-type-field.module.ts rename to modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts index 79ec901faf1..7bfb4b97060 100644 --- a/modules/costs/frontend/module/wp-display/wp-display-costs-by-type-field.module.ts +++ b/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts @@ -101,13 +101,7 @@ export class CostsByTypeDisplayField extends DisplayField { link.textContent = val.spentUnits + ' ' + val.costType.name; element.appendChild(link); - if (i < this.value.elements.length - 1) { - const sep = document.createElement('span'); - sep.textContent = ', '; - - element.appendChild(sep); - } - + this.addSeparator(element, i); } /** @@ -117,13 +111,16 @@ export class CostsByTypeDisplayField extends DisplayField { const span = document.createElement('span'); span.textContent = val.spentUnits + ' ' + val.costType.name; + this.addSeparator(element, i); + } + + private addSeparator(element:HTMLElement, i:Number) { if (i < this.value.elements.length - 1) { const sep = document.createElement('span'); sep.textContent = ', '; element.appendChild(sep); } - } } diff --git a/modules/costs/frontend/module/wp-display/wp-display-currency-field.module.ts b/modules/costs/frontend/module/wp-display/currency-display-field.module.ts similarity index 100% rename from modules/costs/frontend/module/wp-display/wp-display-currency-field.module.ts rename to modules/costs/frontend/module/wp-display/currency-display-field.module.ts diff --git a/modules/costs/spec/features/costs_edit_fields_spec.rb b/modules/costs/spec/features/costs_edit_fields_spec.rb index eb3784d1ce5..0e4d39c285d 100644 --- a/modules/costs/spec/features/costs_edit_fields_spec.rb +++ b/modules/costs/spec/features/costs_edit_fields_spec.rb @@ -44,10 +44,10 @@ describe 'Work Package cost fields', type: :feature, js: true do split_create.click_create_wp_button type_task split_create.expect_fully_loaded - expect(page).to have_selector('.wp-edit-field--container.costObject') - expect(page).to have_no_selector('.wp-edit-field--container.laborCosts') - expect(page).to have_no_selector('.wp-edit-field--container.materialCosts') - expect(page).to have_no_selector('.wp-edit-field--container.overallCosts') + expect(page).to have_selector('.inline-edit--container.costObject') + expect(page).to have_no_selector('.inline-edit--container.laborCosts') + expect(page).to have_no_selector('.inline-edit--container.materialCosts') + expect(page).to have_no_selector('.inline-edit--container.overallCosts') field = split_create.edit_field(:costObject) field.openSelectField diff --git a/modules/costs/spec/features/costs_table_sums.rb b/modules/costs/spec/features/costs_table_sums.rb index 5247af0dd53..39b7cbcd459 100644 --- a/modules/costs/spec/features/costs_table_sums.rb +++ b/modules/costs/spec/features/costs_table_sums.rb @@ -75,8 +75,8 @@ describe 'Work Package table cost sums', type: :feature, js: true do it 'shows the sums' do within('tr.sum.group.all') do - expect(page).to have_selector('.wp-table--cell-span', text: '2.50 EUR', count: 3) - expect(page).to have_selector('.wp-table--cell-span', text: '-', count: 1) + expect(page).to have_selector('.inline-edit--display-field', text: '2.50 EUR', count: 3) + expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 1) end end end @@ -85,7 +85,7 @@ describe 'Work Package table cost sums', type: :feature, js: true do it 'does not show the sums' do within('tr.sum.group.all') do - expect(page).to have_selector('.wp-table--cell-span', text: '-', count: 4) + expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 4) end end end diff --git a/modules/costs/spec/features/time_entries_spec.rb b/modules/costs/spec/features/time_entries_spec.rb index 64f6b32c2bb..b74319d0614 100644 --- a/modules/costs/spec/features/time_entries_spec.rb +++ b/modules/costs/spec/features/time_entries_spec.rb @@ -64,8 +64,8 @@ describe 'Work Package table cost entries', type: :feature, js: true do parent_row = wp_table.row(parent) wp_row = wp_table.row(work_package) - expect(parent_row).to have_selector('.wp-edit-field.spentTime', text: '12.5 h') - expect(wp_row).to have_selector('.wp-edit-field.spentTime', text: '2.5 h') + expect(parent_row).to have_selector('.inline-edit--container.spentTime', text: '12.5 h') + expect(wp_row).to have_selector('.inline-edit--container.spentTime', text: '2.5 h') end it 'creates an activity' do diff --git a/modules/dashboards/spec/features/custom_text_spec.rb b/modules/dashboards/spec/features/custom_text_spec.rb index 26020f6c585..f81a7df48d2 100644 --- a/modules/dashboards/spec/features/custom_text_spec.rb +++ b/modules/dashboards/spec/features/custom_text_spec.rb @@ -52,7 +52,7 @@ describe 'Project description widget on dashboard', type: :feature, js: true do end let(:image_fixture) { Rails.root.join('spec/fixtures/files/image.png') } let(:editor) { ::Components::WysiwygEditor.new 'body' } - let(:field) { WorkPackageEditorField.new(page, 'description', selector: '.wp-inline-edit--active-field') } + let(:field) { WorkPackageEditorField.new(page, 'description', selector: '.inline-edit--active-field') } before do login_as user @@ -82,7 +82,7 @@ describe 'Project description widget on dashboard', type: :feature, js: true do within custom_text_widget.area do expect(page) - .to have_selector('.wp-edit-field--display-field', text: 'My own little text') + .to have_selector('.inline-edit--display-field', text: 'My own little text') find('.inplace-editing--container').click @@ -90,7 +90,7 @@ describe 'Project description widget on dashboard', type: :feature, js: true do field.cancel_by_click expect(page) - .to have_selector('.wp-edit-field--display-field', text: 'My own little text') + .to have_selector('.inline-edit--display-field', text: 'My own little text') end dashboard_page.expect_no_notification message: I18n.t('js.notice_successful_update') diff --git a/modules/my_page/spec/features/my/custom_text_spec.rb b/modules/my_page/spec/features/my/custom_text_spec.rb index afa4648e5b8..1f0bd623e48 100644 --- a/modules/my_page/spec/features/my/custom_text_spec.rb +++ b/modules/my_page/spec/features/my/custom_text_spec.rb @@ -51,7 +51,7 @@ describe 'Custom text widget on my page', type: :feature, js: true do end let(:image_fixture) { Rails.root.join('spec/fixtures/files/image.png') } let(:editor) { ::Components::WysiwygEditor.new 'body' } - let(:field) { WorkPackageEditorField.new(page, 'description', selector: '.wp-inline-edit--active-field') } + let(:field) { WorkPackageEditorField.new(page, 'description', selector: '.inline-edit--active-field') } before do login_as user @@ -76,7 +76,7 @@ describe 'Custom text widget on my page', type: :feature, js: true do field.save! expect(page) - .to have_selector('.wp-edit-field--display-field', text: 'My own little text') + .to have_selector('.inline-edit--display-field', text: 'My own little text') find('.inplace-editing--container').click @@ -84,7 +84,7 @@ describe 'Custom text widget on my page', type: :feature, js: true do field.cancel_by_click expect(page) - .to have_selector('.wp-edit-field--display-field', text: 'My own little text') + .to have_selector('.inline-edit--display-field', text: 'My own little text') # adding an image find('.inplace-editing--container').click diff --git a/spec/features/accessibility/work_packages/work_package_query_spec.rb b/spec/features/accessibility/work_packages/work_package_query_spec.rb index bb37c87395f..f91eece2e5c 100644 --- a/spec/features/accessibility/work_packages/work_package_query_spec.rb +++ b/spec/features/accessibility/work_packages/work_package_query_spec.rb @@ -165,10 +165,10 @@ describe 'Work package index accessibility', type: :feature, selenium: true do context 'focus' do let(:first_link_selector) do - ".wp-row-#{work_package.id} .wp-table--cell-span.type" + ".wp-row-#{work_package.id} .inline-edit--display-field.type" end let(:second_link_selector) do - ".wp-row-#{another_work_package.id} .wp-table--cell-span.type" + ".wp-row-#{another_work_package.id} .inline-edit--display-field.type" end it 'navigates with J and K' do diff --git a/spec/features/custom_fields/multi_user_custom_field_spec.rb b/spec/features/custom_fields/multi_user_custom_field_spec.rb index 3ce7ab517b3..29139d705a2 100644 --- a/spec/features/custom_fields/multi_user_custom_field_spec.rb +++ b/spec/features/custom_fields/multi_user_custom_field_spec.rb @@ -74,7 +74,7 @@ describe "multi select custom values", js: true do expect(page).to have_text "Billy Nobbler" expect(page).to have_text "Anton Lupin" - page.find(".wp-table--cell-span", text: "Billy Nobbler").click + page.find(".inline-edit--display-field", text: "Billy Nobbler").click cf_edit_field.unset_value "Anton Lupin", true cf_edit_field.set_value "Cooper Quatermaine" diff --git a/spec/features/types/form_configuration_spec.rb b/spec/features/types/form_configuration_spec.rb index cef990d491d..a9399a70190 100644 --- a/spec/features/types/form_configuration_spec.rb +++ b/spec/features/types/form_configuration_spec.rb @@ -200,7 +200,7 @@ describe 'form configuration', type: :feature, js: true do wp_page.click_create_wp_button(type) wp_page.expect_group('Estimates and time') do - expect(page).to have_selector('.wp-edit-field.estimatedTime') + expect(page).to have_selector('.inline-edit--container.estimatedTime') end find('#work-packages--edit-actions-cancel').click diff --git a/spec/features/work_packages/bulk/move_work_package_spec.rb b/spec/features/work_packages/bulk/move_work_package_spec.rb index 6bb4cd56d10..f5689bc3883 100644 --- a/spec/features/work_packages/bulk/move_work_package_spec.rb +++ b/spec/features/work_packages/bulk/move_work_package_spec.rb @@ -85,7 +85,7 @@ describe 'Moving a work package through Rails view', js: true do it 'moves parent and child wp to a new project' do expect_angular_frontend_initialized - expect(page).to have_selector('.wp-edit-field.subject', text: work_package.subject, wait: 10) + expect(page).to have_selector('.inline-edit--container.subject', text: work_package.subject, wait: 10) expect(page).to have_selector('#projects-menu', text: 'Target') # Should move its children @@ -98,7 +98,7 @@ describe 'Moving a work package through Rails view', js: true do it 'does moves the work package and changes the type' do expect_angular_frontend_initialized - expect(page).to have_selector('.wp-edit-field.subject', text: work_package.subject, wait: 10) + expect(page).to have_selector('.inline-edit--container.subject', text: work_package.subject, wait: 10) expect(page).to have_selector('#projects-menu', text: 'Target') # Should NOT have moved diff --git a/spec/features/work_packages/details/markdown/description_editor_spec.rb b/spec/features/work_packages/details/markdown/description_editor_spec.rb index 1f5c3c3da2c..6b2fb15c829 100644 --- a/spec/features/work_packages/details/markdown/description_editor_spec.rb +++ b/spec/features/work_packages/details/markdown/description_editor_spec.rb @@ -110,7 +110,7 @@ describe 'description inplace editor', js: true, selenium: true do let(:role) { FactoryBot.create :role, permissions: %i(view_work_packages) } it 'does not show the field' do - expect(page).to have_no_selector('.wp-edit-field.description.-editable') + expect(page).to have_no_selector('.inline-edit--display-field.description.-editable') field.display_element.click field.expect_inactive! diff --git a/spec/features/work_packages/new/work_package_default_description_spec.rb b/spec/features/work_packages/new/work_package_default_description_spec.rb index 445f86145bd..1725d1d4d2f 100644 --- a/spec/features/work_packages/new/work_package_default_description_spec.rb +++ b/spec/features/work_packages/new/work_package_default_description_spec.rb @@ -26,29 +26,29 @@ describe 'new work package', js: true, with_mail: false do # Changes in the description shall not be overridden. def change_type_and_expect_description(set_project: false) if !set_project - expect(page).to have_selector('.wp-edit-field.type', text: type_feature.name) + expect(page).to have_selector('.inline-edit--container.type', text: type_feature.name) end - expect(page).to have_selector('.wp-edit-field.description', text: '') + expect(page).to have_selector('.inline-edit--container.description', text: '') type_field.openSelectField type_field.set_value type_task - expect(page).to have_selector('.wp-edit-field.description h1', text: 'New Task template') + expect(page).to have_selector('.inline-edit--container.description h1', text: 'New Task template') type_field.openSelectField type_field.set_value type_bug - expect(page).to have_selector('.wp-edit-field.description h1', text: 'New Bug template') + expect(page).to have_selector('.inline-edit--container.description h1', text: 'New Bug template') description_field.set_value 'Something different than the default.' type_field.openSelectField type_field.set_value type_task - expect(page).to have_no_selector('.wp-edit-field.description h1', text: 'New Task template', wait: 5) + expect(page).to have_no_selector('.inline-edit--container.description h1', text: 'New Task template', wait: 5) description_field.set_value '' type_field.openSelectField type_field.set_value type_bug - expect(page).to have_selector('.wp-edit-field.description h1', text: 'New Bug template') + expect(page).to have_selector('.inline-edit--container.description h1', text: 'New Bug template') if set_project project_field.openSelectField @@ -59,7 +59,7 @@ describe 'new work package', js: true, with_mail: false do scroll_to_and_click find('#work-packages--edit-actions-save') wp_page.expect_notification message: 'Successful creation.' - expect(page).to have_selector('.wp-edit-field--display-field.description h1', text: 'New Bug template') + expect(page).to have_selector('.inline-edit--display-field.description h1', text: 'New Bug template') end before do diff --git a/spec/features/work_packages/table/context_menu_spec.rb b/spec/features/work_packages/table/context_menu_spec.rb index 020493fd078..08c7a623bfe 100644 --- a/spec/features/work_packages/table/context_menu_spec.rb +++ b/spec/features/work_packages/table/context_menu_spec.rb @@ -37,7 +37,7 @@ describe 'Work package table context menu', js: true do # Open full view goto_context_menu list_view menu.choose('Open fullscreen view') - expect(page).to have_selector('.work-packages--show-view .wp-edit-field.subject', + expect(page).to have_selector('.work-packages--show-view .inline-edit--container.subject', text: work_package.subject) # Open log time @@ -69,11 +69,11 @@ describe 'Work package table context menu', js: true do # Open create new child goto_context_menu list_view menu.choose('Create new child') - expect(page).to have_selector('.wp-edit-field.subject input') - expect(page).to have_selector('.wp-inline-edit--field.type') + expect(page).to have_selector('.inline-edit--container.subject input') + expect(page).to have_selector('.inline-edit--field.type') find('#work-packages--edit-actions-cancel').click - expect(page).to have_no_selector('.wp-edit-field.subject input') + expect(page).to have_no_selector('.inline-edit--container.subject input') # Timeline actions only shown when open wp_timeline.expect_timeline!(open: false) diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index ca426b55c77..1d85bfe6b0c 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -53,8 +53,8 @@ module Components row = find_row(relatable) row.find('.relation-row--type').click - expect(row).to have_selector('select.wp-inline-edit--field') - row.find('.wp-inline-edit--field option', text: to_type).select_option + expect(row).to have_selector('select.inline-edit--field') + row.find('.inline-edit--field option', text: to_type).select_option end def hover_action(relatable, action) diff --git a/spec/support/pages/work_packages/abstract_work_package.rb b/spec/support/pages/work_packages/abstract_work_package.rb index e9813aa981f..0a914f2a427 100644 --- a/spec/support/pages/work_packages/abstract_work_package.rb +++ b/spec/support/pages/work_packages/abstract_work_package.rb @@ -36,8 +36,8 @@ module Pages @work_package = work_package @project = project - @type_field_selector = '.wp-edit-field.type' - @subject_field_selector = '.wp-edit-field.subject' + @type_field_selector = '.inline-edit--container.type' + @subject_field_selector = '.inline-edit--container.subject' end def visit_tab!(tab) @@ -80,7 +80,7 @@ module Pages def expect_hidden_field(attribute) page.within(container) do - expect(page).to have_no_selector(".inplace-edit.#{attribute}") + expect(page).to have_no_selector(".inline-edit--display-field.#{attribute}") end end @@ -119,13 +119,13 @@ module Pages if label == 'status' expect(page).to have_selector(".wp-status-button .button", text: value, wait: 10) else - expect(page).to have_selector(".wp-edit-field.#{label.camelize(:lower)}", text: value, wait: 10) + expect(page).to have_selector(".inline-edit--container.#{label.camelize(:lower)}", text: value, wait: 10) end end end def expect_no_attribute(label) - expect(page).not_to have_selector(".wp-edit-field.#{label.downcase}") + expect(page).not_to have_selector(".inline-edit--container.#{label.downcase}") end alias :expect_attribute_hidden :expect_no_attribute diff --git a/spec/support/pages/work_packages/abstract_work_package_create.rb b/spec/support/pages/work_packages/abstract_work_package_create.rb index 36c74a9a267..ef80ec5bb94 100644 --- a/spec/support/pages/work_packages/abstract_work_package_create.rb +++ b/spec/support/pages/work_packages/abstract_work_package_create.rb @@ -49,7 +49,7 @@ module Pages end def select_attribute(property, value) - element = page.first(".wp-edit-field.#{property.downcase} select") + element = page.first(".inline-edit--container.#{property.downcase} select") element.select(value) element diff --git a/spec/support/pages/work_packages/work_packages_table.rb b/spec/support/pages/work_packages/work_packages_table.rb index f7a96ffde31..0ad34b2d678 100644 --- a/spec/support/pages/work_packages/work_packages_table.rb +++ b/spec/support/pages/work_packages/work_packages_table.rb @@ -181,7 +181,7 @@ module Pages def open_full_screen_by_doubleclick(work_package) loading_indicator_saveguard # The 'id' column should have enough space to be clicked - click_target = row(work_package).find('.wp-table--cell-span.id') + click_target = row(work_package).find('.inline-edit--display-field.id') page.driver.browser.action.double_click(click_target.native).perform FullWorkPackage.new(work_package, project) diff --git a/spec/support/work_packages/work_package_field.rb b/spec/support/work_packages/work_package_field.rb index eee9a0eecff..7fbf1fad195 100644 --- a/spec/support/work_packages/work_package_field.rb +++ b/spec/support/work_packages/work_package_field.rb @@ -15,7 +15,7 @@ class WorkPackageField @property_name = property_name.to_s @context = context - @selector = selector || ".wp-edit-field--container.#{property_name}" + @selector = selector || ".inline-edit--container.#{property_name}" end def field_container @@ -23,7 +23,7 @@ class WorkPackageField end def display_selector - '.wp-edit-field--display-field' + '.inline-edit--display-field' end def display_element @@ -203,7 +203,7 @@ class WorkPackageField if property_name == 'description' '.op-ckeditor--wrapper' else - '.wp-inline-edit--field' + '.inline-edit--field' end end diff --git a/spec/support/work_packages/work_package_multi_select_field.rb b/spec/support/work_packages/work_package_multi_select_field.rb index ba454f5f329..1adb6363f47 100644 --- a/spec/support/work_packages/work_package_multi_select_field.rb +++ b/spec/support/work_packages/work_package_multi_select_field.rb @@ -3,7 +3,7 @@ require_relative './work_package_field' class WorkPackageMultiSelectField < WorkPackageField def multiselect? - field_container.has_selector?('.wp-inline-edit--toggle-multiselect .icon-minus2') + field_container.has_selector?('.inline-edit--toggle-multiselect .icon-minus2') end def expect_save_button(enabled: true)