Skip to content

Commit

Permalink
Implement assignee editing
Browse files Browse the repository at this point in the history
  • Loading branch information
rudolfs committed Dec 5, 2024
1 parent 132fc66 commit da594a4
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 13 deletions.
183 changes: 183 additions & 0 deletions src/components/AssigneeInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script lang="ts">
import type { Author } from "@bindings/cob/Author";
import { authorForNodeId, parseNodeId } from "@app/lib/utils";
import Icon from "@app/components/Icon.svelte";
import NodeId from "@app/components/NodeId.svelte";
import TextInput from "@app/components/TextInput.svelte";
interface Props {
allowedToEdit: boolean;
assignees: Author[];
submitInProgress: boolean;
save: (updatedAssignees: string[]) => void;
}
const {
allowedToEdit = false,
assignees,
submitInProgress = false,
save,
}: Props = $props();
let updatedAssignees: Author[] = $state([]);
let showInput: boolean = $state(false);
let inputValue = $state("");
let validationMessage: string | undefined = $state(undefined);
let valid: boolean = $state(false);
let assignee: string | undefined = undefined;
let removeToggles: Record<string, boolean> = $state({});
$effect(() => {
// Reset component state whenever the assignees change in the parent. This
// happens when the issue ID changes for example when the user navigates
// to a different issue via the sidebar.
updatedAssignees = assignees;
showInput = false;
validationMessage = undefined;
valid = true;
removeToggles = {};
});
$effect(() => {
if (inputValue === "") {
validationMessage = "";
valid = true;
} else {
const parsedNodeId = parseNodeId(inputValue);
if (parsedNodeId) {
assignee = `${parsedNodeId.prefix}${parsedNodeId.pubkey}`;
if (updatedAssignees.find(({ did }) => did === assignee)) {
validationMessage = "This assignee is already added";
valid = false;
} else {
validationMessage = undefined;
valid = true;
}
} else {
validationMessage = "This assignee is not valid";
valid = false;
}
}
});
function addAssignee() {
if (valid && assignee) {
updatedAssignees = [...updatedAssignees, { did: assignee }];
inputValue = "";
save($state.snapshot(updatedAssignees.map(x => x.did)));
showInput = false;
}
}
function removeAssignee(assignee: Author) {
updatedAssignees = updatedAssignees.filter(
({ did }) => did !== assignee.did,
);
save($state.snapshot(updatedAssignees.map(x => x.did)));
showInput = false;
}
</script>

<style>
.header {
font-size: var(--font-size-small);
margin-bottom: 0.5rem;
color: var(--color-foreground-dim);
}
.body {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
gap: 0.5rem;
font-size: var(--font-size-small);
}
.validation-message {
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--color-foreground-red);
position: relative;
margin-top: 0.5rem;
}
button {
border: 0;
cursor: pointer;
gap: 0.5rem;
background-color: transparent;
border: none;
display: flex;
color: var(--color-foreground-default);
}
</style>

<div style:width="100%">
<div class="global-flex" style:align-items="flex-start">
<div class="header">Assignees</div>

{#if allowedToEdit}
<div class="global-flex" style:margin-left="auto">
{#if showInput}
<Icon onclick={addAssignee} name="checkmark" styleCursor="pointer" />
<Icon
onclick={() => {
inputValue = "";
showInput = false;
}}
name="cross"
styleCursor="pointer" />
{:else}
<Icon
name="plus"
onclick={() => (showInput = true)}
styleCursor="pointer"></Icon>
{/if}
</div>
{/if}
</div>

<div class="body">
{#if allowedToEdit}
{#each updatedAssignees as assignee}
<button
class="txt-small"
onclick={() =>
(removeToggles[assignee.did] = !removeToggles[assignee.did])}>
<NodeId {...authorForNodeId(assignee)} />
{#if removeToggles[assignee.did]}
<Icon name="cross" onclick={() => removeAssignee(assignee)} />
{/if}
</button>
{:else}
<div class="txt-missing">Not assigned to anyone.</div>
{/each}
{:else}
{#each updatedAssignees as assignee}
<NodeId {...authorForNodeId(assignee)} />
{:else}
<div class="txt-missing">Not assigned to anyone.</div>
{/each}
{/if}
</div>

{#if showInput}
<div style:margin-top="0.5rem">
<TextInput
autofocus
{valid}
disabled={submitInProgress}
placeholder="Add assignee"
bind:value={inputValue}
onSubmit={addAssignee} />
{#if !valid && validationMessage}
<div class="validation-message">
<Icon name="warning" />{validationMessage}
</div>
{/if}
</div>
{/if}
</div>
24 changes: 24 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,27 @@ export function isMac() {
export function modifierKey() {
return isMac() ? "⌘" : "ctrl";
}

export function parseNodeId(
nid: string,
): { prefix: string; pubkey: string } | undefined {
const match = /^(did:key:)?(z[a-zA-Z0-9]+)$/.exec(nid);
if (match) {
let hex: Uint8Array | undefined = undefined;
try {
hex = bs58.decode(match[2].substring(1));
} catch (error) {
console.error("utils.parseNodId: Not able to decode received NID", error);
return undefined;
}
// This checks also that the first 2 bytes are equal
// to the ed25519 public key type used.
if (hex && !(hex.byteLength === 34 && hex[0] === 0xed && hex[1] === 1)) {
return undefined;
}

return { prefix: match[1] || "did:key:", pubkey: match[2] };
}

return undefined;
}
45 changes: 32 additions & 13 deletions src/views/repo/Issue.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
import * as roles from "@app/lib/roles";
import { invoke } from "@app/lib/invoke";
import {
publicKeyFromDid,
scrollIntoView,
authorForNodeId,
} from "@app/lib/utils";
import { publicKeyFromDid, scrollIntoView } from "@app/lib/utils";
import { announce } from "@app/components/AnnounceSwitch.svelte";
Expand All @@ -38,6 +34,7 @@
import Layout from "./Layout.svelte";
import Sidebar from "@app/components/Sidebar.svelte";
import AssigneeInput from "@app/components/AssigneeInput.svelte";
interface Props {
repo: RepoInfo;
Expand Down Expand Up @@ -65,6 +62,7 @@
let editingTitle = $state(false);
let updatedTitle = $state("");
let labelSaveInProgress: boolean = $state(false);
let assigneesSaveInProgress: boolean = $state(false);
$effect(() => {
// The component doesn't get destroyed when we switch between different
Expand Down Expand Up @@ -102,6 +100,26 @@
}
}
async function saveAssignees(assignees: string[]) {
try {
assigneesSaveInProgress = true;
await invoke("edit_issue", {
rid: repo.rid,
cobId: issue.id,
action: {
type: "assign",
assignees,
},
opts: { announce: $announce },
});
} catch (error) {
console.error("Editing assignees failed", error);
} finally {
assigneesSaveInProgress = false;
await reload();
}
}
async function toggleReply() {
topLevelReplyOpen = !topLevelReplyOpen;
if (!topLevelReplyOpen) {
Expand Down Expand Up @@ -457,14 +475,15 @@
<div class="metadata-divider"></div>

<div class="metadata-section" style:flex="1">
<div class="metadata-section-title">Assignees</div>
<div class="global-flex" style:flex-wrap="wrap">
{#each issue.assignees as assignee}
<NodeId {...authorForNodeId(assignee)} />
{:else}
<span class="txt-missing">Not assigned to anyone.</span>
{/each}
</div>
<AssigneeInput
allowedToEdit={!!roles.isDelegateOrAuthor(
config.publicKey,
repo.delegates.map(delegate => delegate.did),
issue.body.author.did,
)}
assignees={issue.assignees}
submitInProgress={assigneesSaveInProgress}
save={saveAssignees} />
</div>
</Border>

Expand Down

0 comments on commit da594a4

Please sign in to comment.