Refactor project reactivity

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier
2022-02-11 16:21:03 +01:00
parent 88cc74dc02
commit b9c4cda4cf
16 changed files with 367 additions and 298 deletions
+1 -1
View File
@@ -36,7 +36,7 @@
proj.path({
urn: project.urn,
seed: seed?.host,
addressOrName: profile?.name ?? profile?.address,
profile: profile?.name ?? profile?.address,
revision: project.head,
})
);
+20 -28
View File
@@ -15,9 +15,7 @@ const defaultProps = {
"hyn9diwfnytahjq8u3iw63h9jte1ydcatxax3saymwdxqu1zo645pe"
]
},
branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"]
],
branches: { "master": "e678629cd37c770c640a2cd997fc76303c815772" },
revision: "e678629cd37c770c640a2cd997fc76303c815772",
};
@@ -25,8 +23,7 @@ describe('BranchSelector', function () {
it("Render with commit = head and branch listing", () => {
mount(BranchSelector, {
props: defaultProps
}, styles
);
}, styles);
cy.findByText("master").should("exist");
cy.get("div.commit > div.hash.desktop").should("have.text", "e678629");
});
@@ -36,19 +33,18 @@ describe('BranchSelector', function () {
cy.viewport(800, 300);
mount(BranchSelector, {
props: {
...defaultProps, branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"],
["feature-branch", "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5"],
["xyz", "debf82ef3623ec11751a993bda85bac2ff1c6f00"],
],
...defaultProps, branches: {
"master": "e678629cd37c770c640a2cd997fc76303c815772",
"feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
"xyz": "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
branchesDropdown: true,
}, callbacks: {
revisionChanged: cy.stub().as("revisionChanged")
branchChanged: cy.stub().as("branchChanged")
}
}, styles
);
}, styles);
cy.get("div.dropdown > div").first().click();
cy.get("@revisionChanged")
cy.get("@branchChanged")
.should("be.calledOnce")
.its("firstCall.args.0.detail")
.should("equal", "feature-branch");
@@ -57,16 +53,14 @@ describe('BranchSelector', function () {
it("Render with commit != head, passing a branch as rev and branch listing", () => {
mount(BranchSelector, {
props: {
...defaultProps, branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"],
["feature-branch", "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5"],
["xyz", "debf82ef3623ec11751a993bda85bac2ff1c6f00"],
["xyz", "8b18e8006849e36872cb928d87179e4e222fb689"]
],
...defaultProps, branches: {
"master": "e678629cd37c770c640a2cd997fc76303c815772",
"feature-branch": "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5",
"xyz": "debf82ef3623ec11751a993bda85bac2ff1c6f00",
},
revision: "feature-branch"
}
}, styles
);
}, styles);
cy.findByText("feature-branch").should("exist");
cy.get("div.commit > div.hash.desktop").should("have.text", "29e8b7b");
});
@@ -87,12 +81,11 @@ describe('BranchSelector', function () {
props: {
...defaultProps,
revision: "e678629cd37c770c640a2cd997fc76303c815772",
branches: [],
branches: {},
}
}, styles
);
cy.findByText("master").should("exist");
cy.get("div.commit > div.hash").should("have.text", "e678629");
cy.get("div.commit > div.hash.desktop").should("have.text", "e678629cd37c770c640a2cd997fc76303c815772");
});
it("Render without branch listing, commit != head", () => {
@@ -100,10 +93,9 @@ describe('BranchSelector', function () {
props: {
...defaultProps,
revision: "6b84e519d3c535879eb2b9ee8457bb70ca275a75",
branches: [],
branches: {},
}
}, styles
);
}, styles);
cy.get("div.commit > div.hash.desktop").should("have.text", "6b84e519d3c535879eb2b9ee8457bb70ca275a75");
});
});
+36 -43
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { ProjectInfo, getOid } from "@app/project";
import { ProjectInfo, Branches, getOid } from "@app/project";
import { formatCommit, isOid } from "@app/utils";
import Dropdown from "@app/Dropdown.svelte";
export let branches: [string, string][];
export let branches: Branches;
export let project: ProjectInfo;
export let revision: string;
export let toggleDropdown: (input: string) => void;
@@ -12,25 +12,17 @@
const dispatch = createEventDispatcher();
const switchBranch = (name: string) => {
dispatch("revisionChanged", name);
dispatch("branchChanged", name);
};
const showSelector = branches.length > 1;
let branchLabel: string | null = null;
// Sort branches array alphabetically
const sortBranches = ([firstBranchName,]: [string, string], [secondBranchName,]: [string, string]) => {
if (firstBranchName < secondBranchName) return -1;
if (firstBranchName > secondBranchName) return 1;
return 0;
};
let branchLabel: string;
branches = branches.sort(sortBranches);
$: commit = getOid(project.head, revision, branches);
$: isLabel = commit == project.head || !isOid(revision);
$: if (commit == project.head) {
$: branchList = Object.keys(branches).sort();
$: showSelector = branchList.length > 1;
$: head = branches[project.defaultBranch];
$: commit = getOid(revision, branches) || head;
$: if (commit == head) {
branchLabel = project.defaultBranch;
} else if (!isOid(revision)) {
} else if (! isOid(revision)) {
branchLabel = revision;
}
</script>
@@ -60,10 +52,10 @@
color: var(--color-secondary);
background-color: var(--color-secondary-background-darker);
padding: 0.5rem 0.75rem;
border-radius: inherit;
border-radius: 0 0.25rem 0.25rem 0;
}
.hidden {
display: none;
.commit .hash.unlabeled {
border-radius: 0.25rem;
}
.stat {
font-family: var(--font-family-monospace);
@@ -74,33 +66,34 @@
<div class="commit">
<!-- Check for branches listing feature -->
{#if branches.length > 0}
<span>
<div
class="stat branch"
class:not-allowed={!showSelector}
class:hidden={!isLabel}
on:click={() => showSelector && toggleDropdown("branch")}
>
{branchLabel}
</div>
<Dropdown
items={branches.map(([name,]) => name)}
visible={branchesDropdown}
on:select={(e) => switchBranch(e.detail)} />
</span>
<div class="hash desktop">
{#if isLabel}
{#if branchList.length > 0}
{#if branchLabel}
<span>
<div
class="stat branch"
class:not-allowed={!showSelector}
on:click={() => showSelector && toggleDropdown("branch")}
>
{branchLabel}
</div>
<Dropdown
items={branchList}
visible={branchesDropdown}
on:select={(e) => switchBranch(e.detail)} />
</span>
<div class="hash desktop">
{formatCommit(commit)}
{:else}
</div>
{:else}
<div class="unlabeled hash desktop">
{commit}
{/if}
</div>
</div>
{/if}
<div class="hash mobile">
{formatCommit(commit)}
</div>
<!-- If there is no branch listing available, show default branch name if commit is head and else show entire commit -->
{:else if commit === project.head}
{:else if commit === head}
<div class="stat branch not-allowed">
{project.defaultBranch}
</div>
@@ -108,7 +101,7 @@
{formatCommit(commit)}
</div>
{:else}
<div class="hash desktop">
<div class="unlabeled hash desktop">
{commit}
</div>
<div class="hash mobile">
+12 -26
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { navigate } from 'svelte-routing';
import type { Readable } from 'svelte/store';
import * as proj from '@app/project';
import Loading from '@app/Loading.svelte';
import * as utils from '@app/utils';
@@ -13,25 +13,19 @@
Loaded,
}
type State = { status: Status.Loading; path: string }
type State =
{ status: Status.Loading; path: string }
| { status: Status.Loaded; path: string; blob: proj.Blob };
export let source: proj.Source;
export let tree: proj.Tree;
export let locator: string; // eg. "master/README.md"
export let content: proj.ProjectContent;
export let revision: string;
export let path: string;
export let browserStore: Readable<proj.Browser>;
let { urn, addressOrName, seed, peer, project, branches } = source;
const { urn, project } = source;
// This is reactive to respond to path changes that don't originate from this
// component, eg. when using the browser's "back" button.
$: [revision_, path_] = proj.splitPrefixFromPath(locator, branches, project.head);
// Bind content to file tree to trigger updates in parent components.
$: content = proj.ProjectContent.Tree;
$: revision = revision_;
$: path = path_;
$: browser = $browserStore;
$: path = browser.path || "/";
$: revision = browser.revision || browser.branches[project.head];
// When the component is loaded the first time, the blob is yet to be loaded.
let state: State = { status: Status.Loading, path };
@@ -63,29 +57,21 @@
// Close mobile tree if user navigates to other file
mobileFileTree = false;
navigateBrowser(revision, newPath);
};
const navigateBrowser = (revision: string, path?: string) => {
// Replaces path with current path if none passed.
if (path === undefined) path = state.path;
if (addressOrName) {
navigate(proj.path({ peer, urn, addressOrName, revision, path }));
} else {
navigate(proj.path({ peer, urn, seed: seed.host, revision, path }));
if (path) {
proj.navigateTo({ path: newPath, revision }, source);
}
};
const fetchTree = async (path: string) => {
return proj.getTree(urn, commit, path, seed.api);
return proj.getTree(urn, commit, path, source.seed.api);
};
const toggleMobileFileTree = () => {
mobileFileTree = !mobileFileTree;
};
$: commit = proj.getOid(project.head, revision, branches);
$: commit = proj.getOid(revision, browser.branches) || project.head;
$: getBlob = loadBlob(path);
$: loadingPath = state.status == Status.Loading ? state.path : null;
</script>
+11 -20
View File
@@ -1,31 +1,22 @@
<script lang="ts">
import * as proj from "@app/project";
import Changeset from "@app/base/projects/SourceBrowser/Changeset.svelte";
import { navigate } from "svelte-routing";
import { formatCommitTime } from "@app/commit";
import { formatCommit } from "@app/utils";
export let content: proj.ProjectContent;
export let revision: string;
export let locator: string;
export let source: proj.Source;
export let commit: string;
const { addressOrName, peer, seed, project, urn, branches } = source;
proj.browse({ content: proj.ProjectContent.Commit });
const navigateCommit = (path: string, content?: proj.ProjectContent) => {
// Replaces path with current path if none passed.
if (path === undefined) path = "/";
if (addressOrName) {
navigate(proj.path({ content, peer, urn, addressOrName, revision, path }));
} else {
navigate(proj.path({ content, peer, urn, seed: seed.host, revision, path }));
}
const { seed, urn } = source;
const onBrowse = (event: { detail: string }) => {
proj.navigateTo({
content: proj.ProjectContent.Tree,
revision: commit,
path: event.detail
}, source);
};
$: [revision_,] = proj.splitPrefixFromPath(locator, branches, project.head);
$: content = proj.ProjectContent.Commit;
$: revision = revision_;
</script>
<style>
@@ -72,7 +63,7 @@
}
</style>
{#await proj.getCommit(urn, revision, seed.api) then commit}
{#await proj.getCommit(urn, commit, seed.api) then commit}
<div class="commit">
<header>
<div class="summary">
@@ -95,7 +86,7 @@
<span class="font-mono email desktop-inline">&lt;{commit.header.author.email}&gt;</span>
</div>
</header>
<Changeset stats={commit.stats} diff={commit.diff} on:browse={(event) => navigateCommit(event.detail)} />
<Changeset stats={commit.stats} diff={commit.diff} on:browse={onBrowse} />
</div>
{:catch err}
<div class="commit">
+26 -20
View File
@@ -1,24 +1,30 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import { navigate } from 'svelte-routing';
import * as utils from '@app/utils';
import { ProjectContent, getOid, Source } from '@app/project';
import * as proj from '@app/project';
import { Browser, ProjectContent, Source } from '@app/project';
import AnchorBadge from '@app/base/profiles/AnchorBadge.svelte';
import type { Tree } from "@app/project";
import BranchSelector from './BranchSelector.svelte';
import PeerSelector from './PeerSelector.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let source: Source;
export let path: string;
export let tree: Tree;
export let content: ProjectContent;
export let revision: string;
// If peerSelector should be showed.
export let peerSelector: boolean;
export let commit: string;
export let browserStore: Writable<Browser>;
export let peerSelector: boolean; // If peerSelector should be showed.
export let branches: proj.Branches;
let { urn, peer, project, branches, peers, seed, anchors } = source;
let { urn, project, peers, seed, anchors } = source;
$: browser = $browserStore;
$: revision = browser.revision || commit;
$: content = browser.content;
$: if (Object.keys(browser.branches).length > 0) {
branches = browser.branches;
}
let dropdownState: { [key: string]: boolean } = { clone: false, seed: false, branch: false, peer: false };
function toggleDropdown(input: string) {
@@ -28,22 +34,22 @@
});
}
// Switches between the browser and commit view
// Switches between the browser and commit view.
const toggleContent = (input: ProjectContent) => {
dispatch("routeParamsChange", { urn, content: content === input ? ProjectContent.Tree : input, revision, peer, path });
proj.navigateTo({
content: content === input ? ProjectContent.Tree : input
}, source);
};
const updatePeer = (newPeer: string) => {
const updatePeer = (peer: string) => {
dropdownState.peer = false;
dispatch("routeParamsChange", { urn, content, revision, peer: newPeer, path });
proj.navigateTo({ peer, revision: null }, source);
};
const updateRevision = (newRevision: string) => {
const updateRevision = (revision: string) => {
dropdownState.branch = false;
dispatch("routeParamsChange", { urn, content, revision: newRevision, peer, path });
proj.navigateTo({ revision }, source);
};
$: commit = getOid(project.head, revision, branches);
</script>
<style>
@@ -152,13 +158,13 @@
<header>
{#if peers.length > 0 && peerSelector}
<PeerSelector {peers} {toggleDropdown} {peer}
<PeerSelector {peers} {toggleDropdown} peer={browser.peer}
bind:peersDropdown={dropdownState.peer}
on:peerChanged={(event) => updatePeer(event.detail)} />
{/if}
<BranchSelector {branches} {project} {revision} {toggleDropdown}
bind:branchesDropdown={dropdownState.branch}
on:revisionChanged={(event) => updateRevision(event.detail)} />
on:branchChanged={(event) => updateRevision(event.detail)} />
<div class="anchor">
<AnchorBadge {commit} {anchors}
head={project.head} on:click={(event) => updateRevision(event.detail)} />
+8 -25
View File
@@ -1,39 +1,22 @@
<script lang="ts">
import CommitTeaser from "./Commit/CommitTeaser.svelte";
import { getCommits, Source, getOid, ProjectContent, splitPrefixFromPath } from "@app/project";
import { getCommits, Source, ProjectContent } from "@app/project";
import * as proj from "@app/project";
import Loading from "@app/Loading.svelte";
import { groupCommitHistory, GroupedCommitsHistory } from "@app/commit";
import { navigate } from "svelte-routing";
export let source: Source;
export let locator: string;
export let content: ProjectContent;
export let revision: string;
export let path: string;
export let commit: string;
let { urn, seed, addressOrName, peer, project, branches } = source;
// Bind content to commit history to trigger updates in parent components.
$: [revision_,] = splitPrefixFromPath(locator, branches, project.head);
$: content = ProjectContent.History;
$: revision = revision_;
let { urn, seed } = source;
const navigateHistory = (revision: string, content?: ProjectContent) => {
// Replaces path with current path if none passed.
if (! path) path = "/";
if (addressOrName) {
navigate(proj.path({ content, peer, urn, addressOrName, revision, path }));
} else {
navigate(proj.path({ content, peer, urn, seed: seed.host, revision, path }));
}
proj.navigateTo({ content, revision }, source);
};
async function fetchCommits(revision: string): Promise<GroupedCommitsHistory> {
const commitsQuery = await getCommits(urn, getOid(project.head, revision, branches), seed.api);
const fetchCommits = async (parentCommit: string): Promise<GroupedCommitsHistory> => {
const commitsQuery = await getCommits(urn, parentCommit, seed.api);
return groupCommitHistory(commitsQuery);
}
};
</script>
<style>
@@ -70,7 +53,7 @@
}
</style>
{#await fetchCommits(revision)}
{#await fetchCommits(commit)}
<Loading center />
{:then history}
<div class="history">
+1 -2
View File
@@ -4,13 +4,12 @@
import Dropdown from "@app/Dropdown.svelte";
import { formatSeedId } from "@app/utils";
export let peer: string;
export let peer: string | null = null;
export let peers: string[];
export let toggleDropdown: (input: string) => void;
export let peersDropdown = false;
const dispatch = createEventDispatcher();
const switchPeer = (peer: string) => {
dispatch("peerChanged", peer);
};
+41 -33
View File
@@ -1,57 +1,65 @@
<script lang="ts">
import type { ProjectContent, Source, Tree } from "@app/project";
import type { Writable } from "svelte/store";
import { Route, Router } from "svelte-routing";
import type * as proj from "@app/project";
import { ProjectContent } from "@app/project";
import Browser from "./Browser.svelte";
import Commit from "./Commit.svelte";
import History from "./History.svelte";
import RouteContext from "./RouteContext.svelte";
export let source: Source;
export let tree: Tree;
export let content: ProjectContent;
export let revision: string;
export let path: string;
export let source: proj.Source;
export let tree: proj.Tree;
export let browserStore: Writable<proj.Browser>;
export let peer: string | null;
export let branches: proj.Branches;
let locator = source.anchors[0] || source.project.head;
const project = source.project;
</script>
<Router>
<!-- The default action is to render Browser with the default branch head -->
<Route path="/">
<Browser {source} {tree} {locator}
bind:content={content}
bind:path={path}
bind:revision={revision} />
<RouteContext {browserStore} {peer} {branches} {project}>
<Browser {source} {tree} {browserStore} />
</RouteContext>
</Route>
<Route path="/tree">
<Browser {source} {tree} {locator}
bind:content={content}
bind:path={path}
bind:revision={revision} />
<RouteContext {browserStore} {peer} {branches} {project}>
<Browser {source} {tree} {browserStore} />
</RouteContext>
</Route>
<Route path="/tree/*" let:params>
<Browser {source} {tree} locator={params["*"]}
bind:content={content}
bind:path={path}
bind:revision={revision} />
<RouteContext route={params["*"]} {browserStore} {peer} {branches} {project}>
<Browser {source} {tree} {browserStore} />
</RouteContext>
</Route>
<Route path="/history">
<History {locator} {source} {path}
bind:content={content}
bind:revision={revision} />
<RouteContext {browserStore} content={ProjectContent.History} {peer} {branches} {project} let:commit>
{#if commit}
<History {source} {commit} />
{/if}
</RouteContext>
</Route>
<Route path="/history/*" let:params>
<History locator={params["*"]} {source} {path}
bind:content={content}
bind:revision={revision} />
<RouteContext route={params["*"]} {browserStore} content={ProjectContent.History} {peer} {branches} {project} let:commit>
{#if commit}
<History {source} {commit} />
{/if}
</RouteContext>
</Route>
<Route path="/commit/:commit" let:params>
<Commit {source} locator={params.commit}
bind:content={content}
bind:revision={revision} />
<Route path="/commits/:commit" let:params>
<RouteContext revision={params.commit} {browserStore} {project} content={ProjectContent.Commit} {peer} {branches} let:revision>
{#if revision}
<Commit {source} commit={revision} />
{/if}
</RouteContext>
</Route>
<Route path="/commit/*" let:params>
<Commit {source} locator={params["*"]}
bind:content={content}
bind:revision={revision} />
<Route path="/commits/*" let:params>
<RouteContext route={params["*"]} {browserStore} {project} content={ProjectContent.Commit} {peer} {branches} let:revision>
{#if revision}
<Commit {source} commit={revision} />
{/if}
</RouteContext>
</Route>
</Router>
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import * as proj from '@app/project';
export let browserStore: Writable<proj.Browser>;
export let route: string | null = null;
export let revision: string | null = null;
export let peer: string | null;
export let content: proj.ProjectContent = proj.ProjectContent.Tree;
export let branches: proj.Branches;
export let project: proj.ProjectInfo;
const browse: any = { content, peer, branches, path: "/" };
$: if (route) {
const result = proj.splitPrefixFromPath(route, $browserStore.branches);
console.log("RouteParser", route, result);
if (result) {
const [revision, path] = result;
browse.revision = revision;
browse.path = path;
}
} else if (revision) {
browse.revision = revision;
} else {
browse.revision = branches[project.defaultBranch];
}
$: proj.browse(browse);
$: browser = $browserStore;
$: commit = browser.revision && proj.getOid(browser.revision, browser.branches);
</script>
<slot
revision={browser.revision}
peer={browser.peer}
path={browser.path || "/"}
{commit}
></slot>
+2 -2
View File
@@ -28,6 +28,6 @@
</Route>
<!-- End of eventual dropped routes -->
<Route path="/:addressOrName/:id/*" let:params>
<View {config} addressOrName={params.addressOrName} id={params.id} />
<Route path="/:profile/:id/*" let:params>
<View {config} profileName={params.profile} id={params.id} />
</Route>
@@ -59,6 +59,9 @@
flex-direction: row;
align-items: center;
}
.browse {
display: flex;
}
</style>
<div class="changeset-summary">
@@ -77,8 +80,9 @@
<p class="bold">{path}</p>
<span class="diff-type created">created</span>
</div>
<Icon class="clickable" name="browse" width={20} inline fill
on:click={() => dispatch("browse", path)} />
<div class="browse" on:click={() => dispatch("browse", path)}>
<Icon name="browse" width={20} inline fill />
</div>
</header>
{/each}
{#each diff.deleted as path (path)}
@@ -87,8 +91,9 @@
<p class="bold">{path}</p>
<span class="diff-type deleted">deleted</span>
</div>
<Icon class="clickable" name="browse" width={20} inline fill
on:click={() => dispatch("browse", path)} />
<div class="browse" on:click={() => dispatch("browse", path)}>
<Icon name="browse" width={20} inline fill />
</div>
</header>
{/each}
</div>
@@ -108,6 +108,9 @@
.file-path {
font-size: 1rem;
}
.browse {
display: flex;
}
</style>
<article id={file.path} class="changeset-file">
@@ -116,7 +119,9 @@
<div class="actions">
<p class="file-path">{file.path}</p>
</div>
<Icon name="browse" width={20} inline fill on:click={() => dispatch("browse", file.path)} />
<div class="browse clickable" on:click|stopPropagation={() => dispatch("browse", file.path)}>
<Icon name="browse" width={20} inline fill />
</div>
</header>
{#if !collapsed}
<main>
+36 -56
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import { navigate, Link } from 'svelte-routing';
import { Link } from 'svelte-routing';
import type { Config } from '@app/config';
import * as proj from '@app/project';
import Loading from '@app/Loading.svelte';
import Avatar from '@app/Avatar.svelte';
import { Profile, ProfileType } from '@app/profile';
import type { ProjectInfo } from '@app/project';
import { formatOrg, formatSeedId, isRadicleId, setOpenGraphMetaTag } from '@app/utils';
import { getOid } from '@app/project';
import { formatProfile, formatSeedId, isRadicleId, setOpenGraphMetaTag } from '@app/utils';
import { browserStore } from '@app/project';
import { Seed } from '@app/base/seeds/Seed';
import Header from '@app/base/projects/Header.svelte';
@@ -15,26 +15,28 @@
import NotFound from '@app/NotFound.svelte';
export let id: string; // Project name or URN.
export let addressOrName = "";
export let seedHost = "";
export let peer = "";
export let seedHost: string | null = null;
export let profileName: string | null = null; // Address or name of parent profile.
export let peer: string | null = null;
export let config: Config;
let parentName = formatOrg(addressOrName, config);
// Nb. Once we move the content routing above this component, this should
// no longer be necessary, but right now, we have the project header that
// is rendered before the routes are parsed, so we have to set this here.
proj.browse({ peer });
let parentName = profileName ? formatProfile(profileName, config) : null;
let pageTitle = parentName ? `${parentName}/${id}` : id;
let projectInfo: ProjectInfo | null = null;
let revision: string;
let content: proj.ProjectContent;
let path: string;
let getProject = new Promise<{ profile?: Profile | null; seed?: Seed } | null>((resolve, reject) => {
if (addressOrName) {
Profile.get(addressOrName, ProfileType.Project, config).then(p => resolve({ profile: p })).catch(err => reject(err.message));
if (profileName) {
Profile.get(profileName, ProfileType.Project, config).then(p => resolve({ profile: p })).catch(err => reject(err.message));
} else if (seedHost) {
Seed.lookup(seedHost, config).then(s => resolve({ seed: s })).catch(err => reject(err.message));
} else {
resolve(null);
}
}).then(async (result) => {
}).then(async (result): Promise<proj.Source> => {
if (! result) {
throw new Error("Couldn't load project");
}
@@ -50,23 +52,27 @@
const urn = isRadicleId(id) ? id : info.urn;
const anchors = profile ? await profile.confirmedProjectAnchors(urn, config) : [];
let branches = Array([info.defaultBranch, info.head]) as [string, string][];
let peers: proj.Peer[] = [];
// Older versions of http-api don't include the URN.
if (! info.urn) info.urn = urn;
projectInfo = info;
// Checks for delegates returned from seed node, as feature check of the seed node
if (info.delegates) {
// Check for selected peer to override available branches.
if (peer) {
const branchesByPeer = await proj.getBranchesByPeer(urn, peer || info.delegates[0], seed.api);
branches = [...Object.entries(branchesByPeer.heads)];
}
peers = await proj.getPeers(urn, seed.api);
}
return { urn, addressOrName, seed, peer, project: info, branches, peers, config, profile, anchors };
const peers: proj.PeerId[] = info.delegates
? await proj.getRemotes(urn, seed.api)
: [];
return { urn, seed, project: info, peers, profile, anchors };
});
function rootPath(source: proj.Source): string {
return proj.pathTo({
content: proj.ProjectContent.Tree,
peer: null,
path: "/",
revision: null,
}, source);
}
$: if (projectInfo) {
const baseName = parentName
? `${parentName}/${projectInfo.name}`
@@ -84,26 +90,6 @@
{ prop: "og:url", content: window.location.href }
]);
}
function updateRouteParams({ detail: newParams }: { detail: { urn: string; path: string; revision: string; peer: string; content: proj.ProjectContent } }) {
const newLocation = proj.path({
addressOrName,
seed: seedHost,
urn: newParams.urn,
content: newParams.content,
peer: newParams.peer,
revision: newParams.revision,
path: newParams.path
});
if (newLocation !== window.location.pathname) {
navigate(newLocation);
}
if (content !== newParams.content) content = newParams.content;
if (revision !== newParams.revision) revision = newParams.revision;
if (path !== newParams.path) path = newParams.path;
if (peer !== newParams.peer) peer = newParams.peer;
}
</script>
<style>
@@ -185,7 +171,7 @@
</a>
<span class="divider">/</span>
{/if}
<Link to={proj.path({ urn: result.urn, addressOrName, seed: result.seed.host })}>{result.project.name}</Link>
<Link to={rootPath(result)}>{result.project.name}</Link>
{#if peer}
<span class="divider" title={peer}>/ {formatSeedId(peer)}</span>
{/if}
@@ -194,16 +180,10 @@
<div class="description">{result.project.description}</div>
</header>
{#await proj.getTree(result.urn, getOid(result.project.head, revision, result.branches), "/", result.seed.api) then tree}
<Header {tree} {revision} {content} {path}
source={result}
peerSelector={!!seedHost}
on:routeParamsChange={updateRouteParams} />
<ProjectContentRoutes {tree}
source={result}
bind:content={content}
bind:revision={revision}
bind:path={path} />
<!-- TODO: Should reivision be null? -->
{#await proj.getRoot(result.project, null, peer, result.seed.api) then { tree, branches, commit }}
<Header {tree} {branches} {commit} {browserStore} source={result} peerSelector={!!seedHost} />
<ProjectContentRoutes {tree} {peer} {branches} {browserStore} source={result} />
{:catch err}
<div class="container center-content">
<div class="error error-message text-xsmall">
+114 -35
View File
@@ -1,3 +1,5 @@
import { navigate } from 'svelte-routing';
import { get, writable } from 'svelte/store';
import * as api from '@app/api';
import type { Commit, CommitHeader, CommitsHistory } from '@app/commit';
import { isOid } from '@app/utils';
@@ -5,8 +7,8 @@ import type { Profile } from '@app/profile';
import type { Seed } from '@app/base/seeds/Seed';
export type Urn = string;
export type Peer = string;
export type Branch = { [key: string]: string };
export type PeerId = string;
export type Branches = { [key: string]: string };
export interface Anchor {
confirmed: true;
@@ -29,13 +31,10 @@ export interface PendingAnchor {
// Params to render correctly source code related views
export interface Source {
urn: string;
addressOrName: string;
peer: string;
project: ProjectInfo;
peers: Peer[];
peers: PeerId[];
anchors: string[];
seed: Seed;
branches: [string, string][];
profile?: Profile | null;
}
@@ -53,7 +52,7 @@ export interface ProjectInfo {
description: string;
defaultBranch: string;
maintainers: Urn[];
delegates: Peer[];
delegates: PeerId[];
}
export interface Tree {
@@ -92,8 +91,68 @@ export interface Blob {
info: EntryInfo;
}
export interface Branches {
heads: Branch;
export interface Remote {
heads: Branches;
}
export interface Browser {
content: ProjectContent;
branches: Branches;
revision: string | null;
peer: string | null;
path: string | null;
}
export const browserStore = writable({
content: ProjectContent.Tree,
branches: {},
revision: null,
peer: null,
path: null,
} as Browser);
export interface BrowseTo {
content?: ProjectContent;
revision?: string | null;
path?: string | null;
peer?: string | null;
branches?: Branches;
}
export interface PathOptions {
urn: string;
content?: ProjectContent;
profile?: string | null;
seed?: string | null;
peer?: string | null;
revision?: string | null;
path?: string | null;
}
export function browse(browse: BrowseTo): void {
const browser = get(browserStore);
browserStore.set({ ...browser, ...browse });
}
export function pathTo(browse: BrowseTo, source: Source): string {
const browser = get(browserStore);
const options: PathOptions = {
urn: source.urn,
...browser,
...browse
};
if (source.profile) {
options.profile = source.profile?.nameOrAddress;
} else {
options.seed = source.seed.host;
}
return path(options);
}
export function navigateTo(browse: BrowseTo, source: Source): void {
navigate(pathTo(browse, source));
}
export async function getInfo(nameOrUrn: string, host: api.Host): Promise<ProjectInfo> {
@@ -121,14 +180,41 @@ export async function getDelegateProjects(delegate: string, host: api.Host): Pro
return api.get(`delegates/${delegate}/projects`, {}, host);
}
export async function getBranchesByPeer(urn: string, peer: string, host: api.Host): Promise<Branches> {
export async function getRemote(urn: string, peer: string, host: api.Host): Promise<Remote> {
return api.get(`projects/${urn}/remotes/${peer}`, {}, host);
}
export async function getPeers(urn: string, host: api.Host): Promise<Peer[]> {
export async function getRemotes(urn: string, host: api.Host): Promise<PeerId[]> {
return api.get(`projects/${urn}/remotes`, {}, host);
}
export async function getRoot(
project: ProjectInfo,
revision: string | null,
peer: string | null,
host: api.Host
): Promise<{ tree: Tree; branches: Branches; commit: string }> {
const urn = project.urn;
let remote: Remote = {
heads: { [project.defaultBranch]: project.head }
};
if (peer) {
remote = await getRemote(urn, peer, host);
}
const head = remote.heads[project.defaultBranch];
const commit = revision ? getOid(revision, remote.heads) : head;
if (! commit) {
throw new Error(`Revision ${revision} not found`);
}
const tree = await getTree(urn, commit, "/", host);
return { tree, branches: remote.heads, commit };
}
export async function getTree(
urn: string,
commit: string,
@@ -157,22 +243,12 @@ export async function getReadme(
return api.get(`projects/${urn}/readme/${commit}`, {}, host);
}
export function path(
opts: {
urn: string;
addressOrName?: string;
seed?: string;
peer?: string;
content?: ProjectContent;
revision?: string;
path?: string;
}
): string {
const { urn, addressOrName, seed, peer, content, revision, path } = opts;
export function path(opts: PathOptions): string {
const { urn, profile, seed, peer, content, revision, path } = opts;
const result = [];
if (addressOrName) {
result.push(addressOrName);
if (profile) {
result.push(profile);
} else if (seed) {
result.push("seeds", seed);
}
@@ -188,7 +264,7 @@ export function path(
break;
case ProjectContent.Commit:
result.push("commit");
result.push("commits");
break;
default:
@@ -198,8 +274,6 @@ export function path(
if (revision) {
result.push(revision);
} else if (path) {
result.push("head");
}
// Avoids appending a slash when the path is the root directory.
@@ -211,24 +285,29 @@ export function path(
// We need a SHA1 commit in some places, so we return early if the revision is a SHA and else we look into branches.
// As fallback we use the head commit.
export function getOid(head: string, revision: string, branches?: [string, string][]): string {
export function getOid(revision: string, branches?: Branches): string | null {
if (isOid(revision)) return revision;
if (branches) {
const branch = branches.find(([name,]) => name === revision);
return branch ? branch[1] : head;
const oid = branches[revision];
if (oid) {
return oid;
}
}
return head;
return null;
}
// Splits the path consisting of a revision (eg. branch or commit) and file path into a tuple [revision, file-path]
export function splitPrefixFromPath(input: string, branches: [string, string][], head: string): [string, string] {
const branch = branches.find(([branchName,]) => input.startsWith(branchName));
export function splitPrefixFromPath(input: string, branches: Branches): [string, string] | null {
const branch = Object.entries(branches).find(([branchName,]) => input.startsWith(branchName));
const commitPath = [input.slice(0, 40), input.slice(41)];
if (branch) {
const [rev, path] = [input.slice(0, branch[0].length), input.slice(branch[0].length + 1)];
return [rev, path ? path : "/"];
} else if (isOid(commitPath[0])) {
return [commitPath[0], commitPath[1] ? commitPath[1] : "/"];
}
return [head, "/"];
return null;
}
+2 -2
View File
@@ -131,7 +131,7 @@ export function formatCommit(oid: string): string {
return oid.substring(0, 7);
}
export function formatOrg(input: string, config: Config): string {
export function formatProfile(input: string, config: Config): string {
if (isAddress(input)) {
return ethers.utils.getAddress(input);
} else {
@@ -397,7 +397,7 @@ export async function getSafe(address: string, config: Config): Promise<Safe | n
// For the subgraph we need to pass a lowercase address
const query = await querySubgraph(config.orgs.subgraph, GetSafe, { addr: address.toLowerCase() });
if (! query.safe) {
if (! query?.safe) {
return null;
}