diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 70dec0980a..4477a26e31 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -112,10 +112,11 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest final BrowseCommandRequest other = (BrowseCommandRequest) obj; - return super.equals(obj) && Objects.equal(recursive, other.recursive) + return super.equals(obj) + && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) - && Objects.equal(disableSubRepositoryDetection, - other.disableSubRepositoryDetection); + && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) + && Objects.equal(offset, other.offset); } /** @@ -128,7 +129,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection); + disableSubRepositoryDetection, offset); } /** @@ -147,6 +148,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest .add("recursive", recursive) .add("disableLastCommit", disableLastCommit) .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .add("offset", offset) .toString(); //J+ } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 99e3bde7ac..b1cca3c59f 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -16,9 +16,9 @@ export type File = { length?: number; commitDate?: string; subRepository?: SubRepository; // TODO - partialResult: boolean; - computationAborted: boolean; - truncated: boolean; + partialResult?: boolean; + computationAborted?: boolean; + truncated?: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 55a9abdf18..00cf1de981 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -12,7 +12,7 @@ import { getFetchSourcesFailure, getHunkCount, getSources, - isFetchSourcesPending, isUpdateSourcePending + isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; import Button from "@scm-manager/ui-components/src/buttons/Button"; @@ -86,7 +86,7 @@ class FileTree extends React.Component { } loadMore = () => { - // console.log("smth"); + this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; render() { @@ -124,22 +124,6 @@ class FileTree extends React.Component { }); } - const compareFiles = function(f1: File, f2: File): number { - if (f1.directory) { - if (f2.directory) { - return f1.name.localeCompare(f2.name); - } else { - return -1; - } - } else { - if (f2.directory) { - return 1; - } else { - return f1.name.localeCompare(f2.name); - } - } - }; - hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -149,7 +133,9 @@ class FileTree extends React.Component { } }); - if (files && files.length > 0) { + const loading = hunks.filter(hunk => hunk.loading).length > 0; + + if (loading || (files && files.length > 0)) { let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -195,7 +181,6 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path, 0); const error = getFetchSourcesFailure(state, repository, revision, path, 0); const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; @@ -212,7 +197,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { return { revision, path, - loading, error, hunks }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fa6ff5c1f6..fdd7a9fc78 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -37,6 +37,9 @@ const collection = { length: 176, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", subRepository: undefined, + truncated: true, + partialResult: false, + computationAborted: false, _links: { self: { href: @@ -120,12 +123,23 @@ describe("sources fetch", () => { const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/_/" + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/_/", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -136,17 +150,28 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository with the given revision and path", () => { - fetchMock.getOnce(sourcesUrl + "abc/src", collection); + fetchMock.getOnce(sourcesUrl + "abc/src?offset=0", collection); const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/abc/src" + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/abc/src", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -166,7 +191,7 @@ describe("sources fetch", () => { const actions = store.getActions(); expect(actions[0].type).toBe(FETCH_SOURCES_PENDING); expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE); - expect(actions[1].itemId).toBe("scm/core/_/"); + expect(actions[1].itemId).toBe("scm/core/_//"); expect(actions[1].payload).toBeDefined(); }); }); @@ -180,16 +205,18 @@ describe("reducer tests", () => { it("should store the collection, without revision and path", () => { const expectedState = { - "scm/core/_/": { updatePending: false, sources: collection } + "scm/core/_//0": { pending: false, updatePending: false, sources: collection }, + "scm/core/_//hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "", "", 0, collection))).toEqual(expectedState); }); it("should store the collection, with revision and path", () => { const expectedState = { - "scm/core/abc/src/main": { updatePending: false, sources: collection } + "scm/core/abc/src/main/0": { pending: false, updatePending: false, sources: collection }, + "scm/core/abc/src/main/hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", 0, collection))).toEqual(expectedState); }); }); @@ -197,7 +224,7 @@ describe("selector tests", () => { it("should return false if it is no directory", () => { const state = { sources: { - "scm/core/abc/src/main/package.json": { + "scm/core/abc/src/main/package.json/0": { sources: { noDirectory } } } @@ -208,7 +235,7 @@ describe("selector tests", () => { it("should return true if it is directory", () => { const state = { sources: { - "scm/core/abc/src": noDirectory + "scm/core/abc/src/0": noDirectory } }; expect(isDirectory(state, repository, "abc", "src")).toBe(true); @@ -221,7 +248,7 @@ describe("selector tests", () => { it("should return the source collection without revision and path", () => { const state = { sources: { - "scm/core/_/": { + "scm/core/_//0": { sources: collection } } @@ -232,7 +259,7 @@ describe("selector tests", () => { it("should return the source collection with revision and path", () => { const state = { sources: { - "scm/core/abc/src/main": { + "scm/core/abc/src/main/0": { sources: collection } } @@ -242,15 +269,26 @@ describe("selector tests", () => { it("should return true, when fetch sources is pending", () => { const state = { - pending: { - [FETCH_SOURCES + "/scm/core/_/"]: true + sources: { + "scm/core/_//0": { + pending: true, + sources: {} + } } }; - expect(isFetchSourcesPending(state, repository, "", "")).toEqual(true); + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(true); }); it("should return false, when fetch sources is not pending", () => { - expect(isFetchSourcesPending({}, repository, "", "")).toEqual(false); + const state = { + sources: { + "scm/core/_//0": { + pending: false, + sources: {} + } + } + }; + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(false); }); const error = new Error("incredible error from hell"); @@ -258,13 +296,13 @@ describe("selector tests", () => { it("should return error when fetch sources did fail", () => { const state = { failure: { - [FETCH_SOURCES + "/scm/core/_/"]: error + [FETCH_SOURCES + "/scm/core/_//"]: error } }; - expect(getFetchSourcesFailure(state, repository, "", "")).toEqual(error); + expect(getFetchSourcesFailure(state, repository, "", "", 0)).toEqual(error); }); it("should return undefined when fetch sources did not fail", () => { - expect(getFetchSourcesFailure({}, repository, "", "")).toBe(undefined); + expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(undefined); }); }); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 51c5eeb0e1..a12bf3ba0d 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -1,7 +1,6 @@ import * as types from "../../../modules/types"; import { Action, File, Link, Repository } from "@scm-manager/ui-types"; import { apiClient } from "@scm-manager/ui-components"; -import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES"; @@ -27,8 +26,18 @@ export function fetchSources(repository: Repository, revision: string, path: str updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) ); } + + let offset = 0; + const hunkCount = getHunkCount(state, repository, revision, path); + for (let i = 0; i < hunkCount; ++i) { + const sources = getSources(state, repository, revision, path, i); + if (sources?._embedded.children) { + offset += sources._embedded.children.length; + } + } + return apiClient - .get(createUrl(repository, revision, path, hunk)) + .get(createUrl(repository, revision, path, offset)) .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); @@ -39,7 +48,7 @@ export function fetchSources(repository: Repository, revision: string, path: str }; } -function createUrl(repository: Repository, revision: string, path: string, hunk: number) { +function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -47,14 +56,14 @@ function createUrl(repository: Repository, revision: string, path: string, hunk: // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?offset=${offset}`; } export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path), - payload: { hunk, pending: true, sources: {} } + itemId: createItemId(repository, revision, path, ""), + payload: { hunk, pending: true, updatePending: false, sources: {} } }; } @@ -63,12 +72,12 @@ export function updateSourcesPending( revision: string, path: string, hunk: number, - currentSources: any + currentSources: File ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { hunk, updatePending: true, sources: currentSources }, - itemId: createItemId(repository, revision, path) + payload: { hunk, pending: false, updatePending: true, sources: currentSources }, + itemId: createItemId(repository, revision, path, "") }; } @@ -82,7 +91,7 @@ export function fetchSourcesSuccess( return { type: FETCH_SOURCES_SUCCESS, payload: { hunk, pending: false, updatePending: false, sources }, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } @@ -96,14 +105,14 @@ export function fetchSourcesFailure( return { type: FETCH_SOURCES_FAILURE, payload: error, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } -function createItemId(repository: Repository, revision: string | undefined, path: string) { +function createItemId(repository: Repository, revision: string | undefined, path: string, hunk: number | string) { const revPart = revision ? revision : "_"; const pathPart = path ? path : ""; - return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}`; + return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}/${hunk}`; } // reducer @@ -114,18 +123,38 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { - console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); + if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { return { ...state, - [action.itemId + "/hunkCount"]: action.payload.hunk + 1, - [action.itemId + "/" + action.payload.hunk]: { + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { sources: action.payload.sources, - loading: false + updatePending: false, + pending: false } }; + } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + sources: action.payload.sources, + updatePending: true, + pending: false + } + }; + } else if (action.itemId && action.type === FETCH_SOURCES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + updatePending: false, + pending: true + } + }; + } else { + return state; } - return state; } // selectors @@ -141,7 +170,7 @@ export function isDirectory(state: any, repository: Repository, revision: string export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { if (state.sources) { - const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + const count = state.sources[createItemId(repository, revision, path, "hunkCount")]; return count ? count : 0; } return 0; @@ -152,10 +181,10 @@ export function getSources( repository: Repository, revision: string | undefined, path: string, - hunk: number + hunk = 0 ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; + return state.sources[createItemId(repository, revision, path, hunk)]?.sources; } return null; } @@ -165,9 +194,12 @@ export function isFetchSourcesPending( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): boolean { - return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.pending; + } + return false; } export function isUpdateSourcePending( @@ -177,7 +209,10 @@ export function isUpdateSourcePending( path: string, hunk: number ): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.updatePending; + } + return false; } export function getFetchSourcesFailure( @@ -185,7 +220,7 @@ export function getFetchSourcesFailure( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): Error | null | undefined { - return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); + return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path, "")); }