mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-24 05:29:51 +03:00
Merge pull request #5220 from gitbutlerapp/pr-modal-template-selector
PR details modal: Allow user to choose the PR template
This commit is contained in:
commit
a55e2fcee3
@ -8,12 +8,12 @@
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
|
||||
|
||||
interface Props {
|
||||
@ -157,7 +157,7 @@
|
||||
<ContextMenuItem label="Allow rebasing" onclick={toggleAllowRebasing}>
|
||||
{#snippet control()}
|
||||
<Tooltip text={'Allows changing commits after push\n(force push needed)'}>
|
||||
<Toggle small bind:checked={allowRebasing} on:click={toggleAllowRebasing} />
|
||||
<Toggle small bind:checked={allowRebasing} onclick={toggleAllowRebasing} />
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
</ContextMenuItem>
|
||||
|
@ -2,9 +2,9 @@
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Checkbox from '@gitbutler/ui/Checkbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import type { Writable, Readable } from 'svelte/store';
|
||||
|
||||
export let filtersActive: Readable<boolean>;
|
||||
|
@ -8,12 +8,12 @@
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import GithubIntegration from '$lib/settings/GithubIntegration.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { unique } from '$lib/utils/array';
|
||||
import { getBestBranch, getBestRemote, getBranchRemoteFromRef } from '$lib/utils/branch';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { RemoteBranchInfo } from '$lib/baseBranch/baseBranchService';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -178,7 +178,7 @@
|
||||
bind:this={aiGenCheckbox}
|
||||
checked={$aiGenEnabled}
|
||||
id="aiGenEnabled"
|
||||
on:change={() => {
|
||||
onclick={() => {
|
||||
$aiGenEnabled = !$aiGenEnabled;
|
||||
}}
|
||||
/>
|
||||
|
@ -7,13 +7,16 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import PrDetailsModalHeader from './PrDetailsModalHeader.svelte';
|
||||
import PrTemplateSection from './PrTemplateSection.svelte';
|
||||
import { getPreferredPRAction, PRAction } from './pr';
|
||||
import { AIService } from '$lib/ai/service';
|
||||
import { ForgeService } from '$lib/backend/forge';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import Markdown from '$lib/components/Markdown.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
@ -22,8 +25,8 @@
|
||||
import { showError, showToast } from '$lib/notifications/toasts';
|
||||
import { isFailure } from '$lib/result';
|
||||
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
|
||||
import DropDownButton from '$lib/shared/DropDownButton.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getBranchNameFromRef } from '$lib/utils/branch';
|
||||
import { KeyName, onMetaEnter } from '$lib/utils/hotkeys';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
@ -35,8 +38,7 @@
|
||||
import BorderlessTextarea from '@gitbutler/ui/BorderlessTextarea.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Segment from '@gitbutler/ui/segmentControl/Segment.svelte';
|
||||
import SegmentControl from '@gitbutler/ui/segmentControl/SegmentControl.svelte';
|
||||
import ToggleButton from '@gitbutler/ui/ToggleButton.svelte';
|
||||
import { tick } from 'svelte';
|
||||
import type { DetailedPullRequest, PullRequest } from '$lib/gitHost/interface/types';
|
||||
|
||||
@ -69,7 +71,6 @@
|
||||
const branchStore = getContextStore(VirtualBranch);
|
||||
const branchController = getContext(BranchController);
|
||||
const baseBranchService = getContext(BaseBranchService);
|
||||
const forgeService = getContext(ForgeService);
|
||||
const gitListService = getGitHostListingService();
|
||||
const prService = getGitHostPrService();
|
||||
const aiService = getContext(AIService);
|
||||
@ -84,10 +85,12 @@
|
||||
props.type === 'preview-series' ? props.upstreamName : branch.upstreamName
|
||||
);
|
||||
const baseBranchName = $derived($baseBranch.shortName);
|
||||
const prTemplatePath = $derived(project.git_host.reviewTemplatePath);
|
||||
|
||||
let createPrDropDown = $state<ReturnType<typeof DropDownButton>>();
|
||||
let isDraft = $state<boolean>($preferredPRAction === PRAction.CreateDraft);
|
||||
|
||||
let modal = $state<ReturnType<typeof Modal>>();
|
||||
let templateSelector = $state<ReturnType<typeof PrTemplateSection>>();
|
||||
let isEditing = $state<boolean>(true);
|
||||
let isLoading = $state<boolean>(false);
|
||||
let pullRequestTemplateBody = $state<string | undefined>(undefined);
|
||||
@ -96,6 +99,12 @@
|
||||
let aiDescriptionDirective = $state<string | undefined>(undefined);
|
||||
let showAiBox = $state<boolean>(false);
|
||||
|
||||
async function handleToggleUseTemplate() {
|
||||
if (!templateSelector) return;
|
||||
const displaying = templateSelector.imports.showing;
|
||||
await templateSelector.setUsePullRequestTemplate(!displaying);
|
||||
}
|
||||
|
||||
const canUseAI = $derived.by(() => {
|
||||
return aiConfigurationValid || $aiGenEnabled;
|
||||
});
|
||||
@ -127,15 +136,6 @@
|
||||
const actualBody = $derived<string>(inputBody ?? defaultBody);
|
||||
const actualTitle = $derived<string>(inputTitle ?? defaultTitle);
|
||||
|
||||
// Fetch PR template content
|
||||
$effect(() => {
|
||||
if (modal?.imports.open && pullRequestTemplateBody === undefined && prTemplatePath) {
|
||||
forgeService.getReviewTemplateContent(prTemplatePath).then((template) => {
|
||||
pullRequestTemplateBody = template;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (modal?.imports.open) {
|
||||
aiService.validateConfiguration().then((valid) => {
|
||||
@ -216,10 +216,6 @@
|
||||
close();
|
||||
}
|
||||
|
||||
function handleCheckDraft() {
|
||||
isDraft = !isDraft;
|
||||
}
|
||||
|
||||
async function handleAIButtonPressed() {
|
||||
if (props.type === 'display') return;
|
||||
if (!aiGenEnabled) return;
|
||||
@ -316,59 +312,71 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isPreviewOnly = props.type === 'display';
|
||||
const isDisplay = props.type === 'display';
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} width={580} noPadding {onClose} onKeyDown={handleModalKeydown}>
|
||||
<div class="pr-header">
|
||||
{#if !isPreviewOnly}
|
||||
<h3 class="text-14 text-semibold pr-title">
|
||||
{!isEditing ? actualTitle : 'Create a pull request'}
|
||||
</h3>
|
||||
<SegmentControl
|
||||
defaultIndex={isPreviewOnly ? 1 : 0}
|
||||
onselect={(id) => {
|
||||
if (id === 'write') {
|
||||
isEditing = true;
|
||||
} else {
|
||||
isEditing = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Segment id="write">Edit</Segment>
|
||||
<Segment id="preview">Preview</Segment>
|
||||
</SegmentControl>
|
||||
{:else}
|
||||
<h3 class="text-14 text-semibold pr-title">{actualTitle}</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- HEADER -->
|
||||
{#if !isDisplay}
|
||||
<PrDetailsModalHeader {isDisplay} bind:isEditing />
|
||||
{/if}
|
||||
|
||||
<!-- MAIN FIELDS -->
|
||||
<ScrollableContainer wide maxHeight="66vh" onscroll={showBorderOnScroll}>
|
||||
<div class="pr-content">
|
||||
{#if isPreviewOnly || !isEditing}
|
||||
<div class="pr-description-preview">
|
||||
<Markdown content={actualBody} />
|
||||
{#if isDisplay || !isEditing}
|
||||
<div class="pr-preview" class:display={isDisplay} class:preview={!isDisplay}>
|
||||
<h1 class="text-head-22 pr-preview-title">
|
||||
{actualTitle}
|
||||
</h1>
|
||||
{#if actualBody}
|
||||
<div class="pr-description-preview">
|
||||
<Markdown content={actualBody} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pr-fields">
|
||||
<TextBox
|
||||
placeholder="PR title"
|
||||
value={actualTitle}
|
||||
readonly={!isEditing || isPreviewOnly}
|
||||
readonly={!isEditing || isDisplay}
|
||||
on:input={(e) => {
|
||||
inputTitle = e.detail;
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<div class="features-section">
|
||||
<ToggleButton
|
||||
icon="doc"
|
||||
label="Use PR template"
|
||||
checked={!!templateSelector?.imports.showing}
|
||||
onclick={handleToggleUseTemplate}
|
||||
disabled={!templateSelector?.imports.hasTemplates}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="ai-small"
|
||||
label="AI generation"
|
||||
checked={showAiBox}
|
||||
tooltip={!aiConfigurationValid ? 'AI service is not configured' : undefined}
|
||||
disabled={!canUseAI || aiIsLoading}
|
||||
onclick={() => {
|
||||
showAiBox = !showAiBox;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- PR TEMPLATE SELECT -->
|
||||
<PrTemplateSection bind:this={templateSelector} bind:pullRequestTemplateBody />
|
||||
|
||||
<!-- DESCRIPTION FIELD -->
|
||||
<div class="pr-description-field text-input">
|
||||
<BorderlessTextarea
|
||||
value={actualBody}
|
||||
rows={2}
|
||||
autofocus
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
padding={{ top: 12, right: 12, bottom: 12, left: 12 }}
|
||||
placeholder="Add description…"
|
||||
oninput={(e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
@ -377,68 +385,38 @@
|
||||
/>
|
||||
|
||||
<!-- AI GENRATION -->
|
||||
{#if !isPreviewOnly && canUseAI && isEditing}
|
||||
<div class="pr-ai" class:show-ai-box={showAiBox}>
|
||||
{#if showAiBox}
|
||||
<BorderlessTextarea
|
||||
autofocus
|
||||
bind:value={aiDescriptionDirective}
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
placeholder={aiService.prSummaryMainDirective}
|
||||
onkeydown={onMetaEnter(handleAIButtonPressed)}
|
||||
oninput={(e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
aiDescriptionDirective = target.value;
|
||||
}}
|
||||
/>
|
||||
<div class="pr-ai__actions">
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
onclick={() => {
|
||||
showAiBox = false;
|
||||
aiDescriptionDirective = undefined;
|
||||
}}>Hide</Button
|
||||
>
|
||||
<Button
|
||||
style="neutral"
|
||||
kind="solid"
|
||||
icon="ai-small"
|
||||
tooltip={!aiConfigurationValid
|
||||
? 'You must be logged in or have provided your own API key'
|
||||
: !$aiGenEnabled
|
||||
? 'You must have summary generation enabled'
|
||||
: undefined}
|
||||
disabled={!canUseAI || aiIsLoading}
|
||||
isLoading={aiIsLoading}
|
||||
onclick={handleAIButtonPressed}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pr-ai__actions">
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon="ai-small"
|
||||
tooltip={!aiConfigurationValid
|
||||
? 'You must be logged in or have provided your own API key'
|
||||
: !$aiGenEnabled
|
||||
? 'You must have summary generation enabled'
|
||||
: undefined}
|
||||
disabled={!canUseAI || aiIsLoading}
|
||||
isLoading={aiIsLoading}
|
||||
onclick={() => {
|
||||
showAiBox = true;
|
||||
}}
|
||||
>
|
||||
Generate description
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pr-ai" class:show-ai-box={showAiBox}>
|
||||
{#if showAiBox}
|
||||
<BorderlessTextarea
|
||||
autofocus
|
||||
bind:value={aiDescriptionDirective}
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
placeholder={aiService.prSummaryMainDirective}
|
||||
onkeydown={onMetaEnter(handleAIButtonPressed)}
|
||||
oninput={(e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
aiDescriptionDirective = target.value;
|
||||
}}
|
||||
/>
|
||||
<div class="pr-ai__actions">
|
||||
<Button
|
||||
style="neutral"
|
||||
kind="solid"
|
||||
icon="ai-small"
|
||||
tooltip={!aiConfigurationValid
|
||||
? 'You must be logged in or have provided your own API key'
|
||||
: !$aiGenEnabled
|
||||
? 'You must have summary generation enabled'
|
||||
: undefined}
|
||||
disabled={!canUseAI || aiIsLoading}
|
||||
loading={aiIsLoading}
|
||||
onclick={handleAIButtonPressed}
|
||||
>
|
||||
Generate description
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -448,48 +426,61 @@
|
||||
<!-- FOOTER -->
|
||||
|
||||
{#snippet controls(close)}
|
||||
<div class="pr-footer">
|
||||
{#if props.type !== 'display'}
|
||||
<label class="draft-toggle__wrap">
|
||||
<Toggle id="is-draft-toggle" small checked={isDraft} on:click={handleCheckDraft} />
|
||||
<label class="text-12 draft-toggle__label" for="is-draft-toggle">Create as a draft</label>
|
||||
</label>
|
||||
{#if props.type !== 'display'}
|
||||
<Button style="ghost" outline onclick={close}>Cancel</Button>
|
||||
|
||||
<div class="pr-footer__actions">
|
||||
<Button style="ghost" outline onclick={close}>Cancel</Button>
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
disabled={isLoading || aiIsLoading}
|
||||
{isLoading}
|
||||
type="submit"
|
||||
onclick={async () => await handleCreatePR(close)}
|
||||
>{isDraft ? 'Create draft pull request' : 'Create pull request'}</Button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pr-footer__actions">
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon={prLinkCopied ? 'tick-small' : 'copy-small'}
|
||||
disabled={prLinkCopied}
|
||||
onclick={() => {
|
||||
handlePrLinkCopied(props.pr.htmlUrl);
|
||||
}}>{prLinkCopied ? 'Link copied!' : 'Copy PR link'}</Button
|
||||
>
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon="open-link"
|
||||
onclick={() => {
|
||||
openExternalUrl(props.pr.htmlUrl);
|
||||
}}>Open in browser</Button
|
||||
>
|
||||
</div>
|
||||
<Button style="ghost" outline onclick={close}>Close</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<DropDownButton
|
||||
bind:this={createPrDropDown}
|
||||
style="pop"
|
||||
kind="solid"
|
||||
disabled={isLoading || aiIsLoading || !actualTitle}
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
onclick={async () => await handleCreatePR(close)}
|
||||
>
|
||||
{isDraft ? 'Create pull request draft' : 'Create pull request'}
|
||||
|
||||
{#snippet contextMenuSlot()}
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Create pull request"
|
||||
onclick={() => {
|
||||
isDraft = false;
|
||||
createPrDropDown?.close();
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
label="Create draft pull request"
|
||||
onclick={() => {
|
||||
isDraft = true;
|
||||
createPrDropDown?.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
{/snippet}
|
||||
</DropDownButton>
|
||||
{:else}
|
||||
<div class="pr-footer__actions">
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon={prLinkCopied ? 'tick-small' : 'copy-small'}
|
||||
disabled={prLinkCopied}
|
||||
onclick={() => {
|
||||
handlePrLinkCopied(props.pr.htmlUrl);
|
||||
}}>{prLinkCopied ? 'Link copied!' : 'Copy PR link'}</Button
|
||||
>
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon="open-link"
|
||||
onclick={() => {
|
||||
openExternalUrl(props.pr.htmlUrl);
|
||||
}}>Open in browser</Button
|
||||
>
|
||||
</div>
|
||||
<Button style="ghost" outline onclick={close}>Close</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
@ -500,19 +491,12 @@
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.pr-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
/* FIELDS */
|
||||
|
||||
.pr-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.pr-description-field {
|
||||
@ -525,11 +509,6 @@
|
||||
|
||||
/* PREVIEW */
|
||||
|
||||
.pr-title {
|
||||
flex: 1;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pr-description-preview {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
@ -543,37 +522,44 @@
|
||||
}
|
||||
|
||||
.show-ai-box {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--clr-border-3);
|
||||
}
|
||||
|
||||
.pr-ai__actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
|
||||
.pr-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pr-footer__actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.draft-toggle__wrap {
|
||||
.features-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.draft-toggle__label {
|
||||
color: var(--clr-text-2);
|
||||
/* PREVIEW */
|
||||
.pr-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&.display {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
&.preview {
|
||||
padding: 16px;
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
48
apps/desktop/src/lib/pr/PrDetailsModalHeader.svelte
Normal file
48
apps/desktop/src/lib/pr/PrDetailsModalHeader.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import Segment from '@gitbutler/ui/segmentControl/Segment.svelte';
|
||||
import SegmentControl from '@gitbutler/ui/segmentControl/SegmentControl.svelte';
|
||||
|
||||
interface Props {
|
||||
isDisplay: boolean;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
let { isDisplay, isEditing = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- MAIN -->
|
||||
<div class="pr-header">
|
||||
<div class="pr-header__row">
|
||||
<h3 class="text-14 text-body text-semibold pr-title">Create a pull request</h3>
|
||||
|
||||
<SegmentControl
|
||||
defaultIndex={isDisplay ? 1 : 0}
|
||||
onselect={(id) => {
|
||||
isEditing = id === 'write';
|
||||
}}
|
||||
>
|
||||
<Segment id="write">Edit</Segment>
|
||||
<Segment id="preview">Preview</Segment>
|
||||
</SegmentControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pr-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
.pr-header__row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pr-title {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
110
apps/desktop/src/lib/pr/PrTemplateSection.svelte
Normal file
110
apps/desktop/src/lib/pr/PrTemplateSection.svelte
Normal file
@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { ForgeService } from '$lib/backend/forge';
|
||||
import { ProjectService, ProjectsService } from '$lib/backend/projects';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
|
||||
interface Props {
|
||||
pullRequestTemplateBody: string | undefined;
|
||||
}
|
||||
|
||||
let { pullRequestTemplateBody = $bindable() }: Props = $props();
|
||||
|
||||
const projectsService = getContext(ProjectsService);
|
||||
const projectService = getContext(ProjectService);
|
||||
const forgeService = getContext(ForgeService);
|
||||
|
||||
let allAvailableTemplates = $state<{ label: string; value: string }[]>([]);
|
||||
|
||||
const projectStore = projectService.project;
|
||||
const project = $derived($projectStore);
|
||||
const reviewTemplatePath = $derived(project?.git_host.reviewTemplatePath);
|
||||
const show = $derived(!!reviewTemplatePath);
|
||||
|
||||
// Fetch PR template content
|
||||
$effect(() => {
|
||||
if (!project) return;
|
||||
if (reviewTemplatePath) {
|
||||
forgeService.getReviewTemplateContent(reviewTemplatePath).then((template) => {
|
||||
pullRequestTemplateBody = template;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch available PR templates
|
||||
$effect(() => {
|
||||
if (!project) return;
|
||||
forgeService.getAvailableReviewTemplates().then((availableTemplates) => {
|
||||
if (availableTemplates) {
|
||||
allAvailableTemplates = availableTemplates.map((availableTemplate) => {
|
||||
return {
|
||||
label: availableTemplate,
|
||||
value: availableTemplate
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function setPullRequestTemplatePath(value: string) {
|
||||
if (!project) return;
|
||||
project.git_host.reviewTemplatePath = value;
|
||||
await projectsService.updateProject(project);
|
||||
}
|
||||
|
||||
export async function setUsePullRequestTemplate(value: boolean) {
|
||||
if (!project) return;
|
||||
|
||||
setTemplate: {
|
||||
if (!value) {
|
||||
project.git_host.reviewTemplatePath = undefined;
|
||||
pullRequestTemplateBody = undefined;
|
||||
break setTemplate;
|
||||
}
|
||||
|
||||
if (allAvailableTemplates[0]) {
|
||||
project.git_host.reviewTemplatePath = allAvailableTemplates[0].value;
|
||||
break setTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
await projectsService.updateProject(project);
|
||||
}
|
||||
|
||||
export const imports = {
|
||||
get showing() {
|
||||
return show;
|
||||
},
|
||||
get hasTemplates() {
|
||||
return allAvailableTemplates.length > 0;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="pr-template__wrap">
|
||||
<Select
|
||||
value={reviewTemplatePath}
|
||||
options={allAvailableTemplates.map(({ label, value }) => ({ label, value }))}
|
||||
placeholder="No PR templates found ¯\_(ツ)_/¯"
|
||||
flex="1"
|
||||
searchable
|
||||
disabled={allAvailableTemplates.length <= 1}
|
||||
onselect={setPullRequestTemplatePath}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === reviewTemplatePath} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.pr-template__wrap {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
@ -22,6 +22,7 @@
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
wide?: boolean;
|
||||
flex?: string;
|
||||
options: SelectItem<T>[];
|
||||
value?: T;
|
||||
placeholder?: string;
|
||||
@ -38,6 +39,7 @@
|
||||
disabled,
|
||||
loading,
|
||||
wide,
|
||||
flex,
|
||||
options = [],
|
||||
value,
|
||||
placeholder = 'Select an option...',
|
||||
@ -150,7 +152,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="select-wrapper" class:wide bind:this={selectWrapperEl}>
|
||||
<div class="select-wrapper" class:wide bind:this={selectWrapperEl} style:flex>
|
||||
{#if label}
|
||||
<label for={id} class="select__label text-13 text-body text-semibold">{label}</label>
|
||||
{/if}
|
||||
|
@ -6,7 +6,7 @@
|
||||
appNonAnonMetricsEnabled
|
||||
} from '$lib/config/appSettings';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
|
||||
const errorReportingEnabled = appErrorReportingEnabled();
|
||||
const metricsEnabled = appMetricsEnabled();
|
||||
@ -46,7 +46,7 @@
|
||||
<Toggle
|
||||
id="errorReportingToggle"
|
||||
checked={$errorReportingEnabled}
|
||||
on:click={() => ($errorReportingEnabled = !$errorReportingEnabled)}
|
||||
onclick={() => ($errorReportingEnabled = !$errorReportingEnabled)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -58,7 +58,7 @@
|
||||
<Toggle
|
||||
id="metricsEnabledToggle"
|
||||
checked={$metricsEnabled}
|
||||
on:click={() => ($metricsEnabled = !$metricsEnabled)}
|
||||
onclick={() => ($metricsEnabled = !$metricsEnabled)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -72,7 +72,7 @@
|
||||
<Toggle
|
||||
id="nonAnonMetricsEnabledToggle"
|
||||
checked={$nonAnonMetricsEnabled}
|
||||
on:click={() => ($nonAnonMetricsEnabled = !$nonAnonMetricsEnabled)}
|
||||
onclick={() => ($nonAnonMetricsEnabled = !$nonAnonMetricsEnabled)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -6,10 +6,10 @@
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const userService = getContext(UserService);
|
||||
@ -44,7 +44,7 @@
|
||||
<Toggle
|
||||
id="aiGenEnabled"
|
||||
checked={$aiGenEnabled}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
$aiGenEnabled = !$aiGenEnabled;
|
||||
}}
|
||||
/>
|
||||
|
@ -9,9 +9,9 @@
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
GitButler will sign commits as per your git configuration.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle id="signCommits" checked={signCommits} on:click={handleSignCommitsClick} />
|
||||
<Toggle id="signCommits" checked={signCommits} onclick={handleSignCommitsClick} />
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
{#if signCommits}
|
||||
|
@ -6,10 +6,10 @@
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { User } from '$lib/stores/user';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
<Toggle
|
||||
id="historySync"
|
||||
checked={project.api?.sync || false}
|
||||
on:click={async () => await onSyncChange(!project.api?.sync)}
|
||||
onclick={async () => await onSyncChange(!project.api?.sync)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -124,7 +124,7 @@
|
||||
<Toggle
|
||||
id="branchesySync"
|
||||
checked={project.api?.sync_code || false}
|
||||
on:click={async () => await onSyncCodeChange(!project.api?.sync_code)}
|
||||
onclick={async () => await onSyncCodeChange(!project.api?.sync_code)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -4,8 +4,8 @@
|
||||
import { projectRunCommitHooks } from '$lib/config/config';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
|
||||
const projectsService = getContext(ProjectsService);
|
||||
const project = getContext(Project);
|
||||
@ -56,11 +56,7 @@
|
||||
GitButler will never force push to the target branch.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="allowForcePush"
|
||||
checked={allowForcePushing}
|
||||
on:click={handleAllowForcePushClick}
|
||||
/>
|
||||
<Toggle id="allowForcePush" checked={allowForcePushing} onclick={handleAllowForcePushClick} />
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
||||
@ -73,7 +69,7 @@
|
||||
<Toggle
|
||||
id="omitCertificateCheck"
|
||||
checked={omitCertificateCheck}
|
||||
on:click={handleOmitCertificateCheckClick}
|
||||
onclick={handleOmitCertificateCheckClick}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -1,133 +0,0 @@
|
||||
<script lang="ts">
|
||||
import notFoundSvg from '$lib/assets/empty-state/not-found.svg?raw';
|
||||
import { ForgeService } from '$lib/backend/forge';
|
||||
import { ProjectService, ProjectsService } from '$lib/backend/projects';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
|
||||
|
||||
const projectsService = getContext(ProjectsService);
|
||||
const projectService = getContext(ProjectService);
|
||||
const forgeService = getContext(ForgeService);
|
||||
|
||||
const projectStore = projectService.project;
|
||||
const project = $derived($projectStore);
|
||||
const useTemplate = $derived(!!project?.git_host.reviewTemplatePath);
|
||||
const selectedTemplate = $derived(project?.git_host.reviewTemplatePath);
|
||||
let allAvailableTemplates = $state<{ label: string; value: string }[]>([]);
|
||||
let isTemplatesAvailable = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
forgeService.getAvailableReviewTemplates().then((availableTemplates) => {
|
||||
if (availableTemplates) {
|
||||
allAvailableTemplates = availableTemplates.map((availableTemplate) => {
|
||||
return {
|
||||
label: availableTemplate,
|
||||
value: availableTemplate
|
||||
};
|
||||
});
|
||||
|
||||
isTemplatesAvailable = allAvailableTemplates.length > 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function setUsePullRequestTemplate(value: boolean) {
|
||||
if (!project) return;
|
||||
|
||||
setTemplate: {
|
||||
if (!value) {
|
||||
project.git_host.reviewTemplatePath = undefined;
|
||||
break setTemplate;
|
||||
}
|
||||
|
||||
if (allAvailableTemplates[0]) {
|
||||
project.git_host.reviewTemplatePath = allAvailableTemplates[0].value;
|
||||
break setTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
await projectsService.updateProject(project);
|
||||
}
|
||||
|
||||
async function setPullRequestTemplatePath(value: string) {
|
||||
if (!project) return;
|
||||
project.git_host.reviewTemplatePath = value;
|
||||
await projectsService.updateProject(project);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section>
|
||||
<div class="stack-v">
|
||||
<SectionCard
|
||||
roundedBottom={!useTemplate}
|
||||
orientation="row"
|
||||
labelFor="use-pull-request-template-boolean"
|
||||
>
|
||||
<svelte:fragment slot="title">Pull request templates</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="use-pull-request-template-boolean"
|
||||
checked={useTemplate}
|
||||
disabled={!isTemplatesAvailable}
|
||||
on:click={(e) => {
|
||||
setUsePullRequestTemplate(
|
||||
(e.target as MouseEvent['target'] & { checked: boolean }).checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
If enabled, we will use the path below to set the initial body of any pull requested created
|
||||
on this project through GitButler.
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
||||
{#if useTemplate}
|
||||
<SectionCard roundedTop={false} orientation="row">
|
||||
{#if isTemplatesAvailable}
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
options={allAvailableTemplates.map(({ label, value }) => ({ label, value }))}
|
||||
label="Available Templates"
|
||||
wide={true}
|
||||
searchable
|
||||
disabled={allAvailableTemplates.length === 0}
|
||||
onselect={(value) => {
|
||||
setPullRequestTemplatePath(value);
|
||||
}}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedTemplate} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{:else}
|
||||
<EmptyStatePlaceholder image={notFoundSvg} topBottomPadding={20}>
|
||||
{#snippet caption()}
|
||||
No templates found in the project
|
||||
<span class="text-11">
|
||||
<Link
|
||||
href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository"
|
||||
>How to create a template</Link
|
||||
></span
|
||||
>
|
||||
{/snippet}
|
||||
</EmptyStatePlaceholder>
|
||||
{/if}
|
||||
</SectionCard>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
<Spacer />
|
@ -15,6 +15,7 @@
|
||||
loading?: boolean;
|
||||
wide?: boolean;
|
||||
tooltip?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
menuPosition?: 'top' | 'bottom';
|
||||
children: Snippet;
|
||||
contextMenuSlot: Snippet;
|
||||
@ -29,6 +30,7 @@
|
||||
disabled = false,
|
||||
loading = false,
|
||||
wide = false,
|
||||
type,
|
||||
tooltip,
|
||||
menuPosition = 'bottom',
|
||||
children,
|
||||
@ -58,6 +60,7 @@
|
||||
{style}
|
||||
{icon}
|
||||
{kind}
|
||||
{type}
|
||||
{outline}
|
||||
reversedDirection
|
||||
disabled={disabled || loading}
|
||||
|
@ -13,7 +13,6 @@
|
||||
import CommitSigningForm from '$lib/settings/userPreferences/CommitSigningForm.svelte';
|
||||
import DetailsForm from '$lib/settings/userPreferences/DetailsForm.svelte';
|
||||
import PreferencesForm from '$lib/settings/userPreferences/PreferencesForm.svelte';
|
||||
import PullRequestTemplateForm from '$lib/settings/userPreferences/PullRequestTemplateForm.svelte';
|
||||
import RemoveProjectForm from '$lib/settings/userPreferences/RemoveProjectForm.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
|
||||
@ -47,8 +46,6 @@
|
||||
{#if $baseBranchSwitching}
|
||||
<BaseBranchSwitch />
|
||||
{/if}
|
||||
|
||||
<PullRequestTemplateForm />
|
||||
<RemoveProjectForm />
|
||||
</Section>
|
||||
</TabContent>
|
||||
|
@ -14,9 +14,9 @@
|
||||
} from '$lib/settings/userSettings';
|
||||
import RadioButton from '$lib/shared/RadioButton.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { type Hunk } from '$lib/vbranches/types';
|
||||
import { getContextStoreBySymbol } from '@gitbutler/shared/context';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import type { ContentSection } from '$lib/utils/fileSections';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
<Toggle
|
||||
id="allowDiffLigatures"
|
||||
checked={$userSettings.diffLigatures}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
userSettings.update((s) => ({
|
||||
...s,
|
||||
diffLigatures: !$userSettings.diffLigatures
|
||||
@ -199,7 +199,7 @@
|
||||
<Toggle
|
||||
id="wrapText"
|
||||
checked={$userSettings.wrapText}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
userSettings.update((s) => ({
|
||||
...s,
|
||||
wrapText: !s.wrapText
|
||||
@ -219,7 +219,7 @@
|
||||
<Toggle
|
||||
id="inlineUnifiedDiffs"
|
||||
checked={$userSettings.inlineUnifiedDiffs}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
userSettings.update((s) => ({
|
||||
...s,
|
||||
inlineUnifiedDiffs: !s.inlineUnifiedDiffs
|
||||
@ -288,7 +288,7 @@
|
||||
<Toggle
|
||||
id="branchLaneContents"
|
||||
checked={$autoSelectBranchNameFeature}
|
||||
on:click={() => ($autoSelectBranchNameFeature = !$autoSelectBranchNameFeature)}
|
||||
onclick={() => ($autoSelectBranchNameFeature = !$autoSelectBranchNameFeature)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -7,7 +7,7 @@
|
||||
featureTopics
|
||||
} from '$lib/config/uiFeatureFlags';
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
|
||||
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||
const topicsEnabled = featureTopics();
|
||||
@ -30,7 +30,7 @@
|
||||
<Toggle
|
||||
id="baseBranchSwitching"
|
||||
checked={$baseBranchSwitching}
|
||||
on:click={() => ($baseBranchSwitching = !$baseBranchSwitching)}
|
||||
onclick={() => ($baseBranchSwitching = !$baseBranchSwitching)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -45,7 +45,7 @@
|
||||
<Toggle
|
||||
id="stackingFeature"
|
||||
checked={$stackingFeature}
|
||||
on:click={() => ($stackingFeature = !$stackingFeature)}
|
||||
onclick={() => ($stackingFeature = !$stackingFeature)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -64,7 +64,7 @@
|
||||
<Toggle
|
||||
id="stackingFeatureMultipleSeries"
|
||||
checked={$stackingFeatureMultipleSeries}
|
||||
on:click={() => ($stackingFeatureMultipleSeries = !$stackingFeatureMultipleSeries)}
|
||||
onclick={() => ($stackingFeatureMultipleSeries = !$stackingFeatureMultipleSeries)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -79,7 +79,7 @@
|
||||
<Toggle
|
||||
id="topics"
|
||||
checked={$topicsEnabled}
|
||||
on:click={() => ($topicsEnabled = !$topicsEnabled)}
|
||||
onclick={() => ($topicsEnabled = !$topicsEnabled)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -3,8 +3,8 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const gitConfig = getContext(GitConfigService);
|
||||
@ -36,7 +36,7 @@
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle id="committerSigning" checked={annotateCommits} on:click={toggleCommitterSigning} />
|
||||
<Toggle id="committerSigning" checked={annotateCommits} onclick={toggleCommitterSigning} />
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
</SettingsPage>
|
||||
|
@ -80,7 +80,7 @@ fn get_github_directory_path(root_path: &path::Path) -> path::PathBuf {
|
||||
fn is_review_template_github(path_str: &str) -> bool {
|
||||
path_str == "PULL_REQUEST_TEMPLATE.md"
|
||||
|| path_str == "pull_request_template.md"
|
||||
|| path_str.contains("PULL_REQUEST_TEMPLATE/")
|
||||
|| path_str.contains("PULL_REQUEST_TEMPLATE/") && path_str.ends_with(".md")
|
||||
}
|
||||
|
||||
fn is_valid_review_template_path_github(path: &path::Path, root_path: &path::Path) -> bool {
|
||||
@ -147,7 +147,7 @@ mod tests {
|
||||
fn test_is_review_template_github() {
|
||||
assert!(is_review_template_github("PULL_REQUEST_TEMPLATE.md"));
|
||||
assert!(is_review_template_github("pull_request_template.md"));
|
||||
assert!(is_review_template_github("PULL_REQUEST_TEMPLATE/"));
|
||||
assert!(is_review_template_github("PULL_REQUEST_TEMPLATE/other.md"));
|
||||
assert!(!is_review_template_github("README.md"));
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
readonly?: boolean;
|
||||
fontSize?: number;
|
||||
maxHeight?: string;
|
||||
rows?: number;
|
||||
autofocus?: boolean;
|
||||
padding?: {
|
||||
top: number;
|
||||
@ -32,6 +33,7 @@
|
||||
readonly,
|
||||
fontSize = 14,
|
||||
maxHeight = 'none',
|
||||
rows = 1,
|
||||
autofocus = false,
|
||||
padding = { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
oninput,
|
||||
@ -49,6 +51,15 @@
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (ref) {
|
||||
// reference the value to trigger
|
||||
// the effect when it changes
|
||||
value;
|
||||
autoHeight(ref);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
@ -59,11 +70,10 @@
|
||||
autoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
}}
|
||||
class="borderless-textarea scrollbar"
|
||||
rows={1}
|
||||
{rows}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
oninput={(e) => {
|
||||
autoHeight(e.currentTarget);
|
||||
oninput?.(e);
|
||||
}}
|
||||
onfocus={(e) => {
|
||||
|
@ -1,14 +1,22 @@
|
||||
<script lang="ts">
|
||||
export let small = false;
|
||||
export let disabled = false;
|
||||
export let checked = false;
|
||||
export let value = '';
|
||||
export let id = '';
|
||||
interface Props {
|
||||
small?: boolean;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
value?: string;
|
||||
id?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let { small, disabled, checked = $bindable(), value, id, onclick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:checked
|
||||
on:click|stopPropagation
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onclick?.(e);
|
||||
}}
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
class:small
|
68
packages/ui/src/lib/ToggleButton.svelte
Normal file
68
packages/ui/src/lib/ToggleButton.svelte
Normal file
@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
import Toggle from '$lib/Toggle.svelte';
|
||||
import Tooltip from '$lib/Tooltip.svelte';
|
||||
import iconsJson from '$lib/data/icons.json';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
icon?: keyof typeof iconsJson;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let { id, label, checked = $bindable(), icon, tooltip, disabled, onclick }: Props = $props();
|
||||
|
||||
const toggleId = id || label.toLowerCase().replace(/\s/g, '-');
|
||||
</script>
|
||||
|
||||
<Tooltip text={tooltip}>
|
||||
<label class="toggle-btn" class:disabled for={toggleId}>
|
||||
{#if icon}
|
||||
<div class="toggle-icon">
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-12 text-semibold toggle-btn__label">{label}</span>
|
||||
<Toggle
|
||||
id={toggleId}
|
||||
small
|
||||
{checked}
|
||||
onclick={(e) => {
|
||||
onclick?.(e);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Tooltip>
|
||||
|
||||
<style lang="postcss">
|
||||
.toggle-btn {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: var(--size-button);
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--clr-border-1);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: flex;
|
||||
opacity: 0.5;
|
||||
margin-left: -2px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.toggle-btn__label {
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user