diff --git a/extensions/database/module/scripts/index/database-import-controller.js b/extensions/database/module/scripts/index/database-import-controller.js index 8ca32f0e52f25..ee4af9a58d75e 100644 --- a/extensions/database/module/scripts/index/database-import-controller.js +++ b/extensions/database/module/scripts/index/database-import-controller.js @@ -309,7 +309,7 @@ Refine.DatabaseImportController.prototype._getPreviewData = function(callback, n result.rowModel = data; callback(result); }, - "jsonp" + "json" ); }, "json" diff --git a/extensions/wikibase/module/scripts/dialogs/perform-edits-dialog.js b/extensions/wikibase/module/scripts/dialogs/perform-edits-dialog.js index e515bfee65241..e4e07806059e4 100644 --- a/extensions/wikibase/module/scripts/dialogs/perform-edits-dialog.js +++ b/extensions/wikibase/module/scripts/dialogs/perform-edits-dialog.js @@ -60,7 +60,7 @@ PerformEditsDialog.launch = function(logged_in_username, max_severity) { tag: WikibaseManager.getSelectedWikibaseTagTemplate(), maxEditsPerMinute: WikibaseManager.getSelectedWikibaseMaxEditsPerMinute() }, - { includeEngine: true, cellsChanged: true, columnStatsChanged: true }, + { includeEngine: true, cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }, { onDone: function() { dismiss(); } } ); }; diff --git a/main/src/com/google/refine/browsing/Engine.java b/main/src/com/google/refine/browsing/Engine.java index 005f0f81b9201..9844e79741c2f 100644 --- a/main/src/com/google/refine/browsing/Engine.java +++ b/main/src/com/google/refine/browsing/Engine.java @@ -46,6 +46,7 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import com.google.refine.browsing.util.ConjunctiveFilteredRows; import com.google.refine.browsing.util.FilteredRecordsAsFilteredRows; import com.google.refine.model.Project; +import com.google.refine.model.Record; import com.google.refine.model.Row; /** @@ -105,7 +106,7 @@ public void accept(Project project, RowVisitor visitor) { int c = project.rows.size(); for (int rowIndex = 0; rowIndex < c; rowIndex++) { Row row = project.rows.get(rowIndex); - if (visitor.visit(project, rowIndex, row)) { + if (visitor.visit(project, rowIndex, rowIndex, row)) { break; } } @@ -150,7 +151,8 @@ public void accept(Project project, RecordVisitor visitor) { int c = project.recordModel.getRecordCount(); for (int r = 0; r < c; r++) { - visitor.visit(project, project.recordModel.getRecord(r)); + Record record = project.recordModel.getRecord(r); + visitor.visit(project, record.fromRowIndex, record); } } finally { visitor.end(project); diff --git a/main/src/com/google/refine/browsing/RecordVisitor.java b/main/src/com/google/refine/browsing/RecordVisitor.java index ecdad562b90a8..656b0027e37bd 100644 --- a/main/src/com/google/refine/browsing/RecordVisitor.java +++ b/main/src/com/google/refine/browsing/RecordVisitor.java @@ -44,9 +44,29 @@ public interface RecordVisitor { public void start(Project project); // called before any visit() call + /** + * @deprecated use {@link #visit(Project, int, Record)} + */ + @Deprecated public boolean visit( Project project, Record record); + /** + * @param project + * project the record is part of + * @param sortedStartRowIndex + * zero-based sorted index of the first row in the record + * @param row + * row + * @return true to abort visitation early - no further visit calls will be made + */ + public default boolean visit( + Project project, + int sortedStartRowIndex, + Record record) { + return visit(project, record); + } + public void end(Project project); // called after all visit() calls } diff --git a/main/src/com/google/refine/browsing/RowVisitor.java b/main/src/com/google/refine/browsing/RowVisitor.java index f5c8b85ceda93..4dbb24a5e1db4 100644 --- a/main/src/com/google/refine/browsing/RowVisitor.java +++ b/main/src/com/google/refine/browsing/RowVisitor.java @@ -51,18 +51,39 @@ public interface RowVisitor { /** * @param project - * project + * project the row is part of * @param rowIndex - * zero-based row index + * zero-based row index (unaffected by a temporary sort) * @param row * row * @return true to abort visitation early - no further visit calls will be made + * @deprecated use {@link #visit(Project, int, int, Row)} */ + @Deprecated public boolean visit( Project project, int rowIndex, Row row); + /** + * @param project + * project the row is part of + * @param rowIndex + * zero-based row index (unaffected by a temporary sort) + * @param sortedRowIndex + * zero-based row index in the sorted view (or equal to rowIndex if no temporary sorting is in place) + * @param row + * row + * @return true to abort visitation early - no further visit calls will be made + */ + public default boolean visit( + Project project, + int rowIndex, + int sortedRowIndex, + Row row) { + return visit(project, rowIndex, row); + } + /** * Called after all visit() calls. * diff --git a/main/src/com/google/refine/browsing/facets/ScatterplotDrawingRowVisitor.java b/main/src/com/google/refine/browsing/facets/ScatterplotDrawingRowVisitor.java index 24f82a9e8aa76..296cb179c67a9 100644 --- a/main/src/com/google/refine/browsing/facets/ScatterplotDrawingRowVisitor.java +++ b/main/src/com/google/refine/browsing/facets/ScatterplotDrawingRowVisitor.java @@ -150,7 +150,7 @@ public boolean visit(Project project, int rowIndex, Row row) { @Override public boolean visit(Project project, Record record) { for (int r = record.fromRowIndex; r < record.toRowIndex; r++) { - visit(project, r, project.rows.get(r)); + visit(project, r, r, project.rows.get(r)); } return false; } diff --git a/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRecords.java b/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRecords.java index be6b80b1faf63..c32867f69eafc 100644 --- a/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRecords.java +++ b/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRecords.java @@ -62,7 +62,7 @@ public void accept(Project project, RecordVisitor visitor) { for (int r = 0; r < c; r++) { Record record = project.recordModel.getRecord(r); if (matchRecord(project, record)) { - if (visitor.visit(project, record)) { + if (visitor.visit(project, record.fromRowIndex, record)) { return; } } diff --git a/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRows.java b/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRows.java index d7d2a3d9ca6a9..901a896f0659d 100644 --- a/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRows.java +++ b/main/src/com/google/refine/browsing/util/ConjunctiveFilteredRows.java @@ -74,7 +74,7 @@ public void accept(Project project, RowVisitor visitor) { } protected boolean visitRow(Project project, RowVisitor visitor, int rowIndex, Row row) { - return visitor.visit(project, rowIndex, row); + return visitor.visit(project, rowIndex, rowIndex, row); } protected boolean matchRow(Project project, int rowIndex, Row row) { diff --git a/main/src/com/google/refine/browsing/util/RowVisitorAsRecordVisitor.java b/main/src/com/google/refine/browsing/util/RowVisitorAsRecordVisitor.java index bb9d385001bfb..abab90f696d5a 100644 --- a/main/src/com/google/refine/browsing/util/RowVisitorAsRecordVisitor.java +++ b/main/src/com/google/refine/browsing/util/RowVisitorAsRecordVisitor.java @@ -59,7 +59,7 @@ public void end(Project project) { @Override public boolean visit(Project project, Record record) { for (int r = record.fromRowIndex; r < record.toRowIndex; r++) { - if (_rowVisitor.visit(project, r, project.rows.get(r))) { + if (_rowVisitor.visit(project, r, r, project.rows.get(r))) { return true; } } diff --git a/main/src/com/google/refine/commands/row/GetRowsCommand.java b/main/src/com/google/refine/commands/row/GetRowsCommand.java index 6adbe52da7d50..6c0b6414a48bb 100644 --- a/main/src/com/google/refine/commands/row/GetRowsCommand.java +++ b/main/src/com/google/refine/commands/row/GetRowsCommand.java @@ -34,8 +34,7 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT package com.google.refine.commands.row; import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import javax.servlet.ServletException; @@ -63,54 +62,111 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import com.google.refine.sorting.SortingConfig; import com.google.refine.sorting.SortingRecordVisitor; import com.google.refine.sorting.SortingRowVisitor; -import com.google.refine.util.ParsingUtilities; import com.google.refine.util.Pool; +/** + * Retrieves rows from a project (or importing job). + * + * Those rows can be requested as either: - the batch of rows starting at a given index (included), up to a certain size + * - the batch of rows ending at a given index (excluded), again up to a given size. Filters (defined by facets) and the + * row/record mode toggle can also be provided. + */ public class GetRowsCommand extends Command { protected static class WrappedRow { @JsonUnwrapped protected final Row row; + + /** + * The logical row index (meaning the one from the original, unsorted grid) + */ @JsonProperty("i") protected final int rowIndex; @JsonProperty("j") @JsonInclude(Include.NON_NULL) protected final Integer recordIndex; - protected WrappedRow(Row rowOrRecord, int rowIndex, Integer recordIndex) { + /** + * The row index used for pagination (which can be different from the logical one if a temporary sort is + * applied) + */ + @JsonProperty("k") + protected final int paginationIndex; + + protected WrappedRow(Row rowOrRecord, int rowIndex, Integer recordIndex, int paginationIndex) { this.row = rowOrRecord; this.rowIndex = rowIndex; this.recordIndex = recordIndex; + this.paginationIndex = paginationIndex; } } protected static class JsonResult { + /** + * Mode of the engine (row or record based) + */ @JsonProperty("mode") protected final Mode mode; + /** + * Rows in the view + */ @JsonProperty("rows") protected final List rows; + /** + * Number of rows selected by the current filter + */ @JsonProperty("filtered") protected final int filtered; + /** + * Total number of rows/records in the unfiltered grid + */ @JsonProperty("total") protected final int totalCount; + /** + * Total number of rows in the unfiltered grid (needed to provide a link to the last page) + */ + @JsonProperty("totalRows") + protected final int totalRows; + @JsonProperty("start") - protected final int start; + @JsonInclude(Include.NON_NULL) + protected final Integer start; + @JsonProperty("end") + @JsonInclude(Include.NON_NULL) + protected final Integer end; @JsonProperty("limit") protected final int limit; @JsonProperty("pool") protected final Pool pool; + /** + * The value to use as 'end' when fetching the page before this one. Can be null if there is no such page. + */ + @JsonProperty("previousPageId") + @JsonInclude(Include.NON_NULL) + protected final Integer previousPageId; + /** + * The value to use as 'start' when fetching the page after this one. Can be null if there is no such page. + */ + @JsonProperty("nextPageId") + @JsonInclude(Include.NON_NULL) + protected final Integer nextPageId; + protected JsonResult(Mode mode, List rows, int filtered, - int totalCount, int start, int limit, Pool pool) { + int totalCount, int totalRows, int start, int end, int limit, Pool pool, Integer previousPageId, Integer nextPageId) { this.mode = mode; this.rows = rows; this.filtered = filtered; this.totalCount = totalCount; - this.start = start; + this.totalRows = totalRows; + this.start = start == -1 ? null : start; + this.end = end == -1 ? null : end; this.limit = limit; this.pool = pool; + this.previousPageId = previousPageId; + this.nextPageId = nextPageId; } } @@ -149,27 +205,19 @@ protected void internalRespond(HttpServletRequest request, HttpServletResponse r } Engine engine = getEngine(request, project); - String callback = request.getParameter("callback"); - int start = Math.min(project.rows.size(), Math.max(0, getIntegerParameter(request, "start", 0))); - int limit = Math.min(project.rows.size() - start, Math.max(0, getIntegerParameter(request, "limit", 20))); + int start = getIntegerParameter(request, "start", -1); + int end = getIntegerParameter(request, "end", -1); + int limit = Math.max(0, getIntegerParameter(request, "limit", 20)); - Pool pool = new Pool(); - /* - * Properties options = new Properties(); options.put("project", project); - * options.put("reconCandidateOmitTypes", true); options.put("pool", pool); - */ - - response.setCharacterEncoding("UTF-8"); - response.setHeader("Content-Type", callback == null ? "application/json" : "text/javascript"); - - PrintWriter writer = response.getWriter(); - if (callback != null) { - writer.write(callback); - writer.write("("); + if ((start == -1L) == (end == -1L)) { + respondException(response, new IllegalArgumentException("Exactly one of 'start' and 'end' should be provided.")); + return; } - RowWritingVisitor rwv = new RowWritingVisitor(start, limit); + Pool pool = new Pool(); + + RowWritingVisitor rwv = new RowWritingVisitor(start, end, limit); SortingConfig sortingConfig = null; try { @@ -178,6 +226,8 @@ protected void internalRespond(HttpServletRequest request, HttpServletResponse r sortingConfig = SortingConfig.reconstruct(sortingJson); } } catch (IOException e) { + respondException(response, e); + return; } if (engine.getMode() == Mode.RowBased) { @@ -217,15 +267,31 @@ protected void internalRespond(HttpServletRequest request, HttpServletResponse r } } + List wrappedRows = rwv.results; + + // Compute the indices of the previous and next pages + Integer previousPageId = null; + Integer nextPageId = null; + if (start != -1) { + if (start > 0) { + previousPageId = start; + } + if (!wrappedRows.isEmpty()) { + nextPageId = wrappedRows.get(wrappedRows.size() - 1).paginationIndex + 1; + } + } else { + if (!wrappedRows.isEmpty() && wrappedRows.get(0).paginationIndex > 0) { + previousPageId = wrappedRows.get(0).paginationIndex; + } + nextPageId = end; + } + JsonResult result = new JsonResult(engine.getMode(), rwv.results, rwv.total, engine.getMode() == Mode.RowBased ? project.rows.size() : project.recordModel.getRecordCount(), - start, limit, pool); + rwv.totalRows, start, end, limit, pool, previousPageId, nextPageId); - ParsingUtilities.defaultWriter.writeValue(writer, result); - if (callback != null) { - writer.write(")"); - } + respondJSON(response, result); } catch (Exception e) { respondException(response, e); } @@ -234,15 +300,18 @@ protected void internalRespond(HttpServletRequest request, HttpServletResponse r static protected class RowWritingVisitor implements RowVisitor, RecordVisitor { final int start; + final int end; final int limit; - public List results; + public LinkedList results; public int total; + public int totalRows; - public RowWritingVisitor(int start, int limit) { + public RowWritingVisitor(int start, int end, int limit) { this.start = start; + this.end = end; this.limit = limit; - this.results = new ArrayList<>(); + this.results = new LinkedList<>(); } @Override @@ -257,33 +326,57 @@ public void end(Project project) { @Override public boolean visit(Project project, int rowIndex, Row row) { - if (total >= start && total < start + limit) { - internalVisit(project, rowIndex, row); + return visit(project, rowIndex, rowIndex, row); + } + + @Override + public boolean visit(Project project, int rowIndex, int sortedRowIndex, Row row) { + if ((start != -1 && sortedRowIndex >= start && results.size() < limit) || + (end != -1 && sortedRowIndex < end)) { + if (results.size() >= limit) { + results.removeFirst(); + } + internalVisit(project, rowIndex, row, sortedRowIndex); } total++; + totalRows++; return false; } @Override public boolean visit(Project project, Record record) { - if (total >= start && total < start + limit) { - internalVisit(project, record); + return visit(project, record.fromRowIndex, record); + } + + @Override + public boolean visit(Project project, int sortedStartRowIndex, Record record) { + if ((start != -1 && sortedStartRowIndex >= start && results.size() < limit) || + (end != -1 && sortedStartRowIndex < end)) { + if (results.size() >= limit) { + results.removeFirst(); + while (results.size() > 0 && results.get(0).recordIndex == null) { + results.removeFirst(); + } + } + internalVisit(project, record, sortedStartRowIndex); } total++; + totalRows += record.toRowIndex - record.fromRowIndex; return false; } - public boolean internalVisit(Project project, int rowIndex, Row row) { - results.add(new WrappedRow(row, rowIndex, null)); + public boolean internalVisit(Project project, int rowIndex, Row row, int paginationIndex) { + results.add(new WrappedRow(row, rowIndex, null, paginationIndex)); return false; } - protected boolean internalVisit(Project project, Record record) { + protected boolean internalVisit(Project project, Record record, int sortedStartRowIndex) { for (int r = record.fromRowIndex; r < record.toRowIndex; r++) { Row row = project.rows.get(r); - results.add(new WrappedRow(row, r, r == record.fromRowIndex ? record.recordIndex : null)); + results.add(new WrappedRow(row, r, r == record.fromRowIndex ? record.recordIndex : null, + sortedStartRowIndex + r - record.fromRowIndex)); } return false; } diff --git a/main/src/com/google/refine/sorting/SortingRecordVisitor.java b/main/src/com/google/refine/sorting/SortingRecordVisitor.java index e875923c2f154..3f6f0d7f0053c 100644 --- a/main/src/com/google/refine/sorting/SortingRecordVisitor.java +++ b/main/src/com/google/refine/sorting/SortingRecordVisitor.java @@ -78,8 +78,10 @@ public int compare(Record o1, Record o2) { } }.init(project)); + int sortedIndex = 0; for (Record record : _records) { - _visitor.visit(project, record); + _visitor.visit(project, sortedIndex, record); + sortedIndex += record.toRowIndex - record.fromRowIndex; } _visitor.end(project); diff --git a/main/src/com/google/refine/sorting/SortingRowVisitor.java b/main/src/com/google/refine/sorting/SortingRowVisitor.java index cc2f97b78e564..3d2401cf2018e 100644 --- a/main/src/com/google/refine/sorting/SortingRowVisitor.java +++ b/main/src/com/google/refine/sorting/SortingRowVisitor.java @@ -89,8 +89,10 @@ public int compare(IndexedRow o1, IndexedRow o2) { } }.init(project)); + int sortedIndex = 0; for (IndexedRow indexedRow : _indexedRows) { - _visitor.visit(project, indexedRow.index, indexedRow.row); + _visitor.visit(project, indexedRow.index, sortedIndex, indexedRow.row); + sortedIndex++; } _visitor.end(project); diff --git a/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/pagination.cy.js b/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/pagination.cy.js index 4cec6d19229ac..cc22c0fbce177 100644 --- a/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/pagination.cy.js +++ b/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/pagination.cy.js @@ -27,7 +27,7 @@ describe(__filename, function () { it('Test the "next" button', function () { cy.loadAndVisitProject('food.small'); cy.get('.viewpanel-paging').find('a').contains('next').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 2); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 11); cy.assertCellEquals(0, 'Shrt_Desc', 'CHEESE,COLBY'); cy.assertCellEquals(9, 'Shrt_Desc', 'CHEESE,FONTINA'); }); @@ -37,11 +37,11 @@ describe(__filename, function () { // First go next cy.get('.viewpanel-paging').find('a').contains('next').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 2); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 11); // Then test the previous button cy.get('.viewpanel-paging').find('a').contains('previous').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 1); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 1); cy.assertCellEquals(0, 'Shrt_Desc', 'BUTTER,WITH SALT'); cy.assertCellEquals(9, 'Shrt_Desc', 'CHEESE,CHESHIRE'); }); @@ -50,9 +50,9 @@ describe(__filename, function () { cy.loadAndVisitProject('food.small'); cy.get('.viewpanel-paging').find('a').contains('last').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 20); - cy.assertCellEquals(0, 'Shrt_Desc', 'SPICES,BASIL,DRIED'); - cy.assertCellEquals(8, 'Shrt_Desc', 'CLOVES,GROUND'); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 190); + cy.assertCellEquals(0, 'Shrt_Desc', 'ANISE SEED'); + cy.assertCellEquals(9, 'Shrt_Desc', 'CLOVES,GROUND'); }); it('Test the "first" button', function () { @@ -60,11 +60,11 @@ describe(__filename, function () { // First go next cy.get('.viewpanel-paging').find('a').contains('next').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 2); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 11); - // Then test the previous button + // Then test the First button cy.get('.viewpanel-paging').find('a').contains('first').click(); - cy.get('#viewpanel-paging-current-input').should('have.value', 1); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 1); cy.assertCellEquals(0, 'Shrt_Desc', 'BUTTER,WITH SALT'); cy.assertCellEquals(9, 'Shrt_Desc', 'CHEESE,CHESHIRE'); }); @@ -72,8 +72,8 @@ describe(__filename, function () { it('Test entering an arbitrary page number', function () { cy.loadAndVisitProject('food.small'); - cy.get('#viewpanel-paging-current-input').type('{backspace}2{enter}'); - cy.get('#viewpanel-paging-current-input').should('have.value', 2); + cy.get('#viewpanel-paging-current-min-row').type('{backspace}11{enter}'); + cy.get('#viewpanel-paging-current-min-row').should('have.value', 11); cy.assertCellEquals(0, 'Shrt_Desc', 'CHEESE,COLBY'); cy.assertCellEquals(9, 'Shrt_Desc', 'CHEESE,FONTINA'); }); diff --git a/main/tests/server/src/com/google/refine/browsing/util/ExpressionNominalValueGrouperTests.java b/main/tests/server/src/com/google/refine/browsing/util/ExpressionNominalValueGrouperTests.java index 334b75352bcd3..0442ae2d5611b 100644 --- a/main/tests/server/src/com/google/refine/browsing/util/ExpressionNominalValueGrouperTests.java +++ b/main/tests/server/src/com/google/refine/browsing/util/ExpressionNominalValueGrouperTests.java @@ -49,6 +49,7 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import com.google.refine.model.Cell; import com.google.refine.model.ModelException; import com.google.refine.model.Project; +import com.google.refine.model.Record; import com.google.refine.model.Row; public class ExpressionNominalValueGrouperTests extends RefineTest { @@ -105,7 +106,7 @@ public void expressionNominalValueGrouperStrings() throws Exception { grouper.start(project); for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++) { Row row = project.rows.get(rowIndex); - grouper.visit(project, rowIndex, row); + grouper.visit(project, rowIndex, rowIndex, row); } } finally { grouper.end(project); @@ -133,7 +134,7 @@ public void expressionNominalValueGrouperInts() throws Exception { grouper.start(project); for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++) { Row row = project.rows.get(rowIndex); - grouper.visit(project, rowIndex, row); + grouper.visit(project, rowIndex, rowIndex, row); } } finally { grouper.end(project); @@ -161,7 +162,7 @@ public void expressionNominalValueGrouperDates() throws Exception { grouper.start(project); for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++) { Row row = project.rows.get(rowIndex); - grouper.visit(project, rowIndex, row); + grouper.visit(project, rowIndex, rowIndex, row); } } finally { grouper.end(project); @@ -194,7 +195,8 @@ public void expressionNominalValueGrouperRecords() throws Exception { grouper.start(project); int c = project.recordModel.getRecordCount(); for (int r = 0; r < c; r++) { - grouper.visit(project, project.recordModel.getRecord(r)); + Record record = project.recordModel.getRecord(r); + grouper.visit(project, record.fromRowIndex, record); } } finally { grouper.end(project); diff --git a/main/tests/server/src/com/google/refine/commands/row/GetRowsCommandTest.java b/main/tests/server/src/com/google/refine/commands/row/GetRowsCommandTest.java index 7972414600d74..e8db73e902dc5 100644 --- a/main/tests/server/src/com/google/refine/commands/row/GetRowsCommandTest.java +++ b/main/tests/server/src/com/google/refine/commands/row/GetRowsCommandTest.java @@ -54,18 +54,32 @@ public class GetRowsCommandTest extends RefineTest { Command command = null; Project project = null; StringWriter writer = null; + String sortingConfigJson = null; @BeforeMethod - public void setUp() { + public void setUp() throws IOException { request = mock(HttpServletRequest.class); response = mock(HttpServletResponse.class); - project = createProject(new String[] { "a", "b" }, + project = createProject(new String[] { "foo", "bar" }, new Serializable[][] { - { "c", "d" }, - { null, "f" } + { "a", "b" }, + { null, "c" }, + { "d", "e" }, + { "", "f" }, + { "g", "h" }, }); command = new GetRowsCommand(); writer = new StringWriter(); + sortingConfigJson = "{" + + "\"criteria\":[" + + " {" + + " \"column\":\"bar\"," + + " \"valueType\":\"string\"," + + " \"reverse\":true," + + " \"blankPosition\":2," + + " \"errorPosition\":1," + + " \"caseSensitive\":false" + + "}]}"; when(request.getParameter("project")).thenReturn(String.valueOf(project.id)); try { when(response.getWriter()).thenReturn(new PrintWriter(writer)); @@ -75,9 +89,9 @@ public void setUp() { } @Test - public void testJsonOutputRows() throws ServletException, IOException { + public void testJsonOutputRowsStart() throws ServletException, IOException { String rowJson = "{\n" + - " \"filtered\" : 2,\n" + + " \"filtered\" : 5,\n" + " \"limit\" : 2,\n" + " \"mode\" : \"row-based\",\n" + " \"pool\" : {\n" + @@ -85,26 +99,190 @@ public void testJsonOutputRows() throws ServletException, IOException { " },\n" + " \"rows\" : [ {\n" + " \"cells\" : [ {\n" + + " \"v\" : \"a\"\n" + + " }, {\n" + + " \"v\" : \"b\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 0,\n" + + " \"k\" : 0,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ null, {\n" + " \"v\" : \"c\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 1,\n" + + " \"k\" : 1,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"start\" : 0,\n" + + " \"nextPageId\" : 2,\n" + + " \"total\" : 5,\n" + + " \"totalRows\" : 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"row-based\",\"facets\":[]}"); + when(request.getParameter("start")).thenReturn("0"); + when(request.getParameter("limit")).thenReturn("2"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), rowJson); + } + + @Test + public void testJsonOutputRowsStartWithNoNextPage() throws ServletException, IOException { + String rowJson = "{\n" + + " \"filtered\" : 5,\n" + + " \"limit\" : 2,\n" + + " \"mode\" : \"row-based\",\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"a\"\n" + " }, {\n" + - " \"v\" : \"d\"\n" + + " \"v\" : \"b\"\n" + " } ],\n" + " \"flagged\" : false,\n" + " \"i\" : 0,\n" + + " \"k\" : 0,\n" + " \"starred\" : false\n" + " }, {\n" + " \"cells\" : [ null, {\n" + - " \"v\" : \"f\"\n" + + " \"v\" : \"c\"\n" + " } ],\n" + " \"flagged\" : false,\n" + " \"i\" : 1,\n" + + " \"k\" : 1,\n" + " \"starred\" : false\n" + " } ],\n" + " \"start\" : 0,\n" + - " \"total\" : 2\n" + + " \"nextPageId\": 2,\n" + + " \"total\" : 5,\n" + + " \"totalRows\" : 5\n" + " }"; when(request.getParameter("engine")).thenReturn("{\"mode\":\"row-based\",\"facets\":[]}"); + when(request.getParameter("start")).thenReturn("0"); + when(request.getParameter("limit")).thenReturn("2"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), rowJson); + } + + @Test + public void testJsonOutputRowsEnd() throws ServletException, IOException { + String rowJson = "{\n" + + " \"filtered\" : 5,\n" + + " \"limit\" : 1,\n" + + " \"mode\" : \"row-based\",\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ null, {\n" + + " \"v\" : \"c\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 1,\n" + + " \"k\" : 1,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"end\" : 2,\n" + + " \"previousPageId\": 1,\n" + + " \"nextPageId\": 2,\n" + + " \"total\" : 5,\n" + + " \"totalRows\" : 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"row-based\",\"facets\":[]}"); + when(request.getParameter("end")).thenReturn("2"); + when(request.getParameter("limit")).thenReturn("1"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), rowJson); + } + + @Test + public void testJsonOutputRowsEndWithNoPreviousPage() throws ServletException, IOException { + String rowJson = "{\n" + + " \"filtered\" : 5,\n" + + " \"limit\" : 3,\n" + + " \"mode\" : \"row-based\",\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {" + + " \"cells\": [ {\n" + + " \"v\" : \"a\"\n" + + " }, {\n" + + " \"v\" : \"b\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 0,\n" + + " \"k\" : 0,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ null, {\n" + + " \"v\" : \"c\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 1,\n" + + " \"k\" : 1,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"end\" : 2,\n" + + " \"nextPageId\": 2,\n" + + " \"total\" : 5,\n" + + " \"totalRows\" : 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"row-based\",\"facets\":[]}"); + when(request.getParameter("end")).thenReturn("2"); + when(request.getParameter("limit")).thenReturn("3"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), rowJson); + } + + @Test + public void testJsonOutputRowsSorted() throws ServletException, IOException { + String rowJson = "{\n" + + " \"end\" : 2,\n" + + " \"filtered\" : 5,\n" + + " \"limit\" : 3,\n" + + " \"mode\" : \"row-based\",\n" + + " \"nextPageId\" : 2,\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"g\"\n" + + " }, {\n" + + " \"v\" : \"h\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 4,\n" + + " \"k\" : 0,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"\"\n" + + " }, {\n" + + " \"v\" : \"f\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 3,\n" + + " \"k\" : 1,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"total\" : 5,\n" + + " \"totalRows\" : 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"row-based\",\"facets\":[]}"); + when(request.getParameter("end")).thenReturn("2"); + when(request.getParameter("limit")).thenReturn("3"); + when(request.getParameter("sorting")).thenReturn(sortingConfigJson); command.doPost(request, response); TestUtils.assertEqualsAsJson(writer.toString(), rowJson); } @@ -112,35 +290,177 @@ public void testJsonOutputRows() throws ServletException, IOException { @Test public void testJsonOutputRecords() throws ServletException, IOException { String recordJson = "{\n" + - " \"filtered\" : 1,\n" + - " \"limit\" : 2,\n" + + " \"filtered\" : 3,\n" + + " \"limit\" : 1,\n" + " \"mode\" : \"record-based\",\n" + " \"pool\" : {\n" + " \"recons\" : { }\n" + " },\n" + " \"rows\" : [ {\n" + " \"cells\" : [ {\n" + - " \"v\" : \"c\"\n" + + " \"v\" : \"a\"\n" + " }, {\n" + - " \"v\" : \"d\"\n" + + " \"v\" : \"b\"\n" + " } ],\n" + " \"flagged\" : false,\n" + " \"i\" : 0,\n" + " \"j\" : 0,\n" + + " \"k\" : 0,\n" + " \"starred\" : false\n" + " }, {\n" + " \"cells\" : [ null, {\n" + - " \"v\" : \"f\"\n" + + " \"v\" : \"c\"\n" + " } ],\n" + " \"flagged\" : false,\n" + " \"i\" : 1,\n" + + " \"k\" : 1,\n" + " \"starred\" : false\n" + " } ],\n" + " \"start\" : 0,\n" + - " \"total\" : 1\n" + + " \"nextPageId\" : 2,\n" + + " \"total\" : 3,\n" + + " \"totalRows\": 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"record-based\",\"facets\":[]}"); + when(request.getParameter("start")).thenReturn("0"); + when(request.getParameter("limit")).thenReturn("1"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), recordJson); + } + + @Test + public void testJsonOutputRecordStartFromOffset() throws ServletException, IOException { + String recordJson = "{\n" + + " \"filtered\" : 3,\n" + + " \"limit\" : 1,\n" + + " \"mode\" : \"record-based\",\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"d\"\n" + + " }, {\n" + + " \"v\" : \"e\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 2,\n" + + " \"j\" : 1,\n" + + " \"k\" : 2,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"\"\n" + + " }, {\n" + + " \"v\" : \"f\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 3,\n" + + " \"k\" : 3,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"start\" : 1,\n" + + " \"previousPageId\" : 1,\n" + + " \"nextPageId\" : 4,\n" + + " \"total\" : 3,\n" + + " \"totalRows\": 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"record-based\",\"facets\":[]}"); + when(request.getParameter("start")).thenReturn("1"); + when(request.getParameter("limit")).thenReturn("1"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), recordJson); + } + + @Test + public void testJsonOutputRecordEndToOffset() throws ServletException, IOException { + String recordJson = "{\n" + + " \"filtered\" : 3,\n" + + " \"limit\" : 1,\n" + + " \"mode\" : \"record-based\",\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"d\"\n" + + " }, {\n" + + " \"v\" : \"e\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 2,\n" + + " \"j\" : 1,\n" + + " \"k\" : 2,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"\"\n" + + " }, {\n" + + " \"v\" : \"f\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 3,\n" + + " \"k\" : 3,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"end\" : 4,\n" + + " \"previousPageId\" : 2,\n" + + " \"nextPageId\" : 4,\n" + + " \"total\" : 3,\n" + + " \"totalRows\": 5\n" + " }"; when(request.getParameter("engine")).thenReturn("{\"mode\":\"record-based\",\"facets\":[]}"); + when(request.getParameter("end")).thenReturn("4"); + when(request.getParameter("limit")).thenReturn("1"); + command.doPost(request, response); + TestUtils.assertEqualsAsJson(writer.toString(), recordJson); + } + + @Test + public void testJsonOutputRecordWithSorting() throws ServletException, IOException { + String recordJson = "{\n" + + " \"filtered\" : 3,\n" + + " \"limit\" : 1,\n" + + " \"mode\" : \"record-based\",\n" + + " \"nextPageId\" : 3,\n" + + " \"pool\" : {\n" + + " \"recons\" : { }\n" + + " },\n" + + " \"previousPageId\" : 1,\n" + + " \"rows\" : [ {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"d\"\n" + + " }, {\n" + + " \"v\" : \"e\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 2,\n" + + " \"j\" : 1,\n" + + " \"k\" : 1,\n" + + " \"starred\" : false\n" + + " }, {\n" + + " \"cells\" : [ {\n" + + " \"v\" : \"\"\n" + + " }, {\n" + + " \"v\" : \"f\"\n" + + " } ],\n" + + " \"flagged\" : false,\n" + + " \"i\" : 3,\n" + + " \"k\" : 2,\n" + + " \"starred\" : false\n" + + " } ],\n" + + " \"start\" : 1,\n" + + " \"total\" : 3,\n" + + " \"totalRows\" : 5\n" + + " }"; + + when(request.getParameter("engine")).thenReturn("{\"mode\":\"record-based\",\"facets\":[]}"); + when(request.getParameter("start")).thenReturn("1"); + when(request.getParameter("limit")).thenReturn("1"); + when(request.getParameter("sorting")).thenReturn(sortingConfigJson); command.doPost(request, response); TestUtils.assertEqualsAsJson(writer.toString(), recordJson); } diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json index c636782c89bf9..fb0ae28d20356 100644 --- a/main/webapp/modules/core/langs/translation-en.json +++ b/main/webapp/modules/core/langs/translation-en.json @@ -654,7 +654,6 @@ "core-views/move-to-left": "Move column left", "core-views/move-to-right": "Move column right", "core-views/show-as": "Show as", - "core-views/goto-page": "$1 of $2 {{plural:$2|page|pages}}", "core-views/first": "first", "core-views/previous": "previous", "core-views/next": "next", diff --git a/main/webapp/modules/core/langs/translation-es.json b/main/webapp/modules/core/langs/translation-es.json index 2a404e873d6ba..43d81ba6b6d50 100644 --- a/main/webapp/modules/core/langs/translation-es.json +++ b/main/webapp/modules/core/langs/translation-es.json @@ -517,7 +517,6 @@ "core-views/to-text/header": "A texto", "core-views/to-text/single": "A texto", "core-views/reg-exp": "expresión regular", - "core-views/goto-page": "$1 de $2 {{plural:$2|página|paginas}}", "core-views/word-facet": "Faceta por palabra", "core-views/collapse-left": "Contraer todas las columnas a la izquierda", "core-views/clear-recon": "Quitar la información de cotejo", diff --git a/main/webapp/modules/core/langs/translation-fr.json b/main/webapp/modules/core/langs/translation-fr.json index 66fdf0c8906ae..687004a7b6122 100644 --- a/main/webapp/modules/core/langs/translation-fr.json +++ b/main/webapp/modules/core/langs/translation-fr.json @@ -529,7 +529,6 @@ "core-views/to-text": "En texte…", "core-views/to-text/header": "En texte", "core-views/to-text/single": "En texte", - "core-views/goto-page": "$1 de $2 {{plural:$2|page|pages}}", "core-views/first": "première", "core-views/word-facet": "Facette par mot", "core-views/check-format": "Merci de vérifier le format du fichier.", diff --git a/main/webapp/modules/core/scripts/dialogs/clustering-dialog.js b/main/webapp/modules/core/scripts/dialogs/clustering-dialog.js index 8d4a8ed70a47a..8b497cc00760e 100644 --- a/main/webapp/modules/core/scripts/dialogs/clustering-dialog.js +++ b/main/webapp/modules/core/scripts/dialogs/clustering-dialog.js @@ -467,7 +467,7 @@ ClusteringDialog.prototype._apply = function(onDone) { expression: this._expression, edits: JSON.stringify(edits) }, - { cellsChanged: true }, + { cellsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }, { onError: function(o) { alert("Error: " + o.message); diff --git a/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js b/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js index a0a03de59e4de..83310818b20fa 100644 --- a/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js +++ b/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js @@ -92,7 +92,7 @@ ColumnReorderingDialog.prototype._commit = function() { "reorder-columns", null, { "columnNames" : JSON.stringify(columnNames) }, - { modelsChanged: true }, + { modelsChanged: true, rowIdsPreserved: true }, // TODO could add recordIdsPreserved: true if the record key column did not change { includeEngine: false } ); diff --git a/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js b/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js index 27d88ae4261d5..1632078c78933 100644 --- a/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js +++ b/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js @@ -117,7 +117,7 @@ commonTransformDialog.prototype._commit = function(expression) { repeatCount: repeatCount }, null, - { cellsChanged: true } + { cellsChanged: true, rowIdsPreserved: true } ); }; diff --git a/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js b/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js index d6cd053c50c88..ca2334e71233a 100644 --- a/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js +++ b/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js @@ -10,7 +10,7 @@ var doTextTransform = function(columnName, expression, onError, repeat, repeatCo repeatCount: repeatCount }, null, - { cellsChanged: true } + { cellsChanged: true, rowIdsPreserved: true } ); }; diff --git a/main/webapp/modules/core/scripts/index/default-importing-controller/controller.js b/main/webapp/modules/core/scripts/index/default-importing-controller/controller.js index 8d8abd6129cb1..b8ea5f0cad557 100644 --- a/main/webapp/modules/core/scripts/index/default-importing-controller/controller.js +++ b/main/webapp/modules/core/scripts/index/default-importing-controller/controller.js @@ -287,7 +287,7 @@ Refine.DefaultImportingController.prototype.getPreviewData = function(callback, result.rowModel = data; callback(result); }, - "jsonp" + "json" ).fail(() => { DialogSystem.alert($.i18n('core-index/rows-loading-failed')); }); diff --git a/main/webapp/modules/core/scripts/project.js b/main/webapp/modules/core/scripts/project.js index 7a0ca5be84baf..d7474e1a6b4f4 100644 --- a/main/webapp/modules/core/scripts/project.js +++ b/main/webapp/modules/core/scripts/project.js @@ -304,8 +304,9 @@ Refine.createUpdateFunction = function(options, onFinallyDone) { pushFunction(Refine.reinitializeProjectData); } if (options.everythingChanged || options.modelsChanged || options.rowsChanged || options.rowMetadataChanged || options.cellsChanged || options.engineChanged) { + var preservePage = options.rowIdsPreserved && (options.recordIdsPreserved || ui.browsingEngine.getMode() === "row-based"); pushFunction(function(onDone) { - ui.dataTableView.update(onDone); + ui.dataTableView.update(onDone, preservePage); }); pushFunction(function(onDone) { ui.browsingEngine.update(onDone); @@ -495,7 +496,11 @@ Refine.columnNameToColumnIndex = function(columnName) { return -1; }; -Refine.fetchRows = function(start, limit, onDone, sorting) { +/* + Fetch rows after or before a given row id. The engine configuration can also + be used to set filters (facets) or switch between rows/records mode. +*/ +Refine.fetchRows = function(paginationOptions, limit, onDone, sorting) { var body = { engine: JSON.stringify(ui.browsingEngine.getJSON()) }; @@ -504,7 +509,7 @@ Refine.fetchRows = function(start, limit, onDone, sorting) { } $.post( - "command/core/get-rows?" + $.param({ project: theProject.id, start: start, limit: limit }), + "command/core/get-rows?" + $.param({ ...paginationOptions, project: theProject.id, limit: limit }), body, function(data) { if(data.code === "error") { diff --git a/main/webapp/modules/core/scripts/reconciliation/standard-service-panel.js b/main/webapp/modules/core/scripts/reconciliation/standard-service-panel.js index 60aab14972f17..e6514b703fe09 100644 --- a/main/webapp/modules/core/scripts/reconciliation/standard-service-panel.js +++ b/main/webapp/modules/core/scripts/reconciliation/standard-service-panel.js @@ -401,7 +401,7 @@ ReconStandardServicePanel.prototype.start = function() { limit: parseInt(this._elmts.maxCandidates[0].value) || 0 }) }, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); return true; diff --git a/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js b/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js index ac9d4e3922372..fe49301025738 100644 --- a/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js +++ b/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js @@ -433,7 +433,7 @@ class ReconCellRenderer { command, params, bodyParams, - { cellsChanged: true, columnStatsChanged: columnStatsChanged } + { cellsChanged: true, columnStatsChanged: columnStatsChanged, rowIdsPreserved: true, recordIdsPreserved: true } ); } diff --git a/main/webapp/modules/core/scripts/views/data-table/data-table-view.js b/main/webapp/modules/core/scripts/views/data-table/data-table-view.js index 6a52908bda59d..de260415ff5f5 100644 --- a/main/webapp/modules/core/scripts/views/data-table/data-table-view.js +++ b/main/webapp/modules/core/scripts/views/data-table/data-table-view.js @@ -44,10 +44,7 @@ function DataTableView(div) { this._columnHeaderUIs = []; this._shownulls = false; - this._currentPageNumber = 1; - this._showRows(0); - - this._refocusPageInput = false; + this._showRows({start: 0}); } DataTableView._extenders = []; @@ -84,9 +81,16 @@ DataTableView.prototype.resize = function() { tableContainer.height((tableContainerIntendedHeight - tableContainerVPadding) + "px"); }; -DataTableView.prototype.update = function(onDone) { - this._currentPageNumber = 1; - this._showRows(0, onDone); +DataTableView.prototype.update = function(onDone, preservePage) { + var paginationOptions = { start: 0 }; + if (preservePage) { + if (theProject.rowModel.start !== undefined) { + paginationOptions.start = theProject.rowModel.start; + } else { + paginationOptions.end = theProject.rowModel.end; + } + } + this._showRows(paginationOptions, onDone); }; DataTableView.prototype.render = function() { @@ -114,7 +118,6 @@ DataTableView.prototype.render = function() { '' ); var elmts = DOM.bind(html); - this._div.empty().append(html); ui.summaryBar.updateResultCount(); @@ -142,6 +145,7 @@ DataTableView.prototype.render = function() { } this._renderDataTables(elmts.table[0], elmts.tableHeader[0]); + this._div.empty().append(html); // show/hide null values in cells $(".data-table-null").toggle(self._shownulls); @@ -167,14 +171,18 @@ DataTableView.prototype._renderSortingControls = function(sortingControls) { DataTableView.prototype._renderPagingControls = function(pageSizeControls, pagingControls) { var self = this; - self._lastPageNumber = Math.floor((theProject.rowModel.filtered - 1) / this._pageSize) + 1; - - var from = (theProject.rowModel.start + 1); - var to = Math.min(theProject.rowModel.filtered, theProject.rowModel.start + theProject.rowModel.limit); + var rowIds = theProject.rowModel.rows.map(row => row.k); + if (theProject.rowModel.start !== undefined) { + rowIds.push(theProject.rowModel.start); + } else { + rowIds.push(theProject.rowModel.end); + } + var minRowId = Math.min(... rowIds); + var maxRowId = Math.max(... rowIds); var firstPage = $('« '+$.i18n('core-views/first')+'').appendTo(pagingControls); var previousPage = $('‹ '+$.i18n('core-views/previous')+'').appendTo(pagingControls); - if (theProject.rowModel.start > 0) { + if (theProject.rowModel.previousPageId !== undefined) { firstPage.addClass("action").on('click',function(evt) { self._onClickFirstPage(this, evt); }); previousPage.addClass("action").on('click',function(evt) { self._onClickPreviousPage(this, evt); }); } else { @@ -182,33 +190,20 @@ DataTableView.prototype._renderPagingControls = function(pageSizeControls, pagin previousPage.addClass("inaction"); } - var pageControlsSpan = $('').attr("id", "viewpanel-paging-current"); - - var pageInputSize = 20 + (8 * ui.dataTableView._lastPageNumber.toString().length); - var currentPageInput = $('') - .on('change',function(evt) { self._onChangeGotoPage(this, evt); }) - .on('keydown',function(evt) { self._onKeyDownGotoPage(this, evt); }) - .attr("id", "viewpanel-paging-current-input") + var minRowInputSize = 20 + (8* theProject.rowModel.total.toString().length); + var minRowInput = $('') + .attr("id", "viewpanel-paging-current-min-row") .attr("min", 1) - .attr("max", self._lastPageNumber) - .attr("required", "required") - .val(self._currentPageNumber) - .css("width", pageInputSize +"px"); - - pageControlsSpan.append($.i18n('core-views/goto-page', '', self._lastPageNumber)); - pageControlsSpan.appendTo(pagingControls); + .attr("max", theProject.rowModel.total) + .css("width", minRowInputSize) + .val(minRowId + 1) + .on('change', function(evt) { self._onChangeMinRow(this, evt); }) + .appendTo(pagingControls); + $('').addClass("viewpanel-pagingcount").html(" - " + (maxRowId + 1) + " ").appendTo(pagingControls); - $('span#currentPageInput').replaceWith($(currentPageInput)); - - if(self._refocusPageInput == true) { - self._refocusPageInput = false; - var currentPageInputForFocus = $('input#viewpanel-paging-current-input'); - currentPageInputForFocus.ready(function(evt) { setTimeout(() => { currentPageInputForFocus.focus(); }, 250); }); - } - var nextPage = $(''+$.i18n('core-views/next')+' ›').appendTo(pagingControls); var lastPage = $(''+$.i18n('core-views/last')+' »').appendTo(pagingControls); - if (theProject.rowModel.start + theProject.rowModel.limit < theProject.rowModel.filtered) { + if (theProject.rowModel.nextPageId) { nextPage.addClass("action").on('click',function(evt) { self._onClickNextPage(this, evt); }); lastPage.addClass("action").on('click',function(evt) { self._onClickLastPage(this, evt); }); } else { @@ -228,7 +223,7 @@ DataTableView.prototype._renderPagingControls = function(pageSizeControls, pagin } else { a.text(pageSize).addClass("action").on('click',function(evt) { self._pageSize = pageSize; - self.update(); + self.update(undefined, true); }); } }; @@ -491,9 +486,9 @@ DataTableView.prototype._renderDataTables = function(table, tableHeader) { } }; -DataTableView.prototype._showRows = function(start, onDone) { +DataTableView.prototype._showRows = function(paginationOptions, onDone) { var self = this; - Refine.fetchRows(start, this._pageSize, function() { + Refine.fetchRows(paginationOptions, this._pageSize, function() { self.render(); if (onDone) { @@ -502,60 +497,33 @@ DataTableView.prototype._showRows = function(start, onDone) { }, this._sorting); }; -DataTableView.prototype._onChangeGotoPage = function(elmt, evt) { - var gotoPageNumber = parseInt($('input#viewpanel-paging-current-input').val()); - - if(typeof gotoPageNumber != "number" || isNaN(gotoPageNumber) || gotoPageNumber == "") { - $('input#viewpanel-paging-current-input').val(this._currentPageNumber); - return; - } - - if(gotoPageNumber > this._lastPageNumber) gotoPageNumber = this._lastPageNumber; - if(gotoPageNumber < 1) gotoPageNumber = 1; - - this._currentPageNumber = gotoPageNumber; - this._showRows((gotoPageNumber - 1) * this._pageSize); -}; - -DataTableView.prototype._onKeyDownGotoPage = function(elmt, evt) { - var keyDownCode = evt.key; - - if(['ArrowUp', 'ArrowDown'].indexOf(keyDownCode) == -1) return; - if(self._refocusPageInput == true) return; - - evt.preventDefault(); - this._refocusPageInput = true; - - var newPageValue = $('input#viewpanel-paging-current-input')[0].value; - if(keyDownCode == 'ArrowUp') { - if(newPageValue <= 1) return; - this._onClickPreviousPage(elmt, evt); - } - - if(keyDownCode == 'ArrowDown') { - if(newPageValue >= this._lastPageNumber) return; - this._onClickNextPage(elmt, evt); - } -}; - DataTableView.prototype._onClickPreviousPage = function(elmt, evt) { - this._currentPageNumber--; - this._showRows(theProject.rowModel.start - this._pageSize); + this._showRows({end: theProject.rowModel.previousPageId}); }; DataTableView.prototype._onClickNextPage = function(elmt, evt) { - this._currentPageNumber++; - this._showRows(theProject.rowModel.start + this._pageSize); + this._showRows({start: theProject.rowModel.nextPageId}); }; DataTableView.prototype._onClickFirstPage = function(elmt, evt) { - this._currentPageNumber = 1; - this._showRows(0); + this._showRows({start: 0}); }; DataTableView.prototype._onClickLastPage = function(elmt, evt) { - this._currentPageNumber = this._lastPageNumber; - this._showRows((this._lastPageNumber - 1) * this._pageSize); + this._showRows({end: theProject.rowModel.totalRows}); +}; + +DataTableView.prototype._onChangeMinRow = function(elmt, evt) { + var input = $('#viewpanel-paging-current-min-row'); + var newMinRow = input.val(); + if (newMinRow <= 0) { + newMinRow = 1; + input.val(newMinRow); + } else if (newMinRow > theProject.rowModel.total) { + newMinRow = theProject.rowModel.total; + input.val(theProject.rowModel.total); + } + this._showRows({start: newMinRow - 1}); }; DataTableView.prototype._getSortingCriteriaCount = function() { @@ -868,14 +836,14 @@ DataTableView.prototype._createMenuForAllColumns = function(elmt) { label: $.i18n('core-views/star-rows'), id: "core/star-rows", click: function() { - Refine.postCoreProcess("annotate-rows", { "starred" : "true" }, null, { rowMetadataChanged: true }); + Refine.postCoreProcess("annotate-rows", { "starred" : "true" }, null, { rowMetadataChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }); } }, { label: $.i18n('core-views/unstar-rows'), id: "core/unstar-rows", click: function() { - Refine.postCoreProcess("annotate-rows", { "starred" : "false" }, null, { rowMetadataChanged: true }); + Refine.postCoreProcess("annotate-rows", { "starred" : "false" }, null, { rowMetadataChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }); } }, {}, @@ -883,14 +851,14 @@ DataTableView.prototype._createMenuForAllColumns = function(elmt) { label: $.i18n('core-views/flag-rows'), id: "core/flag-rows", click: function() { - Refine.postCoreProcess("annotate-rows", { "flagged" : "true" }, null, { rowMetadataChanged: true }); + Refine.postCoreProcess("annotate-rows", { "flagged" : "true" }, null, { rowMetadataChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }); } }, { label: $.i18n('core-views/unflag-rows'), id: "core/unflag-rows", click: function() { - Refine.postCoreProcess("annotate-rows", { "flagged" : "false" }, null, { rowMetadataChanged: true }); + Refine.postCoreProcess("annotate-rows", { "flagged" : "false" }, null, { rowMetadataChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }); } }, {}, diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js index c422cf013f2cc..8a9ede723f10d 100644 --- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js +++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js @@ -101,7 +101,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { columnName: column.name }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true } ); }; @@ -112,7 +112,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { columnName: column.name }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true } ); }; 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 aeb10a81c6c9d..9c9dec8fa7f10 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 @@ -44,7 +44,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { repeatCount: repeatCount }, { expression: expression }, - { cellsChanged: true }, + { cellsChanged: true, rowIdsPreserved: true }, callbacks ); }; @@ -95,7 +95,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { onError: $('input[name="create-column-dialog-onerror-choice"]:checked')[0].value }, { expression: previewWidget.getExpression(true) }, - { modelsChanged: true }, + { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }, { onDone: function(o) { dismiss(); @@ -166,7 +166,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { httpHeaders: JSON.stringify(elmts.setHttpHeadersContainer.find("input").serializeArray()) }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); dismiss(); }); @@ -206,7 +206,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { columnName: column.name }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true } ); }; @@ -240,7 +240,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { newColumnName: newColumnName }, null, - {modelsChanged: true}, + {modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true}, { onDone: function () { dismiss(); @@ -261,7 +261,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { index: index }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true } ); }; @@ -275,7 +275,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { index: newidx }, null, - { modelsChanged: true } + { modelsChanged: true, rowIdsPreserved: true } ); } }; @@ -412,7 +412,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { "reorder-columns", null, { "columnNames" : JSON.stringify(columnsToKeep) }, - { modelsChanged: true }, + { modelsChanged: true, rowIdsPreserved: true }, { includeEngine: false } ); } @@ -465,7 +465,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { onError: onError }, { expression: expression }, - { modelsChanged: true }, + { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }, { onFinallyDone: deleteColumns} ); } diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js index ab0e794c5faa6..54f649774b492 100644 --- a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js +++ b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js @@ -50,7 +50,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { "recon-discard-judgments", { columnName: column.name, clearData: false }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); }; @@ -59,7 +59,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { "recon-discard-judgments", { columnName: column.name, clearData: true }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); }; @@ -138,7 +138,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { "recon-match-best-candidates", { columnName: column.name }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); }; @@ -156,7 +156,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { schemaSpace: schemaSpace }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); }; @@ -221,7 +221,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { schemaSpace: service.schemaSpace }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); DialogSystem.dismissUntil(level - 1); @@ -309,7 +309,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { schemaSpace: schemaSpace }, null, - { cellsChanged: true, columnStatsChanged: true } + { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); }; @@ -328,7 +328,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { onError: "set-to-blank" }, { expression: "cell.recon.match.id" }, - { modelsChanged: true }, + { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }, { onDone: dismissDialog }, ); } @@ -337,7 +337,6 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { promptForColumn(successCallBackForAddingIdColumn,'core-views/add-id-col'); - }; @@ -396,7 +395,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { "recon-copy-across-columns", null, config, - { rowsChanged: true } + { rowsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true } ); dismiss(); }