mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 18:49:11 +03:00
commit
5a3d788927
@ -20,3 +20,8 @@ export function featureBranchStacking(): Persisted<boolean> {
|
||||
const key = 'branchStacking';
|
||||
return persisted(false, key);
|
||||
}
|
||||
|
||||
export function featureTopics(): Persisted<boolean> {
|
||||
const key = 'feature--topics';
|
||||
return persisted(false, key);
|
||||
}
|
||||
|
@ -36,6 +36,10 @@ export class AzureDevOps implements GitHost {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
issueService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService() {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -40,6 +40,10 @@ export class BitBucket implements GitHost {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
issueService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService() {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { GitHubBranch } from './githubBranch';
|
||||
import { GitHubChecksMonitor } from './githubChecksMonitor';
|
||||
import { GitHubListingService } from './githubListingService';
|
||||
import { GitHubPrService } from './githubPrService';
|
||||
import { GitHubIssueService } from '$lib/gitHost/github/issueService';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
@ -64,6 +65,13 @@ export class GitHub implements GitHost {
|
||||
);
|
||||
}
|
||||
|
||||
issueService() {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
}
|
||||
return new GitHubIssueService(this.octokit, this.repo);
|
||||
}
|
||||
|
||||
checksMonitor(sourceBranch: string) {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
|
29
apps/desktop/src/lib/gitHost/github/issueService.ts
Normal file
29
apps/desktop/src/lib/gitHost/github/issueService.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
export class GitHubIssueService implements GitHostIssueService {
|
||||
constructor(
|
||||
private octokit: Octokit,
|
||||
private repository: RepoInfo
|
||||
) {}
|
||||
|
||||
async create(title: string, body: string, labels: string[]): Promise<void> {
|
||||
await this.octokit.rest.issues.create({
|
||||
repo: this.repository.name,
|
||||
owner: this.repository.owner,
|
||||
title,
|
||||
body,
|
||||
labels
|
||||
});
|
||||
}
|
||||
|
||||
async listLabels(): Promise<string[]> {
|
||||
return (
|
||||
await this.octokit.rest.issues.listLabelsForRepo({
|
||||
repo: this.repository.name,
|
||||
owner: this.repository.owner
|
||||
})
|
||||
).data.map((label) => label.name);
|
||||
}
|
||||
}
|
@ -41,6 +41,10 @@ export class GitLab implements GitHost {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
issueService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService() {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { buildContextStore } from '$lib/utils/context';
|
||||
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
|
||||
import type { GitHostBranch } from './gitHostBranch';
|
||||
import type { GitHostChecksMonitor } from './gitHostChecksMonitor';
|
||||
import type { GitHostListingService } from './gitHostListingService';
|
||||
@ -8,6 +9,8 @@ export interface GitHost {
|
||||
// Lists PRs for the repo.
|
||||
listService(): GitHostListingService | undefined;
|
||||
|
||||
issueService(): GitHostIssueService | undefined;
|
||||
|
||||
// Detailed information about a specific PR.
|
||||
prService(): GitHostPrService | undefined;
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
export interface GitHostIssueService {
|
||||
create(title: string, body: string, labels: string[]): Promise<void>;
|
||||
listLabels(): Promise<string[]>;
|
||||
}
|
@ -6,8 +6,10 @@
|
||||
import WorkspaceButton from './WorkspaceButton.svelte';
|
||||
import Resizer from '../shared/Resizer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { featureTopics } from '$lib/config/uiFeatureFlags';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import EditButton from '$lib/navigation/EditButton.svelte';
|
||||
import TopicsButton from '$lib/navigation/TopicsButton.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { platformName } from '$lib/platform/platform';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
@ -43,6 +45,8 @@
|
||||
|
||||
const modeService = getContext(ModeService);
|
||||
const mode = modeService.mode;
|
||||
|
||||
const topicsEnabled = featureTopics();
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
@ -120,6 +124,10 @@
|
||||
{:else if $mode?.type === 'Edit'}
|
||||
<EditButton href={`/${project.id}/edit`} isNavCollapsed={$isNavCollapsed} />
|
||||
{/if}
|
||||
|
||||
{#if $topicsEnabled}
|
||||
<TopicsButton href={`/${project.id}/topics`} isNavCollapsed={$isNavCollapsed} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
34
apps/desktop/src/lib/navigation/TopicsButton.svelte
Normal file
34
apps/desktop/src/lib/navigation/TopicsButton.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import DomainButton from '$lib/navigation/DomainButton.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
isNavCollapsed: boolean;
|
||||
}
|
||||
|
||||
const { href, isNavCollapsed }: Props = $props();
|
||||
const label = 'Topics';
|
||||
</script>
|
||||
|
||||
<DomainButton
|
||||
isSelected={$page.url.pathname === href}
|
||||
{isNavCollapsed}
|
||||
tooltipLabel={label}
|
||||
onmousedown={async () => await goto(href)}
|
||||
>
|
||||
<img class="icon" src="/images/domain-icons/working-branches.svg" alt="" />
|
||||
{#if !isNavCollapsed}
|
||||
<span class="text-14 text-semibold" class:collapsed-txt={isNavCollapsed}>{label}</span>
|
||||
{/if}
|
||||
</DomainButton>
|
||||
|
||||
<style lang="postcss">
|
||||
.icon {
|
||||
border-radius: var(--radius-s);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
149
apps/desktop/src/lib/topics/CreateIssueModal.svelte
Normal file
149
apps/desktop/src/lib/topics/CreateIssueModal.svelte
Normal file
@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { TopicService, type Topic } from '$lib/topics/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { createKeybind } from '$lib/utils/hotkeys';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
|
||||
interface Props {
|
||||
registerKeypress?: boolean;
|
||||
topic?: Topic;
|
||||
}
|
||||
|
||||
const { registerKeypress = false, topic }: Props = $props();
|
||||
|
||||
const gitHost = getGitHost();
|
||||
const issueService = $derived($gitHost?.issueService());
|
||||
const topicService = getContext(TopicService);
|
||||
|
||||
let modal = $state<Modal>();
|
||||
let chooseLabelModal = $state<Modal>();
|
||||
|
||||
let availables = $state<string[]>([]);
|
||||
let labels = $state<string[]>([]);
|
||||
|
||||
let title = $state(topic?.title || '');
|
||||
let body = $state(topic?.body || '');
|
||||
|
||||
$effect(() => {
|
||||
issueService?.listLabels().then((labels) => {
|
||||
availables = labels;
|
||||
});
|
||||
});
|
||||
|
||||
let submitProgress = $state<'inert' | 'loading' | 'complete'>('inert');
|
||||
|
||||
async function submit() {
|
||||
submitProgress = 'loading';
|
||||
issueService?.create(title, body, labels);
|
||||
if (topic) {
|
||||
const updatedTopic = { ...topic, hasIssue: true };
|
||||
topicService.update(updatedTopic);
|
||||
} else {
|
||||
topicService.create(title, body, true);
|
||||
}
|
||||
submitProgress = 'complete';
|
||||
|
||||
modal?.close();
|
||||
}
|
||||
|
||||
export function open() {
|
||||
title = topic?.title || '';
|
||||
body = topic?.body || '';
|
||||
labels = [];
|
||||
submitProgress = 'inert';
|
||||
|
||||
modal?.show();
|
||||
}
|
||||
|
||||
let handleKeyDown = $state(() => {});
|
||||
|
||||
$effect(() => {
|
||||
if (registerKeypress) {
|
||||
handleKeyDown = createKeybind({
|
||||
'$mod+i': open
|
||||
});
|
||||
} else {
|
||||
handleKeyDown = () => {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
{#if issueService}
|
||||
<Modal bind:this={modal}>
|
||||
<h2 class="text-18 text-bold">Create an issue</h2>
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Title</p>
|
||||
<TextBox bind:value={title} />
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Body</p>
|
||||
<TextArea bind:value={body} />
|
||||
</div>
|
||||
|
||||
<div class="labels">
|
||||
{#each labels as label}
|
||||
<Button onclick={() => (labels = labels.filter((l) => l !== label))} size="tag"
|
||||
>{label}</Button
|
||||
>
|
||||
{/each}
|
||||
|
||||
<Modal bind:this={chooseLabelModal} width="small">
|
||||
<div class="availables">
|
||||
{#each availables.filter((label) => !labels.includes(label)) as label}
|
||||
<Button
|
||||
onclick={() => {
|
||||
labels.push(label);
|
||||
chooseLabelModal?.close();
|
||||
}}
|
||||
size="tag">{label}</Button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</Modal>
|
||||
<Button icon="plus-small" size="tag" onclick={() => chooseLabelModal?.show()}
|
||||
>Add Label</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#snippet controls()}
|
||||
<Button onclick={() => modal?.close()}>Cancel</Button>
|
||||
<Button kind="solid" style="pop" onclick={submit} loading={submitProgress === 'loading'}
|
||||
>Submit</Button
|
||||
>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
margin-top: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.availables {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
85
apps/desktop/src/lib/topics/CreateTopicModal.svelte
Normal file
85
apps/desktop/src/lib/topics/CreateTopicModal.svelte
Normal file
@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { TopicService } from '$lib/topics/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { createKeybind } from '$lib/utils/hotkeys';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
|
||||
interface Props {
|
||||
registerKeypress?: boolean;
|
||||
}
|
||||
|
||||
const { registerKeypress = false }: Props = $props();
|
||||
|
||||
const topicService = getContext(TopicService);
|
||||
|
||||
let modal = $state<Modal>();
|
||||
|
||||
let title = $state('');
|
||||
let body = $state('');
|
||||
|
||||
let submitProgress = $state<'inert' | 'loading' | 'complete'>('inert');
|
||||
|
||||
async function submit() {
|
||||
submitProgress = 'loading';
|
||||
topicService.create(title, body);
|
||||
submitProgress = 'complete';
|
||||
|
||||
modal?.close();
|
||||
}
|
||||
|
||||
export function open() {
|
||||
title = '';
|
||||
body = '';
|
||||
submitProgress = 'inert';
|
||||
|
||||
modal?.show();
|
||||
}
|
||||
|
||||
let handleKeyDown = $state(() => {});
|
||||
|
||||
$effect(() => {
|
||||
if (registerKeypress) {
|
||||
handleKeyDown = createKeybind({
|
||||
'$mod+k': open
|
||||
});
|
||||
} else {
|
||||
handleKeyDown = () => {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<h2 class="text-18 text-bold">Create an topic</h2>
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Title</p>
|
||||
<TextBox bind:value={title} />
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Body</p>
|
||||
<TextArea bind:value={body} />
|
||||
</div>
|
||||
|
||||
{#snippet controls()}
|
||||
<Button onclick={() => modal?.close()}>Cancel</Button>
|
||||
<Button kind="solid" style="pop" onclick={submit} loading={submitProgress === 'loading'}
|
||||
>Submit</Button
|
||||
>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<style lang="postcss">
|
||||
.input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
116
apps/desktop/src/lib/topics/Topic.svelte
Normal file
116
apps/desktop/src/lib/topics/Topic.svelte
Normal file
@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import CreateIssueModal from '$lib/topics/CreateIssueModal.svelte';
|
||||
import { TopicService, type Topic } from '$lib/topics/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
|
||||
interface Props {
|
||||
topic: Topic;
|
||||
}
|
||||
|
||||
const { topic }: Props = $props();
|
||||
|
||||
const topicService = getContext(TopicService);
|
||||
|
||||
let deleteModal = $state<Modal>();
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
let createIssueModal = $state<CreateIssueModal>();
|
||||
</script>
|
||||
|
||||
<CreateIssueModal bind:this={createIssueModal} {topic} />
|
||||
|
||||
<Modal bind:this={deleteModal} width="small">
|
||||
<p>Are you sure you want to delete this topic?</p>
|
||||
{#snippet controls()}
|
||||
<Button onclick={() => deleteModal?.close()}>Cancel</Button>
|
||||
<Button
|
||||
onclick={() => {
|
||||
topicService.remove(topic);
|
||||
deleteModal?.close();
|
||||
}}
|
||||
kind="solid"
|
||||
style="error">Delete</Button
|
||||
>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<div class="topic">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="header" onclick={() => (expanded = !expanded)}>
|
||||
<p class="text-14 text-bold title">{topic.title}</p>
|
||||
|
||||
<div class="header__details">
|
||||
{#if topic.hasIssue}
|
||||
<Button size="tag" clickable={false}>Has Issue</Button>
|
||||
{/if}
|
||||
|
||||
{#if expanded}
|
||||
<Icon name="chevron-down" />
|
||||
{:else}
|
||||
<Icon name="chevron-up" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
<div class="footer">
|
||||
<p class="text-14">{topic.body}</p>
|
||||
<div class="footer__actions">
|
||||
{#if !topic.hasIssue}
|
||||
<Button onclick={() => createIssueModal?.open()}>Convert to issue</Button>
|
||||
{/if}
|
||||
<Button icon="bin" style="error" onclick={() => deleteModal?.show()} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.topic {
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header__details {
|
||||
display: flex;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--clr-border-3);
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer__actions {
|
||||
display: flex;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
51
apps/desktop/src/lib/topics/service.ts
Normal file
51
apps/desktop/src/lib/topics/service.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
|
||||
|
||||
export type Topic = {
|
||||
title: string;
|
||||
body: string;
|
||||
hasIssue: boolean;
|
||||
createdAt: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export class TopicService {
|
||||
topics = persisted<Topic[]>([], this.localStorageKey);
|
||||
|
||||
constructor(
|
||||
private project: Project,
|
||||
private issueService: Readable<GitHostIssueService | undefined>
|
||||
) {}
|
||||
|
||||
private get localStorageKey(): string {
|
||||
return `TopicService--${this.project.id}`;
|
||||
}
|
||||
|
||||
create(title: string, body: string, hasIssue: boolean = false): Topic {
|
||||
const topic = {
|
||||
title,
|
||||
body,
|
||||
hasIssue,
|
||||
createdAt: Date.now(),
|
||||
id: crypto.randomUUID()
|
||||
};
|
||||
|
||||
this.topics.set([topic, ...get(this.topics)]);
|
||||
|
||||
return topic;
|
||||
}
|
||||
|
||||
update(topic: Topic) {
|
||||
const filteredTopics = get(this.topics).filter((storedTopic) => storedTopic.id !== topic.id);
|
||||
|
||||
this.topics.set([topic, ...filteredTopics]);
|
||||
}
|
||||
|
||||
remove(topic: Topic) {
|
||||
const filteredTopics = get(this.topics).filter((storedTopic) => storedTopic.id !== topic.id);
|
||||
|
||||
this.topics.set(filteredTopics);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
gitHostPullRequestTemplatePath,
|
||||
gitHostUsePullRequestTemplate
|
||||
} from '$lib/config/config';
|
||||
import { featureTopics } from '$lib/config/uiFeatureFlags';
|
||||
import { ReorderDropzoneManagerFactory } from '$lib/dragging/reorderDropzoneManager';
|
||||
import { DefaultGitHostFactory } from '$lib/gitHost/gitHostFactory';
|
||||
import { octokitFromAccessToken } from '$lib/gitHost/github/octokit';
|
||||
@ -28,12 +29,16 @@
|
||||
import Navigation from '$lib/navigation/Navigation.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import CreateIssueModal from '$lib/topics/CreateIssueModal.svelte';
|
||||
import CreateTopicModal from '$lib/topics/CreateTopicModal.svelte';
|
||||
import { TopicService } from '$lib/topics/service';
|
||||
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
|
||||
import { parseRemoteUrl } from '$lib/url/gitUrl';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import { onDestroy, setContext, type Snippet } from 'svelte';
|
||||
import { derived as storeDerived } from 'svelte/store';
|
||||
import type { LayoutData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
@ -93,6 +98,14 @@
|
||||
const listServiceStore = createGitHostListingServiceStore(undefined);
|
||||
const gitHostStore = createGitHostStore(undefined);
|
||||
const branchServiceStore = createBranchServiceStore(undefined);
|
||||
const gitHostIssueSerice = storeDerived(gitHostStore, (gitHostStore) =>
|
||||
gitHostStore?.issueService()
|
||||
);
|
||||
|
||||
$effect.pre(() => {
|
||||
const topicService = new TopicService(project, gitHostIssueSerice);
|
||||
setContext(TopicService, topicService);
|
||||
});
|
||||
|
||||
$effect.pre(() => {
|
||||
const combinedBranchListingService = new CombinedBranchListingService(
|
||||
@ -172,8 +185,17 @@
|
||||
onDestroy(() => {
|
||||
clearFetchInterval();
|
||||
});
|
||||
|
||||
const topicsEnabled = featureTopics();
|
||||
</script>
|
||||
|
||||
{#if $topicsEnabled}
|
||||
{#if $gitHostStore?.issueService()}
|
||||
<CreateIssueModal registerKeypress />
|
||||
{/if}
|
||||
<CreateTopicModal registerKeypress />
|
||||
{/if}
|
||||
|
||||
<!-- forces components to be recreated when projectId changes -->
|
||||
{#key projectId}
|
||||
<ProjectSettingsMenuAction
|
||||
|
59
apps/desktop/src/routes/[projectId]/topics/+page.svelte
Normal file
59
apps/desktop/src/routes/[projectId]/topics/+page.svelte
Normal file
@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import CreateIssueModal from '$lib/topics/CreateIssueModal.svelte';
|
||||
import CreateTopicModal from '$lib/topics/CreateTopicModal.svelte';
|
||||
import Topic from '$lib/topics/Topic.svelte';
|
||||
import { TopicService } from '$lib/topics/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
|
||||
const topicService = getContext(TopicService);
|
||||
const topics = topicService.topics;
|
||||
|
||||
const sortedTopics = $derived.by(() => {
|
||||
const clonedTopics = structuredClone($topics);
|
||||
clonedTopics.sort((a, b) => b.createdAt - a.createdAt);
|
||||
|
||||
return clonedTopics;
|
||||
});
|
||||
|
||||
let createTopicModal = $state<CreateTopicModal>();
|
||||
let createIssueModal = $state<CreateIssueModal>();
|
||||
</script>
|
||||
|
||||
<CreateTopicModal bind:this={createTopicModal} />
|
||||
<CreateIssueModal bind:this={createIssueModal} />
|
||||
|
||||
<SettingsPage title="Topics">
|
||||
<div>
|
||||
<div class="topic__actions">
|
||||
<Button kind="solid" style="pop" onclick={() => createTopicModal?.open()}>Create Topic</Button
|
||||
>
|
||||
<Button style="pop" onclick={() => createIssueModal?.open()}>Create Issue</Button>
|
||||
</div>
|
||||
<div class="container">
|
||||
{#each sortedTopics as topic}
|
||||
<Topic {topic} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsPage>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
width: 100%;
|
||||
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-l);
|
||||
|
||||
border: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
.topic__actions {
|
||||
display: flex;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
@ -3,7 +3,8 @@
|
||||
import {
|
||||
featureBaseBranchSwitching,
|
||||
featureInlineUnifiedDiffs,
|
||||
featureBranchStacking
|
||||
featureBranchStacking,
|
||||
featureTopics
|
||||
} from '$lib/config/uiFeatureFlags';
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
@ -11,6 +12,7 @@
|
||||
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||
const inlineUnifiedDiffs = featureInlineUnifiedDiffs();
|
||||
const branchStacking = featureBranchStacking();
|
||||
const topicsEnabled = featureTopics();
|
||||
</script>
|
||||
|
||||
<SettingsPage title="Experimental features">
|
||||
@ -60,6 +62,20 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
<SectionCard labelFor="topics" orientation="row">
|
||||
<svelte:fragment slot="title">Topics</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
A highly experimental form of note taking / conversation. The form & function may change
|
||||
drastically, and may result in lost notes.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="topics"
|
||||
checked={$topicsEnabled}
|
||||
on:click={() => ($topicsEnabled = !$topicsEnabled)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
</SettingsPage>
|
||||
|
||||
<style>
|
||||
|
Loading…
Reference in New Issue
Block a user