mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-30 16:50:47 +08:00
Refactor project reactivity
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"><{commit.header.author.email}></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">
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user