diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap
index 889ca4fca..160086848 100644
--- a/src/zabapgit.prog.abap
+++ b/src/zabapgit.prog.abap
@@ -55,6 +55,7 @@ INCLUDE zabapgit_authorizations.
INCLUDE zabapgit_stage.
INCLUDE zabapgit_git_helpers.
INCLUDE zabapgit_repo.
+INCLUDE zabapgit_news.
INCLUDE zabapgit_stage_logic.
INCLUDE zabapgit_2fa.
INCLUDE zabapgit_http.
diff --git a/src/zabapgit_css_common.w3mi.data.css b/src/zabapgit_css_common.w3mi.data.css
index 960708d7f..584ca974e 100644
--- a/src/zabapgit_css_common.w3mi.data.css
+++ b/src/zabapgit_css_common.w3mi.data.css
@@ -62,6 +62,8 @@ input:focus, textarea:focus {
.pad4px { padding: 4px; }
.w100 { width: 100%; }
.w40 { width: 40%; }
+.float-right { float: right; }
+.pad-right { padding-right: 6px; }
/* STRUCTURE DIVS, HEADER & FOOTER */
div#header {
@@ -126,6 +128,7 @@ div.repo {
margin-top: 3px;
background-color: #f2f2f2;
padding: 0.5em 1em 0.5em 1em;
+ position: relative;
}
.repo_name span.name {
font-weight: bold;
@@ -742,3 +745,49 @@ div.tutorial h2 {
}
.nav-container ul ul li.separator:first-child { border-top: none; }
.nav-container ul ul li.separator:hover { background-color: inherit; }
+
+/* News Announcement */
+
+div.news {
+ position: absolute;
+ z-index: 99;
+ top: 36px;
+ left: 50%;
+ width: 40em;
+ margin-left: -20em;
+ background-color: white;
+ box-shadow: 1px 1px 3px 2px #dcdcdc;
+}
+
+div.news div.headbar {
+ text-transform: uppercase;
+ font-size: small;
+ padding: 4px 6px;
+}
+
+div.news div.title {
+ color: #f8f8f8;
+ background-color: #888;
+}
+
+div.news div.title a { color: #c6d6ec; }
+div.news div.important { color: #e8961b; }
+
+div.news div.newslist {
+ padding: 0.5em;
+ color: #444;
+}
+
+div.news li {
+ padding-left: 10px;
+ list-style-type: none;
+}
+
+div.news h1:first-child { margin: auto; }
+div.news h1 {
+ font-size: inherit;
+ padding: 6px 4px;
+ margin: 4px auto auto;
+ text-decoration: underline;
+ font-weight: normal;
+}
diff --git a/src/zabapgit_html_chunks.prog.abap b/src/zabapgit_html_chunks.prog.abap
index de94e6cca..7b6238512 100644
--- a/src/zabapgit_html_chunks.prog.abap
+++ b/src/zabapgit_html_chunks.prog.abap
@@ -7,9 +7,9 @@ CLASS lcl_gui_chunk_lib DEFINITION FINAL.
PUBLIC SECTION.
CLASS-METHODS render_error
- IMPORTING ix_error TYPE REF TO lcx_exception OPTIONAL
- iv_error TYPE string OPTIONAL
- RETURNING VALUE(ro_html) TYPE REF TO lcl_html.
+ IMPORTING ix_error TYPE REF TO lcx_exception OPTIONAL
+ iv_error TYPE string OPTIONAL
+ RETURNING VALUE(ro_html) TYPE REF TO lcl_html.
CLASS-METHODS render_repo_top
IMPORTING io_repo TYPE REF TO lcl_repo
@@ -17,13 +17,14 @@ CLASS lcl_gui_chunk_lib DEFINITION FINAL.
iv_show_branch TYPE abap_bool DEFAULT abap_true
iv_interactive_branch TYPE abap_bool DEFAULT abap_false
iv_branch TYPE string OPTIONAL
+ io_news TYPE REF TO lcl_news OPTIONAL
RETURNING VALUE(ro_html) TYPE REF TO lcl_html
RAISING lcx_exception.
CLASS-METHODS render_item_state
- IMPORTING iv1 TYPE char1
- iv2 TYPE char1
- RETURNING VALUE(rv_html) TYPE string.
+ IMPORTING iv1 TYPE char1
+ iv2 TYPE char1
+ RETURNING VALUE(rv_html) TYPE string.
CLASS-METHODS render_branch_span
IMPORTING iv_branch TYPE string
@@ -36,6 +37,12 @@ CLASS lcl_gui_chunk_lib DEFINITION FINAL.
RETURNING VALUE(ro_html) TYPE REF TO lcl_html
RAISING lcx_exception.
+ CLASS-METHODS render_news
+ IMPORTING
+ io_news TYPE REF TO lcl_news
+ RETURNING VALUE(ro_html) TYPE REF TO lcl_html
+ RAISING lcx_exception.
+
ENDCLASS. "lcl_gui_chunk_lib
CLASS lcl_gui_chunk_lib IMPLEMENTATION.
@@ -72,6 +79,13 @@ CLASS lcl_gui_chunk_lib IMPLEMENTATION.
ro_html->add( '
' ).
+ IF io_news IS BOUND AND io_news->has_news( ) = abap_true.
+ ro_html->add_a( iv_act = 'displayNews()'
+ iv_typ = gc_action_type-onclick
+ iv_txt = lcl_html=>icon( iv_name = 'pulse/blue'
+ iv_hint = 'Display changelog' ) ).
+ ENDIF.
+
IF abap_true = lcl_app=>user( )->is_favorite_repo( io_repo->get_key( ) ).
lv_icon = 'star/blue' ##NO_TEXT.
ELSE.
@@ -215,4 +229,45 @@ CLASS lcl_gui_chunk_lib IMPLEMENTATION.
ro_html->add( '' ).
ENDMETHOD. "render_js_error_stub
+ METHOD render_news.
+
+ DATA lt_log TYPE lcl_news=>tt_log.
+ FIELD-SYMBOLS: LIKE LINE OF lt_log.
+
+ CREATE OBJECT ro_html.
+
+ IF io_news IS NOT BOUND OR io_news->has_news( ) = abap_false.
+ RETURN.
+ ENDIF.
+
+ lt_log = io_news->get_log( ).
+
+ ro_html->add( '' ).
+
+ ro_html->add( ' Announcement of the latest changes'
+ && ' '
+ && ' ' ).
+
+ IF io_news->has_important_news( ) = abap_true.
+ ro_html->add( ' '
+ && lcl_html=>icon( iv_name = 'alert' iv_class = 'pad-right' )
+ && 'Please note changes marked with "!"'
+ && ' ' ).
+ ENDIF.
+
+ " Generate news
+ ro_html->add( | | ).
+ LOOP AT lt_log ASSIGNING .
+ IF -header = 'X'.
+ ro_html->add( |{ -text }| ).
+ ELSE.
+ ro_html->add( |{ -text }| ).
+ ENDIF.
+ ENDLOOP.
+ ro_html->add( ' ' ).
+
+ ro_html->add( ' ' ).
+
+ ENDMETHOD. "render_news
+
ENDCLASS. "lcl_gui_chunk_lib
diff --git a/src/zabapgit_js_common.w3mi.data.js b/src/zabapgit_js_common.w3mi.data.js
index 4e227e2c5..15adca589 100644
--- a/src/zabapgit_js_common.w3mi.data.js
+++ b/src/zabapgit_js_common.w3mi.data.js
@@ -499,3 +499,13 @@ DiffHelper.prototype.highlightButton = function(state) {
this.dom.filterButton.classList.remove("bgorange");
}
};
+
+/**********************************************************
+ * Other functions
+ **********************************************************/
+
+// News announcement
+function displayNews() {
+ var div = document.getElementById("news");
+ div.style.display = (div.style.display)?'':'none';
+}
diff --git a/src/zabapgit_news.prog.abap b/src/zabapgit_news.prog.abap
new file mode 100644
index 000000000..c872439fd
--- /dev/null
+++ b/src/zabapgit_news.prog.abap
@@ -0,0 +1,402 @@
+*&---------------------------------------------------------------------*
+*& Include ZABAPGIT_NEWS
+*&---------------------------------------------------------------------*
+CLASS ltcl_news DEFINITION DEFERRED.
+
+*&---------------------------------------------------------------------*
+*& Class lcl_news
+*&---------------------------------------------------------------------*
+* Class responsible for preparation of data for news announcements
+*----------------------------------------------------------------------*
+CLASS lcl_news DEFINITION FRIENDS ltcl_news.
+
+ PUBLIC SECTION.
+ TYPES:
+ BEGIN OF ty_log,
+ version TYPE string,
+ header TYPE abap_bool,
+ important TYPE abap_bool,
+ text TYPE string,
+ END OF ty_log,
+ tt_log TYPE STANDARD TABLE OF ty_log WITH DEFAULT KEY.
+
+ CONSTANTS:
+ c_log_path TYPE string VALUE '/',
+ c_log_filename TYPE string VALUE 'changelog.txt'.
+
+ CLASS-METHODS create
+ IMPORTING io_repo TYPE REF TO lcl_repo
+ RETURNING VALUE(ro_instance) TYPE REF TO lcl_news
+ RAISING lcx_exception.
+
+ METHODS:
+ constructor
+ IMPORTING iv_rawdata TYPE xstring
+ iv_version TYPE string,
+ has_news
+ RETURNING value(rv_boolean) TYPE abap_bool,
+ get_log
+ RETURNING value(rt_log) TYPE tt_log,
+ has_important_news
+ RETURNING value(rv_boolean) TYPE abap_bool.
+
+ PRIVATE SECTION.
+ DATA mt_log TYPE tt_log.
+
+ CLASS-METHODS:
+ split_string
+ IMPORTING iv_string TYPE string
+ RETURNING value(rt_lines) TYPE string_table,
+
+ convert_version_to_numeric
+ IMPORTING iv_version TYPE string
+ RETURNING value(rv_version) TYPE i,
+
+ parse_data
+ IMPORTING it_lines TYPE string_table
+ iv_version TYPE string
+ RETURNING value(rt_log) TYPE tt_log,
+
+ compare_versions
+ IMPORTING iv_a TYPE string
+ iv_b TYPE string
+ RETURNING value(rv_result) TYPE i.
+
+ENDCLASS. "lcl_news
+
+*----------------------------------------------------------------------*
+* CLASS lcl_news IMPLEMENTATION
+*----------------------------------------------------------------------*
+*
+*----------------------------------------------------------------------*
+CLASS lcl_news IMPLEMENTATION.
+
+ METHOD constructor.
+
+ DATA: lt_lines TYPE string_table,
+ lv_string TYPE string.
+
+ lv_string = lcl_convert=>xstring_to_string_utf8( iv_rawdata ).
+ lt_lines = lcl_news=>split_string( lv_string ).
+ mt_log = lcl_news=>parse_data( it_lines = lt_lines iv_version = iv_version ).
+
+ ENDMETHOD. "constructor
+
+ METHOD create.
+
+ DATA: lt_remote TYPE ty_files_tt,
+ lo_repo_online TYPE REF TO lcl_repo_online.
+
+ FIELD-SYMBOLS LIKE LINE OF lt_remote.
+
+ IF io_repo->is_offline( ) = abap_false.
+ lo_repo_online ?= io_repo.
+
+ " News announcement temporary restricted to abapGit only
+ IF lo_repo_online->get_url( ) CS '/abapGit.git'.
+ lt_remote = io_repo->get_files_remote( ).
+
+ READ TABLE lt_remote ASSIGNING
+ WITH KEY path = c_log_path filename = c_log_filename.
+
+ IF sy-subrc = 0.
+ CREATE OBJECT ro_instance EXPORTING
+ iv_rawdata = -data
+ iv_version = gc_abap_version.
+* iv_version = 'v1.30.0'. " for debug
+ ENDIF.
+ ENDIF.
+ ENDIF.
+
+ ENDMETHOD. "create
+
+ METHOD split_string.
+
+ DATA ls_line LIKE LINE OF rt_lines.
+
+ FIND FIRST OCCURRENCE OF cl_abap_char_utilities=>cr_lf IN iv_string.
+
+ " Convert string into table depending on separator type CR_LF vs. LF
+ IF sy-subrc = 0.
+ SPLIT iv_string AT cl_abap_char_utilities=>cr_lf INTO TABLE rt_lines.
+ ELSE.
+ SPLIT iv_string AT cl_abap_char_utilities=>newline INTO TABLE rt_lines.
+ ENDIF.
+
+ ENDMETHOD. "split_string
+
+ METHOD parse_data.
+
+ CONSTANTS:
+ lc_changelog_version TYPE string
+ VALUE '^\d{4}-\d{2}-\d{2}\s+v(\d{1,3}\.\d{1,3}\.\d{1,3})\s*$',
+ lc_internal_version TYPE string
+ VALUE '^v?(\d{1,3}\.\d{1,3}\.\d{1,3})\s*$'.
+
+ DATA:
+ lv_first_version_found TYPE abap_bool,
+ lv_version TYPE string,
+ lv_current_version TYPE string,
+ ls_log LIKE LINE OF rt_log.
+
+ FIELD-SYMBOLS: LIKE LINE OF it_lines.
+
+ " Internal program version should be in format "vXXX.XXX.XXX"
+ FIND FIRST OCCURRENCE OF REGEX lc_internal_version IN iv_version SUBMATCHES lv_current_version.
+
+ IF sy-subrc IS NOT INITIAL.
+ RETURN. " Internal format of program version is not correct. TODO implement error message
+ ENDIF.
+
+ LOOP AT it_lines ASSIGNING .
+ CLEAR: lv_version, ls_log-text, ls_log-important, ls_log-header.
+
+ IF IS INITIAL OR CO ' -='. " Skip empty and technical lines
+ CONTINUE.
+ ENDIF.
+
+ " Check if line is a header line
+ FIND FIRST OCCURRENCE OF REGEX lc_changelog_version IN SUBMATCHES lv_version.
+
+ " Skip entries before first version found
+ IF lv_first_version_found = abap_false.
+ IF sy-subrc IS NOT INITIAL.
+ CONTINUE.
+ ELSE.
+ lv_first_version_found = abap_true.
+ ENDIF.
+ ENDIF.
+
+ "Skip everything below current version
+ IF lv_version IS NOT INITIAL
+ AND lcl_news=>compare_versions( iv_a = lv_version iv_b = lv_current_version ) < 1.
+ EXIT.
+ ENDIF.
+
+ " Populate log
+ IF lv_version IS NOT INITIAL. " Version header
+ ls_log-version = lv_version. " ... stays for all subsequent non-version lines
+ ls_log-header = abap_true.
+ ELSE. " Version line item
+ FIND FIRST OCCURRENCE OF REGEX '^\s*!' IN .
+ IF sy-subrc IS INITIAL.
+ ls_log-important = abap_true. " Change is important
+ ENDIF.
+ ENDIF.
+
+ ls_log-text = .
+
+ APPEND ls_log TO rt_log.
+ ENDLOOP.
+
+ ENDMETHOD. "parse_data
+
+ METHOD has_news.
+
+ rv_boolean = boolc( lines( mt_log ) > 0 ).
+
+ ENDMETHOD. "has_news
+
+ METHOD get_log.
+
+ rt_log = me->mt_log.
+
+ ENDMETHOD. "get_log
+
+ METHOD has_important_news.
+
+ READ TABLE mt_log WITH KEY important = abap_true TRANSPORTING NO FIELDS.
+
+ rv_boolean = boolc( sy-subrc IS INITIAL ).
+
+ ENDMETHOD. "has_important_news
+
+ METHOD compare_versions.
+
+ DATA:
+ lv_version_a TYPE i,
+ lv_version_b TYPE i.
+
+ " Convert versions to numeric
+ lv_version_a = lcl_news=>convert_version_to_numeric( iv_a ).
+ lv_version_b = lcl_news=>convert_version_to_numeric( iv_b ).
+
+ " Compare versions
+ IF lv_version_a > lv_version_b.
+ rv_result = 1.
+ ELSEIF lv_version_a < lv_version_b.
+ rv_result = -1.
+ ELSE.
+ rv_result = 0.
+ ENDIF.
+
+ ENDMETHOD. "compare_versions
+
+ METHOD convert_version_to_numeric.
+
+ DATA: lv_major TYPE numc4,
+ lv_minor TYPE numc4,
+ lv_release TYPE numc4.
+
+ SPLIT iv_version AT '.' INTO lv_major lv_minor lv_release.
+
+ " Calculated value of version number
+ rv_version = lv_major * 1000000 + lv_minor * 1000 + lv_release.
+
+ ENDMETHOD. "convert_version_to_numeric
+
+ENDCLASS. "lcl_news
+
+*----------------------------------------------------------------------*
+* CLASS ltcl_news DEFINITION
+*----------------------------------------------------------------------*
+* Definition of test class for news announcement
+*----------------------------------------------------------------------*
+CLASS ltcl_news DEFINITION FINAL
+ FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
+
+ PRIVATE SECTION.
+
+ METHODS:
+ split_string FOR TESTING,
+ convert_version_to_numeric FOR TESTING,
+ compare_versions FOR TESTING,
+ parse_data FOR TESTING.
+
+ENDCLASS. "ltcl_news DEFINITION
+
+*----------------------------------------------------------------------*
+* CLASS ltcl_news IMPLEMENTATION
+*----------------------------------------------------------------------*
+* Implementation of test class for news announcement
+*----------------------------------------------------------------------*
+CLASS ltcl_news IMPLEMENTATION.
+
+ METHOD split_string.
+
+ DATA: lt_act TYPE string_table,
+ lt_exp TYPE string_table.
+
+ APPEND 'ABC' TO lt_exp.
+ APPEND '123' TO lt_exp.
+
+ " Case 1. String separated by CRLF
+ lt_act = lcl_news=>split_string( 'ABC' && cl_abap_char_utilities=>cr_lf && '123' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = lt_exp
+ act = lt_act
+ msg = ' Error during string split: CRLF' ).
+
+ CLEAR: lt_act.
+
+ " Case 2. String separated by LF
+ lt_act = lcl_news=>split_string( 'ABC' && cl_abap_char_utilities=>newline && '123' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = lt_exp
+ act = lt_act
+ msg = ' Error during string split: LF' ).
+
+ ENDMETHOD. "split_string.
+
+ METHOD convert_version_to_numeric.
+
+ DATA: lv_version_exp TYPE i VALUE 1023010,
+ lv_version_act TYPE i.
+
+ lv_version_act = lcl_news=>convert_version_to_numeric( '1.23.10' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = lv_version_exp
+ act = lv_version_act
+ msg = ' Error during conversion of version to numeric value' ).
+
+ ENDMETHOD. "convert_version_to_numeric
+
+ METHOD compare_versions.
+
+ DATA lv_result TYPE i.
+
+ " Case 1: version A > version B
+ lv_result = lcl_news=>compare_versions( iv_a = '1.28.10' iv_b = '1.23.10' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = 1
+ act = lv_result
+ msg = ' Error during comparison of versions. Case: A > B' ).
+
+ CLEAR: lv_result.
+
+ " Case 2: version A < version B
+ lv_result = lcl_news=>compare_versions( iv_a = '1.28.10' iv_b = '2.23.10' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = -1
+ act = lv_result
+ msg = ' Error during comparison of versions. Case: A < B' ).
+
+ CLEAR: lv_result.
+
+ " Case 3: version A = version B
+ lv_result = lcl_news=>compare_versions( iv_a = '1.28.10' iv_b = '1.28.10' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = 0
+ act = lv_result
+ msg = ' Error during comparison of versions. Case: A = B' ).
+
+ ENDMETHOD. "compare_versions
+
+ DEFINE _add_news_log_entry.
+ CLEAR: ls_log.
+ ls_log-version = &1.
+ ls_log-header = &2.
+ ls_log-important = &3.
+ ls_log-text = &4.
+ APPEND ls_log TO lt_log_exp.
+ END-OF-DEFINITION.
+
+ METHOD parse_data.
+
+ DATA:
+ lt_log_exp TYPE lcl_news=>tt_log,
+ lt_log_act TYPE lcl_news=>tt_log,
+ ls_log LIKE LINE OF lt_log_exp,
+ lt_lines TYPE string_table.
+
+ " Generate test data
+ APPEND '======' TO lt_lines.
+ APPEND '------' TO lt_lines.
+ APPEND ` ` TO lt_lines.
+ APPEND 'abapGit changelog' TO lt_lines.
+ APPEND '2017-02-13 v1.28.0' TO lt_lines.
+ APPEND '------------------' TO lt_lines.
+ APPEND '+ Staging page redesigned' TO lt_lines.
+ APPEND '! Support for core data services' TO lt_lines.
+ APPEND ` ` TO lt_lines.
+ APPEND '2017-01-25 v1.27.0' TO lt_lines.
+ APPEND '------------------' TO lt_lines.
+ APPEND '+ Two factor authentication with github.com' TO lt_lines.
+ APPEND '2017-01-25 v1.26.0' TO lt_lines.
+
+ " Generate expected results
+ _add_news_log_entry '1.28.0' 'X' '' '2017-02-13 v1.28.0'.
+ _add_news_log_entry '1.28.0' '' '' '+ Staging page redesigned'.
+ _add_news_log_entry '1.28.0' '' 'X' '! Support for core data services'.
+ _add_news_log_entry '1.27.0' 'X' '' '2017-01-25 v1.27.0'.
+ _add_news_log_entry '1.27.0' '' '' '+ Two factor authentication with github.com'.
+
+ " Case 1. Test parsing of data
+ lt_log_act = lcl_news=>parse_data( it_lines = lt_lines iv_version = '1.26.01' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = lt_log_exp
+ act = lt_log_act
+ msg = ' Error during parsing: Case 1.' ).
+
+ CLEAR: lt_log_act, lt_log_exp.
+
+ " Case 2. Negative test - format of version is not correct
+ lt_log_act = lcl_news=>parse_data( it_lines = lt_lines iv_version = 'version.1.27.00' ).
+
+ cl_abap_unit_assert=>assert_equals( exp = lt_log_exp
+ act = lt_log_act
+ msg = ' Error during parsing: Case 2.' ).
+
+ ENDMETHOD. "parse_data
+
+ENDCLASS. "ltcl_news IMPLEMENTATION
diff --git a/src/zabapgit_news.prog.xml b/src/zabapgit_news.prog.xml
new file mode 100644
index 000000000..753deeb21
--- /dev/null
+++ b/src/zabapgit_news.prog.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ ZABAPGIT_NEWS
+ A
+ X
+ I
+ K
+ E
+ X
+
+
+ -
+ R
+ Include ZABAPGIT_NEWS
+ 21
+
+
+
+
+
diff --git a/src/zabapgit_page_main.prog.abap b/src/zabapgit_page_main.prog.abap
index 99c024f47..0fa1c5392 100644
--- a/src/zabapgit_page_main.prog.abap
+++ b/src/zabapgit_page_main.prog.abap
@@ -303,11 +303,19 @@ CLASS lcl_gui_page_main IMPLEMENTATION.
METHOD render_repo.
+ DATA lo_news TYPE REF TO lcl_news.
+
CREATE OBJECT ro_html.
+ lo_news = lcl_news=>create( io_repo ).
+
ro_html->add( || ).
ro_html->add( lcl_gui_chunk_lib=>render_repo_top( io_repo = io_repo
+ io_news = lo_news
iv_interactive_branch = abap_true ) ).
+
+ ro_html->add( lcl_gui_chunk_lib=>render_news( io_news = lo_news ) ).
+
ro_html->add( mo_repo_content->render( ) ).
ro_html->add( ' ' ).
|