Restyle and move issue/patch state buttons

This commit is contained in:
Rūdolfs Ošiņš
2025-04-04 10:26:56 +02:00
parent 5845bf3cd1
commit 82560dbb0b
6 changed files with 154 additions and 187 deletions
-22
View File
@@ -1,22 +0,0 @@
<script lang="ts">
import type { Issue } from "@bindings/cob/issue/Issue";
import capitalize from "lodash/capitalize";
import { issueStatusColor } from "@app/lib/utils";
interface Props {
state: Issue["state"];
}
const { state }: Props = $props();
</script>
<div
class="global-counter txt-small"
style:width="fit-content"
style:color="var(--color-foreground-match-background)"
style:background-color={issueStatusColor[state.status]}>
{capitalize(state.status)}{state.status === "closed"
? ` as ${state.reason}`
: ""}
</div>
+68 -65
View File
@@ -1,88 +1,91 @@
<script lang="ts">
import type { State } from "@bindings/cob/issue/State";
import capitalize from "lodash/capitalize";
import isEqual from "lodash/isEqual";
import { closeFocused } from "@app/components/Popover.svelte";
import { issueStatusBackgroundColor, issueStatusColor } from "@app/lib/utils";
import Border from "@app/components/Border.svelte";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownListItem.svelte";
import Icon from "@app/components/Icon.svelte";
import Popover from "@app/components/Popover.svelte";
const {
save,
issueState,
}: {
save: (state: State) => Promise<void>;
issueState: State;
} = $props();
interface Props {
selectedState: State;
onSelect: (selectedStatus: State) => void;
}
const actions: { caption: string; state: State }[] = [
{ caption: "Reopen", state: { status: "open" } },
{
caption: "Close as solved",
state: { status: "closed", reason: "solved" },
},
{ caption: "Close as other", state: { status: "closed", reason: "other" } },
];
let selectedAction = $state(
issueState.status === "open" ? actions[1] : actions[0],
);
// React to state changes that come from outside of this button.
$effect(() => {
selectedAction = issueState.status === "open" ? actions[1] : actions[0];
});
const { selectedState, onSelect }: Props = $props();
</script>
<style>
.main {
button {
cursor: pointer;
border: 0;
background: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: var(--font-size-small);
}
.badge {
gap: 6px;
padding-right: 10px;
}
</style>
<div class="main">
<Button
styleHeight="2.5rem"
variant="secondary"
flatRight
onclick={() => void save($state.snapshot(selectedAction["state"]))}>
{selectedAction["caption"]}
</Button>
<Popover
popoverPadding="0"
popoverPositionTop="3rem"
popoverPositionRight="0">
{#snippet toggle(onclick)}
<Button styleHeight="2.5rem" flatLeft {onclick} variant="secondary">
<Popover popoverPadding="0" popoverPositionTop="2rem" popoverPositionLeft="0">
{#snippet toggle(onclick)}
<button {onclick}>
<span
class="global-counter badge"
style:color={issueStatusColor[selectedState.status]}
style:background-color={issueStatusBackgroundColor[
selectedState.status
]}>
<Icon
name={selectedState.status === "open"
? "issue"
: `issue-${selectedState.status}`} />
{capitalize(selectedState.status)}
{selectedState.status === "closed" ? `as ${selectedState.reason}` : ""}
<Icon name="chevron-down" />
</Button>
{/snippet}
{#snippet popover()}
<Border variant="ghost">
<DropdownList
items={actions.filter(a => !isEqual(a.state, issueState))}>
{#snippet item(action)}
<DropdownListItem
styleGap="0.5rem"
styleMinHeight="2.5rem"
selected={isEqual(selectedAction, action)}
onclick={() => {
selectedAction = action;
closeFocused();
}}>
{action.caption}
</DropdownListItem>
{/snippet}
</DropdownList>
</Border>
{/snippet}
</Popover>
</div>
</span>
</button>
{/snippet}
{#snippet popover()}
<Border variant="ghost">
<DropdownList
items={[
{ status: "open" },
{ status: "closed", reason: "solved" },
{ status: "closed", reason: "other" },
] as State[]}>
{#snippet item(state)}
<DropdownListItem
selected={isEqual(selectedState, state)}
onclick={() => {
onSelect(state);
closeFocused();
}}>
<span
class="global-flex"
style:color={issueStatusColor[state.status]}>
<Icon
name={state.status === "open"
? "issue"
: `issue-${state.status}`} />
{capitalize(state.status)}
{state.status === "closed" ? `as ${state.reason}` : ""}
</span>
</DropdownListItem>
{/snippet}
</DropdownList>
</Border>
{/snippet}
</Popover>
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import type { Patch } from "@bindings/cob/patch/Patch";
import capitalize from "lodash/capitalize";
import { patchStatusColor } from "@app/lib/utils";
interface Props {
state: Patch["state"];
}
const { state }: Props = $props();
</script>
<div
class="global-counter txt-small"
style:width="fit-content"
style:color="var(--color-foreground-match-background)"
style:background-color={patchStatusColor[state.status]}>
{capitalize(state.status)}
</div>
+66 -67
View File
@@ -1,89 +1,88 @@
<script lang="ts">
import type { State } from "@bindings/cob/patch/State";
import isEqual from "lodash/isEqual";
import capitalize from "lodash/capitalize";
import { closeFocused } from "@app/components/Popover.svelte";
import { patchStatusBackgroundColor, patchStatusColor } from "@app/lib/utils";
import Border from "@app/components/Border.svelte";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownListItem.svelte";
import Icon from "@app/components/Icon.svelte";
import Popover from "@app/components/Popover.svelte";
const {
save,
patchState,
}: {
save: (state: State) => Promise<void>;
patchState: State;
} = $props();
interface Props {
selectedState: State;
onSelect: (newState: State) => void;
}
const actions: { caption: string; state: State }[] = [
{
caption: "Reopen",
state: { status: "open" },
},
{ caption: "Convert to draft", state: { status: "draft" } },
{ caption: "Archive", state: { status: "archived" } },
];
let selectedAction = $state(
patchState.status === "open" ? actions[1] : actions[0],
);
// React to state changes that come from outside of this button.
$effect(() => {
selectedAction = patchState.status === "open" ? actions[1] : actions[0];
});
const { selectedState, onSelect }: Props = $props();
</script>
<style>
.main {
button {
cursor: pointer;
border: 0;
background: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: var(--font-size-small);
}
.badge {
gap: 6px;
padding-right: 10px;
}
</style>
<div class="main">
<Button
styleHeight="2.5rem"
variant="secondary"
flatRight
onclick={() =>
void save($state.snapshot(selectedAction["state"]) as State)}>
{selectedAction["caption"]}
</Button>
<Popover
popoverPadding="0"
popoverPositionTop="3rem"
popoverPositionRight="0">
{#snippet toggle(onclick)}
<Button styleHeight="2.5rem" flatLeft {onclick} variant="secondary">
<Popover popoverPadding="0" popoverPositionTop="2rem" popoverPositionLeft="0">
{#snippet toggle(onclick)}
<button {onclick}>
<span
class="global-counter badge"
style:color={patchStatusColor[selectedState.status]}
style:background-color={patchStatusBackgroundColor[
selectedState.status
]}>
<Icon
name={selectedState.status === "open"
? "patch"
: `patch-${selectedState.status}`} />
{capitalize(selectedState.status)}
<Icon name="chevron-down" />
</Button>
{/snippet}
{#snippet popover()}
<Border variant="ghost">
<DropdownList
items={actions.filter(a => !isEqual(a.state, patchState))}>
{#snippet item(action)}
<DropdownListItem
styleGap="0.5rem"
styleMinHeight="2.5rem"
selected={isEqual(selectedAction, action)}
onclick={() => {
selectedAction = action;
closeFocused();
}}>
{action.caption}
</DropdownListItem>
{/snippet}
</DropdownList>
</Border>
{/snippet}
</Popover>
</div>
</span>
</button>
{/snippet}
{#snippet popover()}
<Border variant="ghost">
<DropdownList
items={[
{ status: "open" },
{ status: "draft" },
{ status: "archived" },
] as State[]}>
{#snippet item(state)}
<DropdownListItem
selected={selectedState.status === state.status}
onclick={() => {
onSelect(state);
closeFocused();
}}>
<span
class="global-flex"
style:color={patchStatusColor[state.status]}>
<Icon
name={state.status === "open"
? "patch"
: `patch-${state.status}`} />
{capitalize(state.status)}
</span>
</DropdownListItem>
{/snippet}
</DropdownList>
</Border>
{/snippet}
</Popover>
+8 -4
View File
@@ -30,7 +30,6 @@
import Icon from "@app/components/Icon.svelte";
import InlineTitle from "@app/components/InlineTitle.svelte";
import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
import IssueStateBadge from "@app/components/IssueStateBadge.svelte";
import IssueStateButton from "@app/components/IssueStateButton.svelte";
import IssueTimeline from "@app/components/IssueTimeline.svelte";
import LabelInput from "@app/components/LabelInput.svelte";
@@ -410,7 +409,6 @@
updatedTitle = issue.title;
editingTitle = !editingTitle;
}} />
<IssueStateButton issueState={issue.state} save={saveState} />
</div>
</div>
{:else}
@@ -433,7 +431,6 @@
{#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), issue.body.author.did, )}
<div class="title-icons">
<Icon name="pen" onclick={() => (editingTitle = !editingTitle)} />
<IssueStateButton issueState={issue.state} save={saveState} />
</div>
{/if}
</div>
@@ -443,7 +440,14 @@
<Border variant="ghost" styleGap="0">
<div class="metadata-section" style:min-width="8rem">
<div class="metadata-section-title">Status</div>
<IssueStateBadge state={issue.state} />
<IssueStateButton
selectedState={issue.state}
onSelect={newState => {
void saveState(newState);
if (status !== "all" && newState.status !== status) {
void loadIssues("all");
}
}} />
</div>
<div class="metadata-divider"></div>
+12 -9
View File
@@ -38,7 +38,6 @@
import Link from "@app/components/Link.svelte";
import NakedButton from "@app/components/NakedButton.svelte";
import OutlineButton from "@app/components/OutlineButton.svelte";
import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
import PatchStateButton from "@app/components/PatchStateButton.svelte";
import PatchTeaser from "@app/components/PatchTeaser.svelte";
import PatchTimeline from "@app/components/PatchTimeline.svelte";
@@ -174,20 +173,17 @@
}
}
async function saveState(state: Patch["state"]) {
async function saveState(newState: Patch["state"]) {
try {
await invoke("edit_patch", {
rid: repo.rid,
cobId: patch.id,
action: {
type: "lifecycle",
state,
state: newState,
},
opts: { announce: $nodeRunning && $announce },
});
if (initialStatus !== undefined) {
status = state["status"];
}
} catch (error) {
console.error("Changing state failed", error);
} finally {
@@ -343,6 +339,7 @@
flex-direction: column;
align-items: flex-start;
height: 100%;
z-index: 20;
}
.metadata-section-title {
margin-bottom: 0.5rem;
@@ -594,7 +591,6 @@
updatedTitle = patch.title;
editingTitle = !editingTitle;
}} />
<PatchStateButton patchState={patch.state} save={saveState} />
</div>
</div>
{:else}
@@ -618,7 +614,6 @@
<Icon
name="pen"
onclick={() => (editingTitle = !editingTitle)} />
<PatchStateButton patchState={patch.state} save={saveState} />
</div>
{/if}
</div>
@@ -627,7 +622,15 @@
<Border variant="ghost" styleGap="0">
<div class="metadata-section" style:min-width="8rem">
<div class="metadata-section-title">Status</div>
<PatchStateBadge state={patch.state} />
<PatchStateButton
selectedState={patch.state}
onSelect={newState => {
void saveState(newState);
if (status !== undefined && newState.status !== status) {
status = undefined;
void loadPatches(status);
}
}} />
</div>
<div class="metadata-divider"></div>