diff --git a/main/webapp/modules/core/MOD-INF/controller.js b/main/webapp/modules/core/MOD-INF/controller.js index 226e77d33..cae639734 100644 --- a/main/webapp/modules/core/MOD-INF/controller.js +++ b/main/webapp/modules/core/MOD-INF/controller.js @@ -516,6 +516,7 @@ function init() { "styles/widgets/slider-widget.less", "styles/views/data-table-view.less", + "styles/views/column-join.less", "styles/dialogs/expression-preview-dialog.less", "styles/dialogs/clustering-dialog.less", diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json index 39a732d5e..edf25b8f3 100644 --- a/main/webapp/modules/core/langs/translation-en.json +++ b/main/webapp/modules/core/langs/translation-en.json @@ -550,6 +550,7 @@ "core-views/url-fetch": "Formulate the URLs to fetch:", "core-views/http-headers": "HTTP headers to be used when fetching URLs:", "core-views/enter-col-name": "Enter new column name", + "core-views/join-col":"Join columns", "core-views/split-col": "Split column", "core-views/several-col": "into several columns", "core-views/how-split": "How to Split Column", @@ -680,6 +681,18 @@ "core-views/use-values-as-identifiers2": "Mark cells as reconciled with their values as identifiers", "core-views/choose-reconciliation-service": "Choose a reconciliation service", "core-views/choose-reconciliation-service-alert": "Please choose a reconciliation service first.", + "core-views/column-join": "Join columns", + "core-views/column-join-before-column-picker": "Select and order columns to join", + "core-views/column-join-before-options": "Select options", + "core-views/column-join-field-separator": "Separator between the content of each column :", + "core-views/column-join-field-separator-advice": "Enter one or more characters, or keep blank to join the columns without separator.", + "core-views/column-join-replace-nulls": "Replace nulls with...", + "core-views/column-join-skip-nulls": "Skip nulls.", + "core-views/column-join-replace-nulls-advice": "Enter one or more characters, or keep blank to replace nulls with blank strings.", + "core-views/column-join-write-selected-column": "Write result in selected column.", + "core-views/column-join-copy-to-new-column": "Write result in new column named...", + "core-views/column-join-delete-joined-columns": "Delete joined columns.", + "core-views/column-join-dont-escape": "In separator and nulls substitutes, use \\n for new lines, \\t for tabulation, \\\\n for \\n, \\\\t for \\t.", "core-buttons/cancel": "Cancel", "core-buttons/ok": "  OK  ", "core-buttons/import-proj": "Import Project", diff --git a/main/webapp/modules/core/scripts/views/data-table/column-join.html b/main/webapp/modules/core/scripts/views/data-table/column-join.html new file mode 100644 index 000000000..275bb7231 --- /dev/null +++ b/main/webapp/modules/core/scripts/views/data-table/column-join.html @@ -0,0 +1,64 @@ +
+
+
+
+
+

+
+
+
+ + +
+
+
+
+

+
+
+ + +

+
+
+
+ + + +

+ +
+
+ + +

+
+
+
+ + +
+
+
+ + +
+
+ + + +
+
+
+ + +
+
+
+
+
+ +
\ No newline at end of file diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js index 845ab5c3f..676ced7ab 100644 --- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js +++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js @@ -33,6 +33,22 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var columnIndex = Refine.columnNameToColumnIndex(column.name); + var doTextTransform = function(columnName, expression, onError, repeat, repeatCount, callbacks) { + callbacks = callbacks || {}; + Refine.postCoreProcess( + "text-transform", + { + columnName: columnName, + onError: onError, + repeat: repeat, + repeatCount: repeatCount + }, + { expression: expression }, + { cellsChanged: true }, + callbacks + ); + }; + var doAddColumn = function() { var frame = $( DOM.loadHTML("core", "scripts/views/data-table/add-column-dialog.html") @@ -320,14 +336,224 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { ); dismiss(); }); + }; + + var doJoinColumns = function() { + var self = this; + var dialog = $(DOM.loadHTML("core","scripts/views/data-table/column-join.html")); + var elmts = DOM.bind(dialog); + var level = DialogSystem.showDialog(dialog); + // Escape strings + function escapeString(s,dontEscape) { + var dontEscape = dontEscape || false; + var temp = s; + if (dontEscape) { + // replace "\n" with newline and "\t" with tab + temp = temp.replace(/\\n/g, '\n').replace(/\\t/g, '\t'); + // replace "\" with "\\" + temp = temp.replace(/\\/g, '\\\\'); + // replace "\newline" with "\n" and "\tab" with "\t" + temp = temp.replace(/\\\n/g, '\\n').replace(/\\\t/g, '\\t'); + // replace ' with \' + temp = temp.replace(/'/g, "\\'"); + } + else { + // escape \ and ' + temp = s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") ; + // useless : .replace(/"/g, '\\"') + } + return temp; + }; + // Close the dialog window + var dismiss = function() { + DialogSystem.dismissUntil(level - 1); + }; + // Join the columns according to user input + var transform = function() { + // function called in a callback + var deleteColumns = function() { + if (deleteJoinedColumns) { + console.log (theProject); + var columnsToKeep = theProject.columnModel.columns + .map (function (col) {return col.name;}) + .filter (function(colName) { + // keep the selected column if it contains the result + return ( + (columnsToJoin.indexOf (colName) == -1) || + ((writeOrCopy !="copy-to-new-column") && (colName == column.name))); + }); + Refine.postCoreProcess( + "reorder-columns", + null, + { "columnNames" : JSON.stringify(columnsToKeep) }, + { modelsChanged: true }, + { includeEngine: false } + ); + } + }; + // get options + var onError = "keep-original" ; + var repeat = false ; + var repeatCount = ""; + var deleteJoinedColumns = elmts.delete_joined_columnsInput[0].checked; + var writeOrCopy = $("input[name='write-or-copy']:checked")[0].value; + var newColumnName = $.trim(elmts.new_column_nameInput[0].value); + var manageNulls = $("input[name='manage-nulls']:checked")[0].value; + var nullSubstitute = elmts.null_substituteInput[0].value; + var fieldSeparator = elmts.field_separatorInput[0].value; + var dontEscape = elmts.dont_escapeInput[0].checked; + // fix options if they are not consistent + if (newColumnName != "") { + writeOrCopy ="copy-to-new-column"; + } else + { + writeOrCopy ="write-selected-column"; + } + if (nullSubstitute != "") { + manageNulls ="replace-nulls"; + } + // build GREL expression + var columnsToJoin = []; + elmts.column_join_columnPicker + .find('.column-join-column input[type="checkbox"]:checked') + .each(function() { + columnsToJoin.push (this.closest ('.column-join-column').getAttribute('column')); + }); + expression = columnsToJoin.map (function (colName) { + if (manageNulls == "skip-nulls") { + return "cells['"+escapeString(colName) +"'].value"; + } + else { + return "coalesce(cells['"+escapeString(colName)+"'].value,'"+ escapeString(nullSubstitute,dontEscape) + "')"; + } + }).join (','); + expression = 'join ([' + expression + '],\'' + escapeString(fieldSeparator,dontEscape) + "')"; + // apply expression to selected column or new column + if (writeOrCopy =="copy-to-new-column") { + Refine.postCoreProcess( + "add-column", + { + baseColumnName: column.name, + newColumnName: newColumnName, + columnInsertIndex: columnIndex + 1, + onError: onError + }, + { expression: expression }, + { modelsChanged: true }, + { onFinallyDone: deleteColumns} + ); + } + else { + doTextTransform( + column.name, + expression, + onError, + repeat, + repeatCount, + { onFinallyDone: deleteColumns}); + } + }; + // core of doJoinColumn + elmts.dialogHeader.text($.i18n('core-views/column-join')); + elmts.or_views_column_join_before_column_picker.text($.i18n('core-views/column-join-before-column-picker')); + elmts.or_views_column_join_before_options.text($.i18n('core-views/column-join-before-options')); + elmts.or_views_column_join_replace_nulls.text($.i18n('core-views/column-join-replace-nulls')); + elmts.or_views_column_join_replace_nulls_advice.text($.i18n('core-views/column-join-replace-nulls-advice')); + elmts.or_views_column_join_skip_nulls.text($.i18n('core-views/column-join-skip-nulls')); + elmts.or_views_column_join_write_selected_column.text($.i18n('core-views/column-join-write-selected-column')); + elmts.or_views_column_join_copy_to_new_column.text($.i18n('core-views/column-join-copy-to-new-column')); + elmts.or_views_column_join_delete_joined_columns.text($.i18n('core-views/column-join-delete-joined-columns')); + elmts.or_views_column_join_field_separator.text($.i18n('core-views/column-join-field-separator')); + elmts.or_views_column_join_field_separator_advice.text($.i18n('core-views/column-join-field-separator-advice')); + elmts.or_views_column_join_dont_escape.text($.i18n('core-views/column-join-dont-escape')); + elmts.selectAllButton.html($.i18n('core-buttons/select-all')); + elmts.deselectAllButton.html($.i18n('core-buttons/deselect-all')); + elmts.okButton.html($.i18n('core-buttons/ok')); + elmts.cancelButton.html($.i18n('core-buttons/cancel')); + /* + * Populate column list. + */ + for (var i = 0; i < theProject.columnModel.columns.length; i++) { + var col = theProject.columnModel.columns[i]; + var colName = col.name; + var div = $('
'). + addClass("column-join-column") + .attr("column", colName) + .appendTo(elmts.column_join_columnPicker); + $(''). + attr('type', 'checkbox') + .prop('checked',(i == columnIndex) ? true : false) + .appendTo(div); + $('') + .text(colName) + .appendTo(div); + } + // Move the selected column on the top of the list + if (columnIndex > 0) { + selectedColumn = elmts.column_join_columnPicker + .find('.column-join-column') + .eq(columnIndex); + selectedColumn.parent().prepend(selectedColumn); + } + // Make the list sortable + elmts.column_join_columnPicker.sortable({}); + /* + * Hook up event handlers. + */ + elmts.column_join_columnPicker + .find('.column-join-column') + .click(function() { + elmts.column_join_columnPicker + .find('.column-join-column') + .removeClass('selected'); + $(this).addClass('selected'); + }); + elmts.selectAllButton + .click(function() { + elmts.column_join_columnPicker + .find('input[type="checkbox"]') + .prop('checked',true); + }); + elmts.deselectAllButton + .click(function() { + elmts.column_join_columnPicker + .find('input[type="checkbox"]') + .prop('checked',false); + }); + elmts.okButton.click(function() { + transform(); + dismiss(); + }); + elmts.cancelButton.click(function() { + dismiss(); + }); + elmts.new_column_nameInput.change(function() { + if (elmts.new_column_nameInput[0].value != "") { + elmts.copy_to_new_columnInput.prop('checked',true); + } else + { + elmts.write_selected_columnInput.prop('checked',true); + } + }); + elmts.null_substituteInput.change(function() { + elmts.replace_nullsInput.prop('checked',true); + }); }; +/* + * Create global menu + */ MenuSystem.appendTo(menu, [ "core/edit-column" ], [ { id: "core/split-column", label: $.i18n('core-views/split-into-col')+"...", click: doSplitColumn }, + { + id: "core/join-column", + label: $.i18n('core-views/join-col')+"...", + click : doJoinColumns + }, {}, { id: "core/add-column", @@ -369,7 +595,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { { id: "core/move-column-to-left", label: $.i18n('core-views/move-to-left'), - click: function() { doMoveColumnBy(-1); } + click: function() { doMoveColumnBy(-1);} }, { id: "core/move-column-to-right", diff --git a/main/webapp/modules/core/styles/views/column-join.less b/main/webapp/modules/core/styles/views/column-join.less new file mode 100644 index 000000000..bc6187125 --- /dev/null +++ b/main/webapp/modules/core/styles/views/column-join.less @@ -0,0 +1,85 @@ +/* + +Copyright 2019, Mathieu Saby. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +/* +Same properties as dialogs/custom-tabular-exporter-dialog.less, applied to the class and ids of colum-join.html +*/ + +@import-less url("../theme.less"); + +.dialog-header,.dialog_body,.dialog-footer,.global_selection { + clear:both; +} + +.column-join-left-pane { + float:left; + width : 40%; + margin-right: 10px; +} + +.column-join-inner-left-pane,.column-join-inner-right-pane { + border: 1px solid @chrome_primary; + height: 24em; + padding: @padding_loose; + } + +.column-join-inner-left-pane-list { + overflow: auto; + height:22em; + } + + +.column-join-global-selection { + margin-top:0.5em; +} + +.column-join-column { + border: 1px solid @chrome_primary; + background: @fill_secondary; + padding: @padding_tighter; + margin: @padding_tight; + cursor: move; + .rounded_corners(); + } +.column-join-column.selected { + background: @chrome_primary; + font-weight: bold; + } + +.column-join-inner-right-pane .option { + margin-top:1em; +} + +.column-join-inner-right-pane .tip,.column-join-inner-left-pane .tip{ + font-size:0.9em; +}