diff --git a/apps/spreadsheeteditor/main/app/controller/DataTab.js b/apps/spreadsheeteditor/main/app/controller/DataTab.js index c09d9ac0d..5fe81fb6b 100644 --- a/apps/spreadsheeteditor/main/app/controller/DataTab.js +++ b/apps/spreadsheeteditor/main/app/controller/DataTab.js @@ -43,7 +43,8 @@ define([ 'core', 'spreadsheeteditor/main/app/view/DataTab', 'spreadsheeteditor/main/app/view/GroupDialog', - 'spreadsheeteditor/main/app/view/SortDialog' + 'spreadsheeteditor/main/app/view/SortDialog', + 'spreadsheeteditor/main/app/view/RemoveDuplicatesDialog' ], function () { 'use strict'; @@ -88,7 +89,8 @@ define([ 'data:show': this.onShowClick, 'data:hide': this.onHideClick, 'data:groupsettings': this.onGroupSettings, - 'data:sortcustom': this.onCustomSort + 'data:sortcustom': this.onCustomSort, + 'data:remduplicates': this.onRemoveDuplicates }, 'Statusbar': { 'sheet:changed': this.onApiSheetChanged @@ -258,6 +260,49 @@ define([ } }, + onRemoveDuplicates: function() { + var me = this; + if (this.api) { + var res = this.api.asc_sortCellsRangeExpand(); + if (res) { + var config = { + width: 500, + title: this.txtRemDuplicates, + msg: this.txtExpandRemDuplicates, + + buttons: [ {caption: this.txtExpand, primary: true, value: 'expand'}, + {caption: this.txtRemSelected, primary: true, value: 'remove'}, + 'cancel'], + callback: function(btn){ + if (btn == 'expand' || btn == 'remove') { + setTimeout(function(){ + me.showRemDuplicates(btn == 'expand'); + },1); + } + } + }; + Common.UI.alert(config); + } else + me.showRemDuplicates(res !== null); + } + }, + + showRemDuplicates: function(expand) { + var me = this, + props = me.api.asc_getRemoveDuplicates(expand); + if (props) { + (new SSE.Views.RemoveDuplicatesDialog({ + props: props, + api: me.api, + handler: function (result, settings) { + if (me && me.api) { + me.api.asc_setRemoveDuplicates(settings, result != 'ok'); + } + } + })).show(); + } + }, + onWorksheetLocked: function(index,locked) { if (index == this.api.asc_getActiveWorksheetIndex()) { Common.Utils.lockControls(SSE.enumLock.sheetLock, locked, {array: this.view.btnsSortDown.concat(this.view.btnsSortUp, this.view.btnCustomSort, this.view.btnGroup, this.view.btnUngroup)}); @@ -271,7 +316,11 @@ define([ this.onWorksheetLocked(currentSheet, this.api.asc_isWorksheetLockedOrDeleted(currentSheet)); }, - textWizard: 'Text to Columns Wizard' + textWizard: 'Text to Columns Wizard', + txtRemDuplicates: 'Remove Duplicates', + txtExpandRemDuplicates: 'The data next to the selection will not be removed. Do you want to expand the selection to include the adjacent data or continue with the currently selected cells only?', + txtExpand: 'Expand', + txtRemSelected: 'Remove in selected' }, SSE.Controllers.DataTab || {})); }); \ No newline at end of file diff --git a/apps/spreadsheeteditor/main/app/controller/Main.js b/apps/spreadsheeteditor/main/app/controller/Main.js index d6c456e67..8f8bff632 100644 --- a/apps/spreadsheeteditor/main/app/controller/Main.js +++ b/apps/spreadsheeteditor/main/app/controller/Main.js @@ -1477,6 +1477,15 @@ define([ config.msg = this.errorFTRangeIncludedOtherTables; break; + + case Asc.c_oAscError.ID.RemoveDuplicates: + config.iconCls = 'info'; + config.title = Common.UI.Window.prototype.textInformation; + config.buttons = ['ok']; + config.msg = (errData.asc_getDuplicateValues()!==null && errData.asc_getUniqueValues()!==null) ? Common.Utils.String.format(this.errRemDuplicates, errData.asc_getDuplicateValues(), errData.asc_getUniqueValues()) : this.errNoDuplicates; + config.maxwidth = 600; + break; + default: config.msg = (typeof id == 'string') ? id : this.errorDefaultMessage.replace('%1', id); break; @@ -1525,7 +1534,7 @@ define([ if (id == Asc.c_oAscError.ID.EditingError || $('.asc-window.modal.alert:visible').length < 1 && (id !== Asc.c_oAscError.ID.ForceSaveTimeout)) { Common.UI.alert(config); - Common.component.Analytics.trackEvent('Internal Error', id.toString()); + (id!==undefined) && Common.component.Analytics.trackEvent('Internal Error', id.toString()); } }, @@ -2519,7 +2528,9 @@ define([ txtValues: 'Values', txtGrandTotal: 'Grand Total', txtRowLbls: 'Row Labels', - txtColLbls: 'Column Labels' + txtColLbls: 'Column Labels', + errNoDuplicates: 'No duplicate values found.', + errRemDuplicates: 'Duplicate values found and deleted: {0}, unique values left: {1}.' } })(), SSE.Controllers.Main || {})) }); diff --git a/apps/spreadsheeteditor/main/app/controller/Toolbar.js b/apps/spreadsheeteditor/main/app/controller/Toolbar.js index c3ed3e331..08cd523a2 100644 --- a/apps/spreadsheeteditor/main/app/controller/Toolbar.js +++ b/apps/spreadsheeteditor/main/app/controller/Toolbar.js @@ -2273,7 +2273,7 @@ define([ } need_disable = this._state.controlsdisabled.filters || (val===null); toolbar.lockToolbar(SSE.enumLock.ruleFilter, need_disable, - { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, toolbar.btnTableTemplate, toolbar.btnInsertTable) }); + { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, toolbar.btnTableTemplate, toolbar.btnInsertTable, toolbar.btnRemoveDuplicates) }); val = (formatTableInfo) ? formatTableInfo.asc_getTableStyleName() : null; if (this._state.tablestylename !== val && this.toolbar.mnuTableTemplatePicker) { @@ -2303,11 +2303,12 @@ define([ toolbar.lockToolbar(SSE.enumLock.multiselect, this._state.multiselect, { array: [toolbar.btnTableTemplate, toolbar.btnInsertHyperlink, toolbar.btnInsertTable]}); this._state.inpivot = !!info.asc_getPivotTableInfo(); - toolbar.lockToolbar(SSE.enumLock.editPivot, this._state.inpivot, { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsClearAutofilter, toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, toolbar.btnMerge, toolbar.btnInsertHyperlink, toolbar.btnInsertTable)}); + toolbar.lockToolbar(SSE.enumLock.editPivot, this._state.inpivot, { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsClearAutofilter, toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, + toolbar.btnMerge, toolbar.btnInsertHyperlink, toolbar.btnInsertTable, toolbar.btnRemoveDuplicates)}); need_disable = !this.appConfig.canModifyFilter; - toolbar.lockToolbar(SSE.enumLock.cantModifyFilter, need_disable, { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, toolbar.btnTableTemplate, toolbar.btnClearStyle.menu.items[0], toolbar.btnClearStyle.menu.items[2], - toolbar.btnInsertTable)}); + toolbar.lockToolbar(SSE.enumLock.cantModifyFilter, need_disable, { array: toolbar.btnsSetAutofilter.concat(toolbar.btnsSortDown, toolbar.btnsSortUp, toolbar.btnCustomSort, toolbar.btnTableTemplate, + toolbar.btnClearStyle.menu.items[0], toolbar.btnClearStyle.menu.items[2], toolbar.btnInsertTable, toolbar.btnRemoveDuplicates)}); } @@ -3175,6 +3176,7 @@ define([ me.toolbar.btnsSetAutofilter = datatab.getButtons('set-filter'); me.toolbar.btnsClearAutofilter = datatab.getButtons('clear-filter'); me.toolbar.btnCustomSort = datatab.getButtons('sort-custom'); + me.toolbar.btnRemoveDuplicates = datatab.getButtons('rem-duplicates'); var formulatab = me.getApplication().getController('FormulaDialog'); formulatab.setConfig({toolbar: me}); diff --git a/apps/spreadsheeteditor/main/app/template/Toolbar.template b/apps/spreadsheeteditor/main/app/template/Toolbar.template index 0cc1fe7b9..958f2d26d 100644 --- a/apps/spreadsheeteditor/main/app/template/Toolbar.template +++ b/apps/spreadsheeteditor/main/app/template/Toolbar.template @@ -205,6 +205,7 @@
+
diff --git a/apps/spreadsheeteditor/main/app/view/DataTab.js b/apps/spreadsheeteditor/main/app/view/DataTab.js index 5b2f59a8d..3ad81a080 100644 --- a/apps/spreadsheeteditor/main/app/view/DataTab.js +++ b/apps/spreadsheeteditor/main/app/view/DataTab.js @@ -66,6 +66,9 @@ define([ me.btnTextToColumns.on('click', function (b, e) { me.fireEvent('data:tocolumns'); }); + me.btnRemoveDuplicates.on('click', function (b, e) { + me.fireEvent('data:remduplicates'); + }); // isn't used for awhile // me.btnShow.on('click', function (b, e) { // me.fireEvent('data:show'); @@ -166,6 +169,16 @@ define([ // Common.Utils.injectComponent($host.find('#slot-btn-hide-details'), this.btnHide); // this.lockedControls.push(this.btnHide); + this.btnRemoveDuplicates = new Common.UI.Button({ + cls: 'btn-toolbar x-huge icon-top', + iconCls: 'toolbar__icon btn-remove-duplicates', + caption: this.capBtnTextRemDuplicates, + disabled: true, + lock: [_set.editCell, _set.selChart, _set.selChartText, _set.selShape, _set.selShapeText, _set.selImage, _set.lostConnect, _set.coAuth, _set.ruleFilter, _set.editPivot, _set.cantModifyFilter, _set.sheetLock] + }); + Common.Utils.injectComponent($host.find('#slot-btn-rem-duplicates'), this.btnRemoveDuplicates); + this.lockedControls.push(this.btnRemoveDuplicates); + this.btnCustomSort = new Common.UI.Button({ cls: 'btn-toolbar x-huge icon-top', iconCls: 'toolbar__icon btn-custom-sort', @@ -226,6 +239,7 @@ define([ me.btnGroup.setMenu(_menu); me.btnTextToColumns.updateHint(me.tipToColumns); + me.btnRemoveDuplicates.updateHint(me.tipRemDuplicates); me.btnsSortDown.forEach( function(btn) { btn.updateHint(me.toolbar.txtSortAZ); @@ -261,6 +275,8 @@ define([ return this.btnsSetAutofilter; else if (type == 'clear-filter') return this.btnsClearAutofilter; + else if (type == 'rem-duplicates') + return this.btnRemoveDuplicates; else if (type===undefined) return this.lockedControls; return []; @@ -290,7 +306,9 @@ define([ textBelow: 'Summary rows below detail', textRightOf: 'Summary columns to right of detail', capBtnTextCustomSort: 'Custom Sort', - tipCustomSort: 'Custom sort' + tipCustomSort: 'Custom sort', + capBtnTextRemDuplicates: 'Remove Duplicates', + tipRemDuplicates: 'Remove duplicate rows from a sheet' } }()), SSE.Views.DataTab || {})); }); diff --git a/apps/spreadsheeteditor/main/app/view/RemoveDuplicatesDialog.js b/apps/spreadsheeteditor/main/app/view/RemoveDuplicatesDialog.js new file mode 100644 index 000000000..887c691ff --- /dev/null +++ b/apps/spreadsheeteditor/main/app/view/RemoveDuplicatesDialog.js @@ -0,0 +1,303 @@ +/* + * + * (c) Copyright Ascensio System SIA 2010-2020 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha + * street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product + * logo when distributing the program. Pursuant to Section 7(e) we decline to + * grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ +/** + * RemoveDuplicatesDialog.js + * + * Created by Julia Radzhabova on 07.04.2020 + * Copyright (c) 2020 Ascensio System SIA. All rights reserved. + * + */ + +define([ + 'common/main/lib/component/Window', + 'common/main/lib/component/ComboBox', + 'common/main/lib/component/ListView' +], function () { + 'use strict'; + + SSE.Views.RemoveDuplicatesDialog = Common.UI.Window.extend(_.extend({ + options: { + width: 350, + style: 'min-width: 230px;', + cls: 'modal-dlg', + buttons: ['ok', 'cancel'] + }, + + initialize : function (options) { + var t = this, + _options = {}; + + _.extend(this.options, { + title: this.txtTitle + }, options || {}); + + this.template = [ + '
', + '
', + '', + '
', + '
', + '
', + '
', + '
', + '', + '
', + '
', + '
' + ].join(''); + + this.options.tpl = _.template(this.template)(this.options); + this.api = this.options.api; + this.props = this.options.props; + this.handler = this.options.handler; + + Common.UI.Window.prototype.initialize.call(this, this.options); + }, + render: function () { + Common.UI.Window.prototype.render.call(this); + + this.chHeaders = new Common.UI.CheckBox({ + el: $('#rem-duplicates-dlg-headers'), + labelText: this.textHeaders + }); + this.chHeaders.on('change', _.bind(function(field, newValue, oldValue, eOpts){ + this.props.asc_setHasHeaders(field.getValue()=='checked'); + this.props.asc_updateColumnList(); + this.updateColumnsList(); + }, this)); + + this.columnsList = new Common.UI.ListView({ + el: $('#rem-duplicates-dlg-columns', this.$window), + store: new Common.UI.DataViewStore(), + simpleAddMode: true, + scrollAlwaysVisible: true, + template: _.template(['
'].join('')), + itemTemplate: _.template([ + '
', + '
' + ].join('')) + }); + this.columnsList.on({ + 'item:change': this.onItemChanged.bind(this), + 'item:add': this.onItemChanged.bind(this), + 'item:select': this.onCellCheck.bind(this) + }); + this.columnsList.onKeyDown = _.bind(this.onListKeyDown, this); + + this.$window.find('.dlg-btn').on('click', _.bind(this.onBtnClick, this)); + this.afterRender(); + }, + + updateColumnsList: function() { + var selectAllState = false, + selectedCells = 0, + arr = [], + store = this.columnsList.store; + + if (store.length<1) { + this.props.asc_getColumnList().forEach(function (item, index) { + var visible = item.asc_getVisible(); + arr.push(new Common.UI.DataViewModel({ + id : index, + selected : false, + allowSelected : true, + value : item.asc_getVal(), + groupid : '1', + check : visible + })); + if (visible) selectedCells++; + }); + + if (selectedCells==arr.length) selectAllState = true; + else if (selectedCells>0) selectAllState = 'indeterminate'; + + if (arr.length>0) + arr.unshift(new Common.UI.DataViewModel({ + id : arr.length, + selected : false, + allowSelected : true, + value : this.textSelectAll, + groupid : '0', + check : selectAllState, + throughIndex : 0 + })); + this.columnsList.store.reset(arr); + this.columnsList.scroller.update({minScrollbarLength : 40, alwaysVisibleY: true, suppressScrollX: true}); + } else { + this.props.asc_getColumnList().forEach(function (item, index) { + store.at(index+1).set('value', item.asc_getVal()); + }); + } + }, + + onItemChanged: function (view, record) { + var state = record.model.get('check'); + if ( state == 'indeterminate' ) + $('input[type=checkbox]', record.$el).prop('indeterminate', true); + else $('input[type=checkbox]', record.$el).prop({checked: state, indeterminate: false}); + }, + + onCellCheck: function (listView, itemView, record) { + if (this.checkCellTrigerBlock) + return; + + var target = '', isLabel = false, bound = null; + + var event = window.event ? window.event : window._event; + if (event) { + target = $(event.currentTarget).find('.list-item'); + + if (target.length) { + bound = target.get(0).getBoundingClientRect(); + var _clientX = event.clientX*Common.Utils.zoom(), + _clientY = event.clientY*Common.Utils.zoom(); + if (bound.left < _clientX && _clientX < bound.right && + bound.top < _clientY && _clientY < bound.bottom) { + isLabel = true; + } + } + + if (isLabel || event.target.className.match('checkbox')) { + this.updateCellCheck(listView, record); + + _.delay(function () { + listView.$el.find('.listview').focus(); + }, 100, this); + } + } + }, + + onListKeyDown: function (e, data) { + var record = null, listView = this.columnsList; + + if (listView.disabled) return; + if (_.isUndefined(undefined)) data = e; + + if (data.keyCode == Common.UI.Keys.SPACE) { + data.preventDefault(); + data.stopPropagation(); + + this.updateCellCheck(listView, listView.getSelectedRec()); + + } else { + Common.UI.DataView.prototype.onKeyDown.call(this.columnsList, e, data); + } + }, + + updateCellCheck: function (listView, record) { + if (record && listView) { + var store = listView.store, + check = !record.get('check'), + me = this; + if ('0' == record.get('groupid')) { + store.each(function(cell) { + cell.set('check', check); + }); + } else { + record.set('check', check); + var selectAllState = check; + for (var i=0; i< store.length; i++) { + var cell = store.at(i); + if ('1' == cell.get('groupid') && cell.get('check') !== check) { + selectAllState = 'indeterminate'; + break; + } + } + this.checkCellTrigerBlock = true; + store.at(0).set('check', selectAllState); + this.checkCellTrigerBlock = undefined; + } + + listView.scroller.update({minScrollbarLength : 40, alwaysVisibleY: true, suppressScrollX: true}); + } + }, + + afterRender: function() { + this._setDefaults(this.props); + }, + + _setDefaults: function (props) { + if (props) { + this.chHeaders.setValue(!!props.asc_getHasHeaders(), true); + this.updateColumnsList(); + } + }, + + getSettings: function () { + var store = this.columnsList.store, + props = this.props.asc_getColumnList(); + store.each(function(item, index) { + if ('1' == item.get('groupid')) { + props[index-1].asc_setVisible(item.get('check')); + } + }); + return this.props; + }, + + onBtnClick: function(event) { + this._handleInput(event.currentTarget.attributes['result'].value); + }, + + onDblClickFormat: function () { + this._handleInput('ok'); + }, + + onPrimary: function(event) { + this._handleInput('ok'); + return false; + }, + + _handleInput: function(state) { + if (this.options.handler) { + this.options.handler.call(this, state, (state == 'ok') ? this.getSettings() : this.props); + } + + this.close(); + }, + + // + txtTitle: 'Remove Duplicates', + textDescription: 'To delete duplicate values, select one or more columns that contain duplicates.', + textHeaders: 'My data has headers', + textColumns: 'Columns', + textSelectAll: 'Select All' + + }, SSE.Views.RemoveDuplicatesDialog || {})); +}); \ No newline at end of file diff --git a/apps/spreadsheeteditor/main/locale/en.json b/apps/spreadsheeteditor/main/locale/en.json index 45122c077..1f3fd1559 100644 --- a/apps/spreadsheeteditor/main/locale/en.json +++ b/apps/spreadsheeteditor/main/locale/en.json @@ -265,6 +265,10 @@ "Common.Views.SymbolTableDialog.textDOQuote": "Double Opening Quote", "Common.Views.SymbolTableDialog.textDCQuote": "Double Closing Quote", "SSE.Controllers.DataTab.textWizard": "Text to Columns", + "SSE.Controllers.DataTab.txtRemDuplicates": "Remove Duplicates", + "SSE.Controllers.DataTab.txtExpandRemDuplicates": "The data next to the selection will not be removed. Do you want to expand the selection to include the adjacent data or continue with the currently selected cells only?", + "SSE.Controllers.DataTab.txtExpand": "Expand", + "SSE.Controllers.DataTab.txtRemSelected": "Remove in selected", "SSE.Controllers.DocumentHolder.alignmentText": "Alignment", "SSE.Controllers.DocumentHolder.centerText": "Center", "SSE.Controllers.DocumentHolder.deleteColumnText": "Delete Column", @@ -807,6 +811,8 @@ "SSE.Controllers.Main.txtGrandTotal": "Grand Total", "SSE.Controllers.Main.txtRowLbls": "Row Labels", "SSE.Controllers.Main.txtColLbls": "Column Labels", + "SSE.Controllers.Main.errNoDuplicates": "No duplicate values found.", + "SSE.Controllers.Main.errRemDuplicates": "Duplicate values found and deleted: {0}, unique values left: {1}.", "SSE.Controllers.Print.strAllSheets": "All Sheets", "SSE.Controllers.Print.textWarning": "Warning", "SSE.Controllers.Print.txtCustom": "Custom", @@ -1420,6 +1426,8 @@ "SSE.Views.DataTab.tipGroup": "Group range of cells", "SSE.Views.DataTab.tipToColumns": "Separate cell text into columns", "SSE.Views.DataTab.tipUngroup": "Ungroup range of cells", + "SSE.Views.DataTab.capBtnTextRemDuplicates": "Remove Duplicates", + "SSE.Views.DataTab.tipRemDuplicates": "Remove duplicate rows from a sheet", "SSE.Views.DigitalFilterDialog.capAnd": "And", "SSE.Views.DigitalFilterDialog.capCondition1": "equals", "SSE.Views.DigitalFilterDialog.capCondition10": "does not end with", @@ -2080,6 +2088,11 @@ "SSE.Views.PrintTitlesDialog.textFirstRow": "First row", "SSE.Views.PrintTitlesDialog.textFirstCol": "First column", "SSE.Views.PrintTitlesDialog.textInvalidRange": "ERROR! Invalid cells range", + "SSE.Views.RemoveDuplicatesDialog.txtTitle": "Remove Duplicates", + "SSE.Views.RemoveDuplicatesDialog.textDescription": "To delete duplicate values, select one or more columns that contain duplicates.", + "SSE.Views.RemoveDuplicatesDialog.textHeaders": "My data has headers", + "SSE.Views.RemoveDuplicatesDialog.textColumns": "Columns", + "SSE.Views.RemoveDuplicatesDialog.textSelectAll": "Select All", "SSE.Views.RightMenu.txtCellSettings": "Cell settings", "SSE.Views.RightMenu.txtChartSettings": "Chart settings", "SSE.Views.RightMenu.txtImageSettings": "Image settings", diff --git a/apps/spreadsheeteditor/main/resources/img/toolbar/1.5x/big/btn-remove-duplicates.png b/apps/spreadsheeteditor/main/resources/img/toolbar/1.5x/big/btn-remove-duplicates.png new file mode 100644 index 000000000..66bb66ca9 Binary files /dev/null and b/apps/spreadsheeteditor/main/resources/img/toolbar/1.5x/big/btn-remove-duplicates.png differ diff --git a/apps/spreadsheeteditor/main/resources/img/toolbar/1x/big/btn-remove-duplicates.png b/apps/spreadsheeteditor/main/resources/img/toolbar/1x/big/btn-remove-duplicates.png new file mode 100644 index 000000000..4705cae78 Binary files /dev/null and b/apps/spreadsheeteditor/main/resources/img/toolbar/1x/big/btn-remove-duplicates.png differ diff --git a/apps/spreadsheeteditor/main/resources/img/toolbar/2x/big/btn-remove-duplicates.png b/apps/spreadsheeteditor/main/resources/img/toolbar/2x/big/btn-remove-duplicates.png new file mode 100644 index 000000000..727024606 Binary files /dev/null and b/apps/spreadsheeteditor/main/resources/img/toolbar/2x/big/btn-remove-duplicates.png differ