mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-31 01:00:47 +08:00
Add issue creation
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Generated
+14
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@wooorm/starry-night": "^1.5.0",
|
||||
"bs58": "^5.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.0.0",
|
||||
"hast-util-to-dom": "^3.1.1",
|
||||
@@ -1100,6 +1101,11 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base-x": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
|
||||
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -1150,6 +1156,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bs58": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"dependencies": {
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"dependencies": {
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@wooorm/starry-night": "^1.5.0",
|
||||
"bs58": "^5.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.0.0",
|
||||
"hast-util-to-dom": "^3.1.1",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
:root {
|
||||
--border-radius: 0.75rem;
|
||||
--border-radius-tiny: 0.25rem;
|
||||
--border-radius-small: 0.5rem;
|
||||
--border-radius-round: 10rem;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import type { Author } from "@app/lib/cobs";
|
||||
|
||||
import {
|
||||
formatNodeId,
|
||||
formatRadicleId,
|
||||
formatSeedId,
|
||||
formatTimestamp,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
color: var(--color-foreground);
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
.id {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.caption {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
@@ -32,11 +37,16 @@
|
||||
</style>
|
||||
|
||||
<span class="authorship txt-tiny">
|
||||
<span class="highlight">
|
||||
{window.HEARTWOOD ? formatSeedId(author.id) : formatRadicleId(author.id)}
|
||||
<span class="id highlight layout-desktop">
|
||||
{window.HEARTWOOD ? formatNodeId(author.id) : formatRadicleId(author.id)}
|
||||
</span>
|
||||
<span class="id highlight layout-mobile">
|
||||
{window.HEARTWOOD
|
||||
? formatNodeId(author.id).replace("did:key:", "")
|
||||
: formatRadicleId(author.id)}
|
||||
</span>
|
||||
<span class="caption"> {caption} </span>
|
||||
<span class="txt-tiny date">
|
||||
<span class="date">
|
||||
{formatTimestamp(timestamp)}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let removeable: boolean = false;
|
||||
export let key: number;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.text {
|
||||
background-color: var(--color-secondary-3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.close {
|
||||
color: var(--color-secondary);
|
||||
border: none;
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
background-color: var(--color-secondary-2);
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close:hover {
|
||||
background-color: var(--color-secondary-5);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.removeable {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chip">
|
||||
<span class="section text" class:removeable><slot name="text" /></span>
|
||||
{#if removeable}
|
||||
<button class="section close" on:click={() => dispatch("remove", key)}>
|
||||
✕
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,36 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Blob } from "@app/lib/project";
|
||||
import type { MaybeBlob } from "@app/lib/project";
|
||||
import type { Comment, Thread } from "@app/lib/issue";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Avatar from "@app/components/Comment/Avatar.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import ReactionSelector from "./Comment/ReactionSelector.svelte";
|
||||
import Reactions from "@app/components/Comment/Reactions.svelte";
|
||||
|
||||
export let comment: Comment | Thread;
|
||||
export let caption = "left a comment";
|
||||
export let getImage: (path: string) => Promise<Blob>;
|
||||
|
||||
const templateComment = `<!--
|
||||
Please enter a comment message for your patch update. Leaving this
|
||||
blank is also okay.
|
||||
-->`;
|
||||
export let caption = "commented";
|
||||
export let getImage: (path: string) => Promise<MaybeBlob>;
|
||||
|
||||
$: source = comment.author.id;
|
||||
$: title = comment.author.profile
|
||||
? comment.author.profile.name
|
||||
: comment.author.id;
|
||||
|
||||
const selectReaction = (event: { detail: string }) => {
|
||||
// TODO: Once we allow adding reactions through the http-api, we should call it here.
|
||||
console.debug(event.detail);
|
||||
};
|
||||
|
||||
const incrementReaction = (event: { detail: string }) => {
|
||||
// TODO: Once we allow increment reactions through the http-api, we should call it here.
|
||||
console.debug(event.detail);
|
||||
};
|
||||
$: title = comment.author.id;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -57,10 +38,7 @@ blank is also okay.
|
||||
.card-body {
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0rem 1rem 1rem 1rem;
|
||||
}
|
||||
.reactions {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,21 +52,13 @@ blank is also okay.
|
||||
{caption}
|
||||
author={comment.author}
|
||||
timestamp={comment.timestamp} />
|
||||
<ReactionSelector on:select={selectReaction} />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{#if comment.body.trim() === "" || comment.body.trim() === templateComment}
|
||||
{#if comment.body.trim() === ""}
|
||||
<span class="txt-missing">No description.</span>
|
||||
{:else}
|
||||
<Markdown content={comment.body} {getImage} />
|
||||
{/if}
|
||||
{#if comment.reactions.length > 0}
|
||||
<div class="reactions">
|
||||
<Reactions
|
||||
reactions={comment.reactions}
|
||||
on:click={incrementReaction} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
function createContainer(source: string) {
|
||||
source = source.replace("did:key:", "");
|
||||
const seed = source.toLowerCase();
|
||||
const avatar = createIcon({
|
||||
seed,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<!-- TODO: Once we are able to add reactions, we should allow people to interact with the reaction handler -->
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
const showReactions = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: string }>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
color: var(--color-foreground-5);
|
||||
border-radius: var(--border-radius);
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.selector > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
background-color: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.modal > div {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.modal > div:last-child {
|
||||
border-top-right-radius: var(--border-radius-small);
|
||||
border-bottom-right-radius: var(--border-radius-small);
|
||||
}
|
||||
.modal > div:first-child {
|
||||
border-top-left-radius: var(--border-radius-small);
|
||||
border-bottom-left-radius: var(--border-radius-small);
|
||||
}
|
||||
.modal > div:hover {
|
||||
background-color: var(--color-foreground-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="selector">
|
||||
<Icon name="ellipsis" />
|
||||
{#if showReactions}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="modal">
|
||||
{#each config.reactions as reaction}
|
||||
<div on:click={() => dispatch("select", reaction)}>
|
||||
{reaction}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let reactions: Record<string, number> | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: string }>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.reactions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if reactions}
|
||||
<div class="reactions">
|
||||
{#each Object.entries(reactions) as [reaction, count]}
|
||||
<!-- TODO: Remove the disabled attribute once we are able to increment reactions -->
|
||||
<Button
|
||||
variant="foreground"
|
||||
size="tiny"
|
||||
on:click={() => dispatch("click", reaction)}>
|
||||
{reaction}
|
||||
{count}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -12,6 +12,7 @@
|
||||
| "ellipsis"
|
||||
| "fork"
|
||||
| "gear"
|
||||
| "chat"
|
||||
| "magnifying-glass"
|
||||
| "moon"
|
||||
| "sun"
|
||||
@@ -70,6 +71,21 @@
|
||||
d="M14 7L17 10V16H10V7H14ZM13
|
||||
8H11V15H16V11H13V8ZM14 8.41421V10H15.5858L14 8.41421ZM8
|
||||
10H9V17H14V18H8V10Z" />
|
||||
{:else if name === "chat"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5 6.5C4.72386 6.5 4.5
|
||||
6.72386 4.5 7V16C4.5 16.2761 4.72386 16.5 5 16.5H14.5147C16.2386 16.5 17.8919
|
||||
17.1848 19.1109 18.4038L19.5 18.7929V7C19.5 6.72386 19.2761 6.5 19 6.5H5ZM3.5
|
||||
7C3.5 6.17157 4.17157 5.5 5 5.5H19C19.8284 5.5 20.5 6.17157 20.5 7V20C20.5
|
||||
20.2022 20.3782 20.3845 20.1913 20.4619C20.0045 20.5393 19.7894 20.4966
|
||||
19.6464 20.3536L18.4038 19.1109C17.3724 18.0795 15.9734 17.5 14.5147
|
||||
17.5H5C4.17157 17.5 3.5 16.8284 3.5 16V7ZM7.5 10C7.5 9.72386 7.72386 9.5 8
|
||||
9.5H16C16.2761 9.5 16.5 9.72386 16.5 10C16.5 10.2761 16.2761 10.5 16
|
||||
10.5H8C7.72386 10.5 7.5 10.2761 7.5 10ZM7.5 13C7.5 12.7239 7.72386 12.5 8
|
||||
12.5H13C13.2761 12.5 13.5 12.7239 13.5 13C13.5 13.2761 13.2761 13.5 13
|
||||
13.5H8C7.72386 13.5 7.5 13.2761 7.5 13Z" />
|
||||
{:else if name === "ellipsis"}
|
||||
<path
|
||||
d="M7 12a2 2 0 1 1-4.001-.001A2 2 0 0 1 7 12zm12-2a2 2 0 1 0 .001
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
|
||||
export let variant: "regular" | "dashed" = "regular";
|
||||
export let variant: "regular" | "form" = "regular";
|
||||
|
||||
export let autofocus: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
@@ -76,14 +76,17 @@
|
||||
font-size: var(--font-size-regular);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.dashed {
|
||||
border: 1px dashed var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0.5rem 1.25rem;
|
||||
.form {
|
||||
background: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid var(--color-foreground-1);
|
||||
}
|
||||
.dashed:hover,
|
||||
.dashed:focus {
|
||||
background: var(--color-background-1);
|
||||
.form::placeholder {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.form:focus,
|
||||
.form:hover {
|
||||
border: 1px solid var(--color-foreground-4);
|
||||
}
|
||||
.left-container {
|
||||
color: var(--color-secondary);
|
||||
@@ -141,7 +144,7 @@
|
||||
|
||||
<input
|
||||
class:regular={variant === "regular"}
|
||||
class:dashed={variant === "dashed"}
|
||||
class:form={variant === "form"}
|
||||
style:padding-left={leftContainerWidth
|
||||
? `${leftContainerWidth}px`
|
||||
: "auto"}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let resizable: boolean = false;
|
||||
export let value: string | number | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
|
||||
let textareaElement: HTMLTextAreaElement | undefined = undefined;
|
||||
|
||||
// We either auto-grow the text area, or allow the user to resize it. These
|
||||
// options are mutually exclusive because a user resized textarea would
|
||||
// automatically shrink upon text input otherwise.
|
||||
$: if (textareaElement && !resizable) {
|
||||
// React to changes to the textarea content.
|
||||
value;
|
||||
|
||||
// Reset height to 0px on every value change so that the textarea
|
||||
// immediately shrinks when all text is deleted.
|
||||
textareaElement.style.height = `0px`;
|
||||
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
submit: never;
|
||||
}>();
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
dispatch("submit");
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
textareaElement?.blur();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
background-color: var(--color-foreground-1);
|
||||
border: 1px solid var(--color-foreground-1);
|
||||
color: var(--color-foreground);
|
||||
border-radius: 0.5rem;
|
||||
font-family: inherit;
|
||||
height: 5rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resizable {
|
||||
resize: vertical;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
textarea::-webkit-resizer {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAD9QTFRFAAAAZWZmZmZmZmVmZWVmwsLBwsLCZ2ZmwsPCZmdlZWZnwcLBZmZkYGJjw8LDwsPBZmZnZWZkZ2ZkwMDBWFtcNbXb2AAAABV0Uk5TAP///////////////////////1H/YDRrSAAAAFBJREFUeJxVjUESgCAMA2mqAoqK6P/f6kzjIXIos5NumpI8g5LbpJnNQvDl52mWUYTquqnXwstshpHaTi+o+hHXccoKmHVW9yvIxv218ntivmOYAWpLfqaRAAAAAElFTkSuQmCC);
|
||||
background-size: 7px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom 1px right 1px;
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
textarea:hover {
|
||||
border: 1px solid var(--color-foreground-4);
|
||||
}
|
||||
</style>
|
||||
|
||||
<textarea
|
||||
bind:this={textareaElement}
|
||||
bind:value
|
||||
class:resizable
|
||||
{placeholder}
|
||||
on:change
|
||||
on:click
|
||||
on:input
|
||||
on:keydown|stopPropagation={handleKeydown}
|
||||
on:keypress />
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
import { defaultSeedPort } from "@app/lib/seed";
|
||||
import { isLocal } from "@app/lib/utils";
|
||||
|
||||
export interface Host {
|
||||
host: string;
|
||||
@@ -19,7 +20,7 @@ export class Request {
|
||||
this.base = api.host;
|
||||
}
|
||||
this.path = path.startsWith("/") ? path.slice(1) : path;
|
||||
this.protocol = api.host === "0.0.0.0" ? "http://" : "https://";
|
||||
this.protocol = isLocal(api.host) ? "http://" : "https://";
|
||||
}
|
||||
|
||||
async get(
|
||||
@@ -87,7 +88,7 @@ export class Request {
|
||||
const query: Record<string, string> = {};
|
||||
for (const [key, val] of Object.entries(params)) {
|
||||
if (val !== undefined && val !== null) {
|
||||
query[key] = val.toString();
|
||||
query[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-14
@@ -1,26 +1,23 @@
|
||||
import type { PeerId } from "@app/lib/project";
|
||||
export type Thread = Comment<Comment[]>;
|
||||
|
||||
export interface Comment<R = null> {
|
||||
author: Author;
|
||||
body: string;
|
||||
reactions: Record<string, number>;
|
||||
timestamp: number;
|
||||
replies: R; // TODO: Remove for Heartwood migration
|
||||
replyTo: R;
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
peer: PeerId;
|
||||
id: string;
|
||||
profile: {
|
||||
name: string;
|
||||
ens: {
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface PeerIdentity {
|
||||
id: string;
|
||||
name: string;
|
||||
ens: {
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface PeerInfo {
|
||||
id: PeerId;
|
||||
id: string;
|
||||
person?: PeerIdentity;
|
||||
delegate: boolean;
|
||||
}
|
||||
|
||||
+28
-5
@@ -1,5 +1,7 @@
|
||||
import { type Host, Request } from "@app/lib/api";
|
||||
import type { Author } from "@app/lib/cobs";
|
||||
import type { Host } from "@app/lib/api";
|
||||
|
||||
import { Request } from "@app/lib/api";
|
||||
|
||||
export interface TimelineItem {
|
||||
person: Author;
|
||||
@@ -14,7 +16,8 @@ export interface IIssue {
|
||||
state: State;
|
||||
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
discussion: Thread[];
|
||||
tags: Tag[];
|
||||
tags: string[];
|
||||
assignees: string[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -38,8 +41,6 @@ export interface Comment<R = null> {
|
||||
|
||||
export type Thread = Comment<Comment[]>;
|
||||
|
||||
export type Tag = string;
|
||||
|
||||
export function groupIssues(issues: Issue[]): {
|
||||
open: Issue[];
|
||||
closed: Issue[];
|
||||
@@ -60,7 +61,8 @@ export class Issue {
|
||||
state: State;
|
||||
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
discussion: Thread[];
|
||||
tags: Tag[];
|
||||
tags: string[];
|
||||
assignees: string[];
|
||||
timestamp: number;
|
||||
|
||||
constructor(issue: IIssue) {
|
||||
@@ -71,6 +73,7 @@ export class Issue {
|
||||
this.comment = issue.comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.discussion = issue.discussion;
|
||||
this.tags = issue.tags;
|
||||
this.assignees = issue.assignees;
|
||||
if (window.HEARTWOOD) {
|
||||
this.timestamp = issue.discussion[0].timestamp;
|
||||
} else {
|
||||
@@ -92,6 +95,26 @@ export class Issue {
|
||||
}
|
||||
}
|
||||
|
||||
static async createIssue(
|
||||
project: string,
|
||||
title: string,
|
||||
description: string,
|
||||
assignees: string[],
|
||||
tags: string[],
|
||||
host: Host,
|
||||
authToken: string,
|
||||
): Promise<void> {
|
||||
await new Request(`projects/${project}/issues`, host).post(
|
||||
{
|
||||
title,
|
||||
description,
|
||||
assignees,
|
||||
tags,
|
||||
},
|
||||
{ Authorization: `Bearer ${authToken}` },
|
||||
);
|
||||
}
|
||||
|
||||
static async getIssues(id: string, host: Host): Promise<Issue[]> {
|
||||
const response: IIssue[] = await new Request(
|
||||
`projects/${id}/issues`,
|
||||
|
||||
+24
-12
@@ -41,10 +41,17 @@ export interface ProjectInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
delegates: string[];
|
||||
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
patches?: number;
|
||||
issues?: number;
|
||||
patches: {
|
||||
proposed: number;
|
||||
draft: number;
|
||||
archived: number;
|
||||
};
|
||||
issues: {
|
||||
open: number;
|
||||
closed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Tree {
|
||||
@@ -153,14 +160,20 @@ export class Project implements ProjectInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
delegates: string[];
|
||||
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
seed: Seed;
|
||||
peers: Peer[];
|
||||
branches: Branches;
|
||||
// At the moment we still have seed nodes which won't return neither patches or issues
|
||||
patches?: number;
|
||||
issues?: number;
|
||||
patches: {
|
||||
proposed: number;
|
||||
draft: number;
|
||||
archived: number;
|
||||
};
|
||||
issues: {
|
||||
open: number;
|
||||
closed: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@@ -174,7 +187,7 @@ export class Project implements ProjectInfo {
|
||||
this.name = info.name;
|
||||
this.description = info.description;
|
||||
this.defaultBranch = info.defaultBranch;
|
||||
this.delegates = info.delegates; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.delegates = info.delegates;
|
||||
this.remotes = info.remotes; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.seed = seed;
|
||||
this.peers = peers;
|
||||
@@ -407,11 +420,10 @@ export class Project implements ProjectInfo {
|
||||
? seedHost.split(":")
|
||||
: [seedHost, defaultSeedPort];
|
||||
|
||||
const seed = await Seed.lookup(host, Number(port));
|
||||
|
||||
if (!seed) {
|
||||
const seed = await Seed.lookup(host, Number(port)).catch(() => {
|
||||
throw new Error("Couldn't load project");
|
||||
}
|
||||
});
|
||||
|
||||
if (!seed?.valid) {
|
||||
throw new Error("Couldn't load project: invalid seed");
|
||||
}
|
||||
|
||||
+18
-3
@@ -256,6 +256,11 @@ export function routeToPath(route: Route) {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/patches${suffix}`;
|
||||
} else if (route.params.view.resource === "patch") {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/patches/${route.params.view.params.patch}`;
|
||||
} else if (
|
||||
route.params.view.resource === "issues" &&
|
||||
route.params.view.params?.view.resource === "new"
|
||||
) {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/issues/new${suffix}`;
|
||||
} else if (route.params.view.resource === "issues") {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/issues${suffix}`;
|
||||
} else if (route.params.view.resource === "issue") {
|
||||
@@ -344,10 +349,20 @@ function resolveProjectRoute(
|
||||
};
|
||||
}
|
||||
} else if (content === "issues") {
|
||||
const issue = segments.shift();
|
||||
if (issue) {
|
||||
const issueOrAction = segments.shift();
|
||||
if (issueOrAction === "new") {
|
||||
return {
|
||||
view: { resource: "issue", params: { issue } },
|
||||
view: { resource: "issues", params: { view: { resource: "new" } } },
|
||||
id,
|
||||
seed,
|
||||
peer,
|
||||
search: sanitizeQueryString(url.search),
|
||||
path: undefined,
|
||||
revision: undefined,
|
||||
};
|
||||
} else if (issueOrAction) {
|
||||
return {
|
||||
view: { resource: "issue", params: { issue: issueOrAction } },
|
||||
id,
|
||||
seed,
|
||||
peer,
|
||||
|
||||
@@ -15,7 +15,12 @@ export interface ProjectsParams {
|
||||
| { resource: "commits" }
|
||||
| { resource: "history" }
|
||||
| { resource: "issue"; params: { issue: string } }
|
||||
| { resource: "issues" }
|
||||
| {
|
||||
resource: "issues";
|
||||
params?: {
|
||||
view: { resource: "new" };
|
||||
};
|
||||
}
|
||||
| { resource: "patch"; params: { patch: string } }
|
||||
| { resource: "patches" };
|
||||
seed: string;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
|
||||
interface Session {
|
||||
export interface Session {
|
||||
id: string;
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
+38
-2
@@ -1,4 +1,5 @@
|
||||
import md5 from "md5";
|
||||
import bs58 from "bs58";
|
||||
import twemojiModule from "twemoji";
|
||||
|
||||
import { assert } from "@app/lib/error";
|
||||
@@ -56,6 +57,41 @@ export function formatRadicleId(id: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Parses a NID into an object of prefix and pubkey,
|
||||
// since prefix can be undefined
|
||||
export function parseNid(
|
||||
nid: string,
|
||||
): { prefix: string; pubkey: string } | undefined {
|
||||
const match = /^(did:key:)?(z[a-zA-Z0-9]+)$/.exec(nid);
|
||||
if (match) {
|
||||
const hex = bs58.decode(match[2].substring(1));
|
||||
// This checks also that the first 2 bytes are equal
|
||||
// to the ed25519 public key type used.
|
||||
if (!(hex.byteLength === 34 && hex[0] === 0xed && hex[1] === 1)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { prefix: match[1] || "did:key:", pubkey: match[2] };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Format a Node Identifier (NID), also represents users or peers
|
||||
export function formatNodeId(nid: string): string {
|
||||
const parsedNid = parseNid(nid);
|
||||
if (parsedNid) {
|
||||
const { prefix, pubkey } = parsedNid;
|
||||
const formattedPubKey =
|
||||
pubkey.substring(0, 6) +
|
||||
"…" +
|
||||
pubkey.substring(pubkey.length - 6, pubkey.length);
|
||||
return `${prefix}${formattedPubKey}`;
|
||||
}
|
||||
|
||||
return nid;
|
||||
}
|
||||
|
||||
export function formatCommit(oid: string): string {
|
||||
return oid.substring(0, 7);
|
||||
}
|
||||
@@ -158,7 +194,7 @@ export const formatTimestamp = (
|
||||
// Check whether the input is a Radicle ID.
|
||||
export function isRadicleId(input: string): boolean {
|
||||
if (window.HEARTWOOD) {
|
||||
return /^rad:[a-zA-Z0-9]+$/.test(input);
|
||||
return /^(did:key:)?[a-zA-Z0-9]+$/.test(input);
|
||||
} else {
|
||||
return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input);
|
||||
}
|
||||
@@ -226,7 +262,7 @@ export function isMarkdownPath(path: string): boolean {
|
||||
export function isDomain(input: string): boolean {
|
||||
return (
|
||||
(/^[a-z][a-z0-9.-]+$/.test(input) && /\.[a-z]+$/.test(input)) ||
|
||||
(!import.meta.env.PROD && /^0.0.0.0$/.test(input))
|
||||
(!import.meta.env.PROD && isLocal(input))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,28 +123,22 @@
|
||||
<span class="txt-bold">{tree.stats.commits}</span>
|
||||
commit(s)
|
||||
</HeaderToggleLabel>
|
||||
{#if project.issues}
|
||||
<HeaderToggleLabel
|
||||
ariaLabel="Issue count"
|
||||
active={activeRoute.params.view.resource === "issues"}
|
||||
disabled={project.issues === 0}
|
||||
clickable={project.issues > 0}
|
||||
on:click={() => toggleContent("issues", false)}>
|
||||
<span class="txt-bold">{project.issues}</span>
|
||||
issue(s)
|
||||
</HeaderToggleLabel>
|
||||
{/if}
|
||||
{#if project.patches}
|
||||
<HeaderToggleLabel
|
||||
ariaLabel="Patch count"
|
||||
clickable={project.patches > 0}
|
||||
active={activeRoute.params.view.resource === "patches"}
|
||||
disabled={project.patches === 0}
|
||||
on:click={() => toggleContent("patches", false)}>
|
||||
<span class="txt-bold">{project.patches}</span>
|
||||
patch(es)
|
||||
</HeaderToggleLabel>
|
||||
{/if}
|
||||
<HeaderToggleLabel
|
||||
ariaLabel="Issue count"
|
||||
active={activeRoute.params.view.resource === "issues"}
|
||||
clickable
|
||||
on:click={() => toggleContent("issues", false)}>
|
||||
<span class="txt-bold">{project.issues.open ?? 0}</span>
|
||||
issue(s)
|
||||
</HeaderToggleLabel>
|
||||
<HeaderToggleLabel
|
||||
ariaLabel="Patch count"
|
||||
clickable
|
||||
active={activeRoute.params.view.resource === "patches"}
|
||||
on:click={() => toggleContent("patches", false)}>
|
||||
<span class="txt-bold">{project.patches.proposed ?? 0}</span>
|
||||
patch(es)
|
||||
</HeaderToggleLabel>
|
||||
<HeaderToggleLabel ariaLabel="Contributor count">
|
||||
<span class="txt-bold">{tree.stats.contributors}</span>
|
||||
contributor(s)
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import type { Issue } from "@app/lib/issue";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Avatar from "@app/components/Comment/Avatar.svelte";
|
||||
import Chip from "@app/components/Chip.svelte";
|
||||
import Comment from "@app/components/Comment.svelte";
|
||||
import { canonicalize, capitalize } from "@app/lib/utils";
|
||||
import { formatNodeId, canonicalize, capitalize } from "@app/lib/utils";
|
||||
import { formatObjectId } from "@app/lib/cobs";
|
||||
|
||||
export let issue: Issue;
|
||||
@@ -51,41 +53,35 @@
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.metadata-section-body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.metadata-section-empty {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.label {
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-tertiary);
|
||||
background-color: var(--color-tertiary-2);
|
||||
padding: 0.25rem 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.summary-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.summary-title {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.id {
|
||||
flex: 1 0 auto;
|
||||
font-size: var(--font-size-tiny);
|
||||
margin-left: 0.75rem;
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.summary-state {
|
||||
margin-left: 2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
@@ -100,25 +96,37 @@
|
||||
.replies {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.assignee {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.tag {
|
||||
max-width: 15rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.issue {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.summary-state {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="issue">
|
||||
<header>
|
||||
<div class="summary">
|
||||
<div class="summary-left">
|
||||
<span class="summary-title txt-medium">
|
||||
{issue.title}
|
||||
</span>
|
||||
<span class="txt-monospace id layout-desktop">{issue.id}</span>
|
||||
<span class="txt-monospace id layout-mobile">
|
||||
{formatObjectId(issue.id)}
|
||||
</span>
|
||||
<div class="summary-title txt-medium">
|
||||
{issue.title}
|
||||
</div>
|
||||
<div class="txt-monospace id layout-desktop">{issue.id}</div>
|
||||
<div class="txt-monospace id layout-mobile">
|
||||
{formatObjectId(issue.id)}
|
||||
</div>
|
||||
<div
|
||||
class="summary-state"
|
||||
@@ -130,7 +138,7 @@
|
||||
<Authorship
|
||||
author={issue.author}
|
||||
timestamp={issue.timestamp}
|
||||
caption="opened on" />
|
||||
caption="opened" />
|
||||
</header>
|
||||
<main>
|
||||
<div class="comments">
|
||||
@@ -153,14 +161,29 @@
|
||||
</div>
|
||||
<div class="metadata layout-desktop">
|
||||
<div class="metadata-section">
|
||||
<div class="metadata-section-header">Assignees</div>
|
||||
<div class="metadata-section-body">
|
||||
{#if issue.assignees?.length}
|
||||
{#each issue.assignees as assignee, key}
|
||||
<Chip {key}>
|
||||
<div slot="text" class="assignee">
|
||||
<Avatar inline source={assignee} title={assignee} />
|
||||
<span>{formatNodeId(assignee)}</span>
|
||||
</div>
|
||||
</Chip>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="metadata-section-empty">No assignees</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="metadata-section-header">Tags</div>
|
||||
<div class="metadata-section-body">
|
||||
{#if issue.tags?.length}
|
||||
{#each issue.tags as tag}
|
||||
<span class="label">{tag}</span>
|
||||
{#each issue.tags as tag, key}
|
||||
<Chip {key}><span class="tag" slot="text">{tag}</span></Chip>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="metadata-section-empty">No tags.</div>
|
||||
<div class="metadata-section-empty">No tags</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import type { Issue } from "@app/lib/issue";
|
||||
|
||||
import { formatObjectId } from "@app/lib/cobs";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
export let issue: Issue;
|
||||
|
||||
@@ -13,11 +12,10 @@
|
||||
|
||||
<style>
|
||||
.issue-teaser {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-foreground-1);
|
||||
display: grid;
|
||||
grid-template-columns: 3rem minmax(0, 1fr) 4rem;
|
||||
padding: 0.75rem 0;
|
||||
background-color: var(--color-foreground-1);
|
||||
}
|
||||
.issue-teaser:hover {
|
||||
background-color: var(--color-foreground-2);
|
||||
@@ -29,27 +27,33 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
flex: min-content;
|
||||
}
|
||||
.column-right {
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 1rem;
|
||||
flex-basis: 5rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
.issue-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.comment-count {
|
||||
color: var(--color-foreground-4);
|
||||
font-weight: var(--font-weight-bold);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.comment-count .emoji {
|
||||
margin-right: 0.25rem;
|
||||
|
||||
.column-right {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 0 1rem;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
}
|
||||
.state-icon {
|
||||
width: 0.5rem;
|
||||
@@ -62,27 +66,6 @@
|
||||
.closed {
|
||||
background-color: var(--color-negative);
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.column-left {
|
||||
overflow: hidden;
|
||||
}
|
||||
.summary {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="issue-teaser">
|
||||
@@ -94,8 +77,7 @@
|
||||
</div>
|
||||
<div class="column-left">
|
||||
<div class="summary">
|
||||
<!-- TODO: Truncation not working on overflow -->
|
||||
{issue.title}
|
||||
<span class="issue-title">{issue.title}</span>
|
||||
<span class="issue-id">{formatObjectId(issue.id)}</span>
|
||||
</div>
|
||||
<Authorship
|
||||
@@ -106,7 +88,7 @@
|
||||
{#if commentCount > 0}
|
||||
<div class="column-right">
|
||||
<div class="comment-count">
|
||||
<span class="txt-tiny emoji" use:twemoji>💬</span>
|
||||
<Icon name="chat" />
|
||||
<span>{commentCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { MaybeBlob, Project } from "@app/lib/project";
|
||||
import type { Session } from "@app/lib/session";
|
||||
|
||||
import * as modal from "@app/lib/modal";
|
||||
import AuthenticationErrorModal from "@app/views/session/AuthenticationErrorModal.svelte";
|
||||
import Avatar from "@app/components/Comment/Avatar.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Chip from "@app/components/Chip.svelte";
|
||||
import Comment from "@app/components/Comment.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
import Textarea from "@app/components/Textarea.svelte";
|
||||
import { Issue } from "@app/lib/issue";
|
||||
import { canonicalize, formatNodeId, parseNid } from "@app/lib/utils";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let session: Session;
|
||||
export let project: Project;
|
||||
|
||||
const dispatch = createEventDispatcher<{ create: never }>();
|
||||
|
||||
// Get an image blob based on a relative path.
|
||||
const getImage = async (imagePath: string): Promise<MaybeBlob> => {
|
||||
const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
|
||||
const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
|
||||
return project.getBlob(commit, finalPath);
|
||||
};
|
||||
|
||||
let preview: boolean = false;
|
||||
|
||||
function handleAddAssignee() {
|
||||
const nid = parseNid(assignee);
|
||||
if (nid) {
|
||||
if (assignees.includes(nid.pubkey)) {
|
||||
assigneeCaption = "This user is already assigned";
|
||||
return;
|
||||
}
|
||||
assignees.push(nid.pubkey);
|
||||
assignees = assignees;
|
||||
assignee = "";
|
||||
} else {
|
||||
assigneeCaption = "This user is not valid";
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddTag() {
|
||||
if (tags.includes(tag)) {
|
||||
tagCaption = "This tag already exists";
|
||||
return;
|
||||
}
|
||||
tags.push(tag);
|
||||
tags = tags;
|
||||
tag = "";
|
||||
}
|
||||
|
||||
let issueTitle = "";
|
||||
let issueText = "";
|
||||
let assignees: string[] = [];
|
||||
let assignee: string = "";
|
||||
let assigneeCaption: string | undefined = undefined;
|
||||
let tags: string[] = [];
|
||||
let tag: string = "";
|
||||
let tagCaption: string | undefined = undefined;
|
||||
|
||||
async function createIssue() {
|
||||
try {
|
||||
await Issue.createIssue(
|
||||
project.id,
|
||||
issueTitle,
|
||||
issueText,
|
||||
assignees,
|
||||
tags,
|
||||
project.seed.addr,
|
||||
session.id,
|
||||
);
|
||||
dispatch("create");
|
||||
} catch {
|
||||
modal.show({
|
||||
component: AuthenticationErrorModal,
|
||||
props: {
|
||||
title: "Authentication failed",
|
||||
subtitle: [
|
||||
"Could not create the issue. Make sure you're still logged in.",
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
padding: 0 2rem 0 8rem;
|
||||
}
|
||||
.form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) 1fr;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.side-bar {
|
||||
border-left: 1px solid var(--color-foreground-4);
|
||||
padding-left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.actions {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.editor {
|
||||
flex: 2;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.section:not(:first-child) {
|
||||
border-top: 1px solid var(--color-foreground-4);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.summary {
|
||||
padding: 1rem;
|
||||
background: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tags {
|
||||
flex-direction: row;
|
||||
}
|
||||
.tag {
|
||||
max-width: 15rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.assignee {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
main {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div class="form">
|
||||
<div class="editor">
|
||||
{#if preview}
|
||||
<div class="summary txt-medium" class:txt-missing={preview}>
|
||||
{#if issueTitle.trim() === ""}
|
||||
No title
|
||||
{:else}
|
||||
{issueTitle}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="comments">
|
||||
<Comment
|
||||
comment={{
|
||||
author: { id: session.publicKey },
|
||||
body: issueText,
|
||||
reactions: {},
|
||||
replies: null,
|
||||
replyTo: null,
|
||||
timestamp: Date.now(),
|
||||
}}
|
||||
{getImage} />
|
||||
</div>
|
||||
{:else}
|
||||
<Textarea bind:value={issueTitle} placeholder="Title" />
|
||||
<Textarea
|
||||
bind:value={issueText}
|
||||
resizable
|
||||
placeholder="Leave a comment" />
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
on:click={() => (preview = !preview)}>
|
||||
{#if preview}
|
||||
Resume editing
|
||||
{:else}
|
||||
Preview
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!issueTitle}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
on:click={createIssue}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-bar layout-desktop">
|
||||
<div class="section txt-small">
|
||||
<span class="txt-bold">Assignees</span>
|
||||
<div class="chips">
|
||||
{#each assignees as assignee, key}
|
||||
<Chip
|
||||
{key}
|
||||
removeable
|
||||
on:remove={e => {
|
||||
assignees.splice(e.detail, 1);
|
||||
assignees = assignees;
|
||||
}}>
|
||||
<div slot="text" class="assignee">
|
||||
<Avatar inline source={assignee} title={assignee} />
|
||||
<span>{formatNodeId(assignee)}</span>
|
||||
</div>
|
||||
</Chip>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
bind:value={assignee}
|
||||
variant="form"
|
||||
on:submit={handleAddAssignee}
|
||||
on:input={() => (assigneeCaption = undefined)}
|
||||
placeholder="Assign this issue"
|
||||
validationMessage={assigneeCaption}
|
||||
valid={Boolean(parseNid(assignee))} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section txt-small">
|
||||
<span class="txt-bold">Tags</span>
|
||||
<div class="chips tags">
|
||||
{#each tags as tag, key}
|
||||
<Chip
|
||||
{key}
|
||||
removeable
|
||||
on:remove={e => {
|
||||
tags.splice(e.detail, 1);
|
||||
tags = tags;
|
||||
}}>
|
||||
<span class="tag" slot="text">{tag}</span>
|
||||
</Chip>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
bind:value={tag}
|
||||
variant="form"
|
||||
on:input={() => (tagCaption = undefined)}
|
||||
on:submit={handleAddTag}
|
||||
placeholder="Tag this issue"
|
||||
validationMessage={tagCaption}
|
||||
valid={tag !== ""} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3,19 +3,23 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Project } from "@app/lib/project";
|
||||
import type { Issue } from "@app/lib/issue";
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import { capitalize } from "@app/lib/utils";
|
||||
import { groupIssues } from "@app/lib/issue";
|
||||
import * as router from "@app/lib/router";
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
|
||||
import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
|
||||
import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import TabBar from "@app/components/TabBar.svelte";
|
||||
|
||||
export let issues: Issue[];
|
||||
export let state: State;
|
||||
export let project: Project;
|
||||
|
||||
let options: Tab<State>[];
|
||||
const { open, closed } = groupIssues(issues);
|
||||
@@ -28,11 +32,11 @@
|
||||
$: options = [
|
||||
{
|
||||
value: "open",
|
||||
count: open.length,
|
||||
count: project.issues.open,
|
||||
},
|
||||
{
|
||||
value: "closed",
|
||||
count: closed.length,
|
||||
count: project.issues.closed,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -49,6 +53,12 @@
|
||||
.teaser:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--color-background);
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.issues {
|
||||
@@ -58,14 +68,29 @@
|
||||
</style>
|
||||
|
||||
<div class="issues">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e =>
|
||||
<div class="section-header">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e =>
|
||||
router.updateProjectRoute({
|
||||
search: `state=${e.detail}`,
|
||||
})}
|
||||
active={state} />
|
||||
</div>
|
||||
<HeaderToggleLabel
|
||||
disabled={!$sessionStore}
|
||||
on:click={() => {
|
||||
router.updateProjectRoute({
|
||||
search: `state=${e.detail}`,
|
||||
})}
|
||||
active={state} />
|
||||
view: {
|
||||
resource: "issues",
|
||||
params: { view: { resource: "new" } },
|
||||
},
|
||||
});
|
||||
}}
|
||||
clickable>
|
||||
New issue
|
||||
</HeaderToggleLabel>
|
||||
</div>
|
||||
|
||||
{#if filteredIssues.length}
|
||||
@@ -87,7 +112,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍣">
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(state)} issues</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
|
||||
@@ -41,15 +41,11 @@
|
||||
{#if element.type === TimelineType.Merge && element.inner.peer.person}
|
||||
<div class="element">
|
||||
<Authorship
|
||||
author={{
|
||||
peer: element.inner.peer.id,
|
||||
id: element.inner.peer.person.id,
|
||||
profile: element.inner.peer.person,
|
||||
}}
|
||||
author={{ id: element.inner.peer.person.id }}
|
||||
caption={`merged to ${formatSeedId(element.inner.peer.id)}`}
|
||||
timestamp={element.timestamp} />
|
||||
</div>
|
||||
{:else if element.type === TimelineType.Review && element.inner.author.profile?.ens?.name}
|
||||
{:else if element.type === TimelineType.Review && element.inner.author.id}
|
||||
<div class="margin-left">
|
||||
<Review review={element.inner} {getImage} />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@app/lib/patch";
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
import type { Project } from "@app/lib/project";
|
||||
|
||||
import PatchTeaser from "./Patch/PatchTeaser.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
@@ -16,6 +17,7 @@
|
||||
|
||||
export let state: State;
|
||||
export let patches: Patch[];
|
||||
export let project: Project;
|
||||
|
||||
let options: Tab<State>[];
|
||||
const sortedPatches = groupPatches(patches);
|
||||
@@ -24,15 +26,15 @@
|
||||
$: options = [
|
||||
{
|
||||
value: "proposed",
|
||||
count: sortedPatches.proposed.length,
|
||||
count: project.patches.proposed,
|
||||
},
|
||||
{
|
||||
value: "draft",
|
||||
count: sortedPatches.draft.length,
|
||||
count: project.patches.draft,
|
||||
},
|
||||
{
|
||||
value: "archived",
|
||||
count: sortedPatches.archived.length,
|
||||
count: project.patches.archived,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -84,7 +86,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍖">
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(state)} patches</div>
|
||||
<div slot="body">No patches matched the current filter</div>
|
||||
</Placeholder>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Dropdown from "@app/components/Dropdown.svelte";
|
||||
import { formatSeedId } from "@app/lib/utils";
|
||||
import type { Peer } from "@app/lib/project";
|
||||
import Floating from "@app/components/Floating.svelte";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import Dropdown from "@app/components/Dropdown.svelte";
|
||||
import Floating from "@app/components/Floating.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { formatNodeId } from "@app/lib/utils";
|
||||
|
||||
export let peer: string | null = null;
|
||||
export let peers: Peer[];
|
||||
@@ -90,7 +91,7 @@
|
||||
<Icon name="fork" />
|
||||
{#if meta}
|
||||
<span class="peer-id">
|
||||
{meta.person?.name ?? formatSeedId(meta.id)}
|
||||
{meta.person?.name ?? formatNodeId(meta.id)}
|
||||
</span>
|
||||
{#if meta.delegate}
|
||||
<Badge variant="primary">delegate</Badge>
|
||||
@@ -98,7 +99,7 @@
|
||||
<!-- If the delegate metadata is not found -->
|
||||
{:else if peer}
|
||||
<span class="peer-id">
|
||||
{formatSeedId(peer)}
|
||||
{formatNodeId(peer)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,19 +9,21 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import NotFound from "@app/components/NotFound.svelte";
|
||||
import { formatSeedId, unreachable } from "@app/lib/utils";
|
||||
import { formatNodeId, unreachable } from "@app/lib/utils";
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
|
||||
import Header from "./Header.svelte";
|
||||
import Browser from "./Browser.svelte";
|
||||
import History from "./History.svelte";
|
||||
import Commit from "./Commit.svelte";
|
||||
import Issues from "./Issues.svelte";
|
||||
import Header from "./Header.svelte";
|
||||
import History from "./History.svelte";
|
||||
import Issue from "./Issue.svelte";
|
||||
import Patches from "./Patches.svelte";
|
||||
import Patch from "./Patch.svelte";
|
||||
import ProjectMeta from "./ProjectMeta.svelte";
|
||||
import Issues from "./Issues.svelte";
|
||||
import Message from "@app/components/Message.svelte";
|
||||
import NewIssue from "./Issue/New.svelte";
|
||||
import Patch from "./Patch.svelte";
|
||||
import Patches from "./Patches.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import ProjectMeta from "./ProjectMeta.svelte";
|
||||
|
||||
export let activeRoute: ProjectRoute;
|
||||
|
||||
@@ -60,6 +62,24 @@
|
||||
return project;
|
||||
};
|
||||
|
||||
const handleIssueCreation = () => {
|
||||
router.push({
|
||||
resource: "projects",
|
||||
params: {
|
||||
id,
|
||||
seed,
|
||||
view: {
|
||||
resource: "issues",
|
||||
},
|
||||
},
|
||||
});
|
||||
// This assignment allows us to have an up-to-date issue count
|
||||
projectPromise = getProject(id, seed, peer);
|
||||
};
|
||||
|
||||
// React to peer changes
|
||||
$: projectPromise = getProject(id, seed, peer);
|
||||
|
||||
// Content can be altered in child components.
|
||||
$: revision = activeRoute.params.revision || null;
|
||||
</script>
|
||||
@@ -92,7 +112,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
{#await getProject(id, seed, peer)}
|
||||
{#await projectPromise}
|
||||
<main>
|
||||
<header>
|
||||
<Loading center />
|
||||
@@ -128,11 +148,25 @@
|
||||
<Message error>{e.message}</Message>
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "issues" && activeRoute.params.view.params?.view.resource === "new"}
|
||||
{#if $sessionStore}
|
||||
<NewIssue
|
||||
on:create={handleIssueCreation}
|
||||
session={$sessionStore}
|
||||
{project} />
|
||||
{:else}
|
||||
<div class="message">
|
||||
<Message error>
|
||||
Could not access the issue creation. Make sure you're still logged
|
||||
in.
|
||||
</Message>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if activeRoute.params.view.resource === "issues"}
|
||||
{#await issue.Issue.getIssues(project.id, project.seed.addr)}
|
||||
<Loading center />
|
||||
{:then issues}
|
||||
<Issues state={issueFilter} {issues} />
|
||||
<Issues {project} state={issueFilter} {issues} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
@@ -152,7 +186,7 @@
|
||||
{#await patch.Patch.getPatches(project.id, project.seed.addr)}
|
||||
<Loading center />
|
||||
{:then patches}
|
||||
<Patches state={patchFilter} {patches} />
|
||||
<Patches {project} state={patchFilter} {patches} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
@@ -176,7 +210,7 @@
|
||||
{#if peer}
|
||||
<Placeholder emoji="🍂">
|
||||
<span slot="title">
|
||||
<span class="txt-monospace">{formatSeedId(peer)}</span>
|
||||
<span class="txt-monospace">{formatNodeId(peer)}</span>
|
||||
</span>
|
||||
<span slot="body">
|
||||
<span style="display: block">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import {
|
||||
formatSeedAddress,
|
||||
formatSeedId,
|
||||
formatNodeId,
|
||||
formatSeedHost,
|
||||
twemoji,
|
||||
} from "@app/lib/utils";
|
||||
@@ -51,7 +51,7 @@
|
||||
resource: "seeds",
|
||||
params: { host: formatSeedHost(seedHost) },
|
||||
}}>
|
||||
<span class="txt-link">{formatSeedId(seed.id)}@{seed.host}</span>
|
||||
<span class="txt-link">{formatNodeId(seed.id)}@{seed.host}</span>
|
||||
</Link>
|
||||
</span>
|
||||
<span class="txt-faded">:{port}</span>
|
||||
|
||||
@@ -260,7 +260,7 @@ test("peer and branch switching", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=alice").click();
|
||||
@@ -329,7 +329,7 @@ test("peer and branch switching", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob").click();
|
||||
|
||||
@@ -16,7 +16,7 @@ test("peer and branch switching", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=alice hybg18").click();
|
||||
@@ -69,7 +69,7 @@ test("peer and branch switching", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob hyyzz9").click();
|
||||
@@ -112,7 +112,7 @@ test("verified badge", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob hyyzz9").click();
|
||||
@@ -146,7 +146,7 @@ test("relative timestamps", async ({ page }) => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
const latestCommit = page.locator(".commit-teaser").first();
|
||||
await expect(latestCommit).toContainText("Bob Belcher committed now");
|
||||
|
||||
@@ -11,11 +11,15 @@ describe("Format functions", () => {
|
||||
|
||||
test.each([
|
||||
{
|
||||
id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
|
||||
expected: "hydkkk…coygh1",
|
||||
id: "did:key:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
expected: "did:key:z6Mkmz…md6fz6",
|
||||
},
|
||||
])("formatSeedId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.formatSeedId(id)).toEqual(expected);
|
||||
{
|
||||
id: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
expected: "did:key:z6Mkmz…md6fz6",
|
||||
},
|
||||
])("formatNodeId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.formatNodeId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("formatRadicleId", () => {
|
||||
@@ -97,13 +101,39 @@ describe("String Assertions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Strings", () => {
|
||||
describe("Parse Functions", () => {
|
||||
test.each([
|
||||
{ input: "https://twitter.com/cloudhead", expected: "cloudhead" },
|
||||
{ input: "sebastinez", expected: "sebastinez" },
|
||||
])("parseUsername", ({ input, expected }) => {
|
||||
expect(utils.parseUsername(input)).toEqual(expected);
|
||||
});
|
||||
test.each([
|
||||
{
|
||||
input: "rad:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
input: "did:key:z6Mkmz…md6fz6",
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
input: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
expected: {
|
||||
prefix: "did:key:",
|
||||
pubkey: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "did:key:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
expected: {
|
||||
prefix: "did:key:",
|
||||
pubkey: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
},
|
||||
},
|
||||
])("parseNid", ({ input, expected }) => {
|
||||
expect(utils.parseNid(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Path Manipulation", () => {
|
||||
|
||||
Reference in New Issue
Block a user