mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-21 08:31:40 +03:00
Merge branch 'master' into askpass-pipe-windows-fix
This commit is contained in:
commit
6d5730d558
12
.github/pr-labeler.yml
vendored
12
.github/pr-labeler.yml
vendored
@ -1,3 +1,9 @@
|
||||
# https://github.com/actions/labeler#create-githublabeleryml
|
||||
rust: ["crates/**/*"]
|
||||
svelte: ["app/**/*"]
|
||||
# https://github.com/actions/labeler#basic-examples
|
||||
|
||||
rust:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: crates/**/*
|
||||
|
||||
svelte:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: app/**/*
|
||||
|
17
.github/workflows/pr-labeler.yml
vendored
17
.github/workflows/pr-labeler.yml
vendored
@ -1,16 +1,19 @@
|
||||
# https://github.com/actions/labeler#create-workflow
|
||||
|
||||
name: Label Pull Requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
prs:
|
||||
name: Triage
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: ".github/pr-labeler.yml"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: "gitbutlerapp/gitbutler"
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
configuration-path: '.github/pr-labeler.yml'
|
||||
|
@ -147,12 +147,6 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let branchFiles: BranchFiles | undefined;
|
||||
|
||||
function onBottomReached() {
|
||||
branchFiles?.loadMore();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $isLaneCollapsed}
|
||||
@ -181,8 +175,6 @@
|
||||
top: 12,
|
||||
bottom: 12
|
||||
}}
|
||||
bottomBuffer={300}
|
||||
on:bottomReached={onBottomReached}
|
||||
>
|
||||
<div
|
||||
bind:this={rsViewport}
|
||||
@ -243,7 +235,6 @@
|
||||
{isUnapplied}
|
||||
showCheckboxes={$commitBoxOpen}
|
||||
allowMultiple
|
||||
bind:this={branchFiles}
|
||||
/>
|
||||
{#if branch.active && branch.conflicted}
|
||||
<div class="card-notifications">
|
||||
@ -264,6 +255,7 @@
|
||||
<CommitDialog
|
||||
projectId={project.id}
|
||||
expanded={commitBoxOpen}
|
||||
hasSectionsAfter={branch.commits.length > 0}
|
||||
on:action={(e) => {
|
||||
if (e.detail === 'generate-branch-name') {
|
||||
generateBranchName();
|
||||
@ -283,7 +275,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-changes" data-dnd-ignore>
|
||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomShift={false}>
|
||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
||||
<svelte:fragment slot="caption"
|
||||
>No uncommitted changes on this branch</svelte:fragment
|
||||
>
|
||||
@ -292,8 +284,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<CommitList {isUnapplied} />
|
||||
<BranchFooter {isUnapplied} />
|
||||
<div class="card-commits">
|
||||
<CommitList {isUnapplied} />
|
||||
<BranchFooter {isUnapplied} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
@ -355,13 +349,18 @@
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
/* overflow: hidden; */
|
||||
/* border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m); */
|
||||
}
|
||||
|
||||
.branch-card__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
/* border-left: 1px solid var(--clr-border-2);
|
||||
border-right: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0; */
|
||||
}
|
||||
|
||||
.card-notifications {
|
||||
|
@ -18,12 +18,6 @@
|
||||
function unselectAllFiles() {
|
||||
fileIdSelection.clear();
|
||||
}
|
||||
|
||||
let branchFilesList: BranchFilesList | undefined;
|
||||
|
||||
export function loadMore() {
|
||||
branchFilesList?.loadMore();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -38,21 +32,12 @@
|
||||
on:click={unselectAllFiles}
|
||||
>
|
||||
{#if files.length > 0}
|
||||
<BranchFilesList
|
||||
bind:this={branchFilesList}
|
||||
{allowMultiple}
|
||||
{readonly}
|
||||
{files}
|
||||
{showCheckboxes}
|
||||
{isUnapplied}
|
||||
/>
|
||||
<BranchFilesList {allowMultiple} {readonly} {files} {showCheckboxes} {isUnapplied} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.branch-files {
|
||||
flex: 1;
|
||||
background: var(--clr-bg-1);
|
||||
/* padding: 0 14px 14px; */
|
||||
}
|
||||
</style>
|
||||
|
@ -71,6 +71,9 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px;
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||
background-color: var(--clr-bg-1);
|
||||
}
|
||||
.header__title {
|
||||
display: flex;
|
||||
|
@ -2,6 +2,7 @@
|
||||
import BranchFilesHeader from './BranchFilesHeader.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import FileListItem from './FileListItem.svelte';
|
||||
import LazyloadContainer from './LazyloadContainer.svelte';
|
||||
import TextBox from '$lib/components/TextBox.svelte';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
@ -64,27 +65,39 @@
|
||||
style="ghost"
|
||||
outline
|
||||
on:mousedown={() => copyToClipboard(mergeDiffCommand + $commit.id.slice(0, 7))}
|
||||
></Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each displayedFiles as file (file.id)}
|
||||
<FileListItem
|
||||
{file}
|
||||
{readonly}
|
||||
{isUnapplied}
|
||||
showCheckbox={showCheckboxes}
|
||||
selected={$fileIdSelection.includes(stringifyFileKey(file.id, $commit?.id))}
|
||||
on:click={(e) => {
|
||||
selectFilesInList(e, file, fileIdSelection, displayedFiles, allowMultiple, $commit);
|
||||
{#if displayedFiles.length > 0}
|
||||
<!-- Maximum amount for initial render is 100 files
|
||||
`minTriggerCount` set to 80 in order to start the loading a bit earlier. -->
|
||||
<LazyloadContainer
|
||||
minTriggerCount={80}
|
||||
ontrigger={() => {
|
||||
console.log('loading more files...');
|
||||
loadMore();
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
e.preventDefault();
|
||||
maybeMoveSelection(e.key, file, displayedFiles, fileIdSelection);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
>
|
||||
{#each displayedFiles as file (file.id)}
|
||||
<FileListItem
|
||||
{file}
|
||||
{readonly}
|
||||
{isUnapplied}
|
||||
showCheckbox={showCheckboxes}
|
||||
selected={$fileIdSelection.includes(stringifyFileKey(file.id, $commit?.id))}
|
||||
on:click={(e) => {
|
||||
selectFilesInList(e, file, fileIdSelection, displayedFiles, allowMultiple, $commit);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
e.preventDefault();
|
||||
maybeMoveSelection(e.key, file, displayedFiles, fileIdSelection);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</LazyloadContainer>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.merge-commit-error {
|
||||
|
@ -5,6 +5,7 @@
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { project } from '$lib/testing/fixtures';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { getLocalCommits, getRemoteCommits, getUnknownCommits } from '$lib/vbranches/contexts';
|
||||
import { Branch } from '$lib/vbranches/types';
|
||||
@ -25,6 +26,7 @@
|
||||
const unknownCommits = getUnknownCommits();
|
||||
|
||||
let isLoading: boolean;
|
||||
let isInViewport = false;
|
||||
|
||||
$: canBePushed = $localCommits.length !== 0 || $unknownCommits.length !== 0;
|
||||
$: hasUnknownCommits = $unknownCommits.length > 0;
|
||||
@ -33,7 +35,25 @@
|
||||
</script>
|
||||
|
||||
{#if !isUnapplied && hasCommits}
|
||||
<div class="actions">
|
||||
<div
|
||||
class="actions"
|
||||
class:sticky={canBePushed}
|
||||
class:not-in-viewport={!isInViewport}
|
||||
use:intersectionObserver={{
|
||||
callback: (entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isInViewport = true;
|
||||
} else {
|
||||
isInViewport = false;
|
||||
}
|
||||
},
|
||||
options: {
|
||||
root: null,
|
||||
rootMargin: '-1px',
|
||||
threshold: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if canBePushed}
|
||||
{#if $prompt}
|
||||
<PassphraseBox prompt={$prompt} error={$promptError} />
|
||||
@ -77,13 +97,14 @@
|
||||
.actions {
|
||||
background: var(--clr-bg-1);
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
}
|
||||
|
||||
/* EMPTY STATE */
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
/* justify-content: space-between; */
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
@ -96,4 +117,16 @@
|
||||
color: var(--clr-text-3);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.sticky {
|
||||
z-index: var(--z-lifted);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.not-in-viewport {
|
||||
border-radius: 0;
|
||||
/* background-color: aquamarine; */
|
||||
}
|
||||
</style>
|
||||
|
@ -17,8 +17,8 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
use:useResize={(frame) => {
|
||||
inputWidth = `${Math.round(frame.width)}px`;
|
||||
use:useResize={(e) => {
|
||||
inputWidth = `${Math.round(e.frame.width)}px`;
|
||||
}}
|
||||
class="branch-name-mesure-el text-base-14 text-bold"
|
||||
bind:this={mesureEl}>{name}</span
|
||||
|
@ -103,7 +103,11 @@
|
||||
</script>
|
||||
|
||||
<Modal bind:this={commitMessageModal} width="small">
|
||||
<CommitMessageInput bind:commitMessage={description} bind:valid={commitMessageValid} />
|
||||
<CommitMessageInput
|
||||
bind:commitMessage={description}
|
||||
bind:valid={commitMessageValid}
|
||||
isExpanded={true}
|
||||
/>
|
||||
{#snippet controls(close)}
|
||||
<Button style="ghost" outline on:click={close}>Cancel</Button>
|
||||
<Button
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Button from './Button.svelte';
|
||||
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||
import { projectRunCommitHooks, persistedCommitMessage } from '$lib/config/config';
|
||||
import { persistedCommitMessage, projectRunCommitHooks } from '$lib/config/config';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch } from '$lib/vbranches/types';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let projectId: string;
|
||||
export let expanded: Writable<boolean>;
|
||||
export let hasSectionsAfter: boolean;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const selectedOwnership = getContextStore(Ownership);
|
||||
@ -21,8 +21,8 @@
|
||||
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
||||
|
||||
let isCommitting = false;
|
||||
|
||||
let commitMessageValid = false;
|
||||
let isInViewport = false;
|
||||
|
||||
async function commit() {
|
||||
const message = $commitMessage;
|
||||
@ -41,17 +41,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
||||
{#if $expanded}
|
||||
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
||||
<CommitMessageInput
|
||||
bind:commitMessage={$commitMessage}
|
||||
bind:valid={commitMessageValid}
|
||||
{commit}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<div
|
||||
class="commit-box"
|
||||
class:not-in-viewport={!isInViewport}
|
||||
class:no-sections-after={!hasSectionsAfter}
|
||||
use:intersectionObserver={{
|
||||
callback: (entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isInViewport = true;
|
||||
} else {
|
||||
isInViewport = false;
|
||||
}
|
||||
},
|
||||
options: {
|
||||
root: null,
|
||||
rootMargin: '-1px',
|
||||
threshold: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CommitMessageInput
|
||||
bind:commitMessage={$commitMessage}
|
||||
bind:valid={commitMessageValid}
|
||||
isExpanded={$expanded}
|
||||
{commit}
|
||||
/>
|
||||
<div class="actions" class:commit-box__actions-expanded={$expanded}>
|
||||
{#if $expanded && !isCommitting}
|
||||
<Button
|
||||
style="ghost"
|
||||
@ -87,27 +102,31 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.commit-box {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
padding: 14px;
|
||||
background: var(--clr-bg-1);
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
transition: background-color var(--transition-medium);
|
||||
}
|
||||
|
||||
.commit-box__expander {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.commit-box__expanded {
|
||||
background-color: var(--clr-bg-2);
|
||||
/* MODIFIERS */
|
||||
.not-in-viewport {
|
||||
z-index: var(--z-ground);
|
||||
}
|
||||
|
||||
.no-sections-after {
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
}
|
||||
</style>
|
||||
|
@ -264,7 +264,7 @@
|
||||
<!-- BASE -->
|
||||
<div class="base-row-container" class:base-row-container_unfolded={baseIsUnfolded}>
|
||||
<div
|
||||
class="commit-group base-row"
|
||||
class="base-row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
on:click|stopPropagation={() => (baseIsUnfolded = !baseIsUnfolded)}
|
||||
@ -303,7 +303,7 @@
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-bg-2);
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
/* border-bottom: 1px solid var(--clr-border-2); */
|
||||
|
||||
--base-top-margin: 8px;
|
||||
--base-icon-top: 16px;
|
||||
@ -313,10 +313,10 @@
|
||||
--avatar-top: 16px;
|
||||
}
|
||||
|
||||
.commit-group {
|
||||
/* padding-right: 14px;
|
||||
padding-left: 8px; */
|
||||
}
|
||||
/* .commit-group {
|
||||
padding-right: 14px;
|
||||
padding-left: 8px;
|
||||
} */
|
||||
|
||||
/* BASE ROW */
|
||||
|
||||
|
@ -19,11 +19,13 @@
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch, LocalFile } from '$lib/vbranches/types';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let isExpanded: boolean;
|
||||
export let commitMessage: string;
|
||||
export let valid: boolean = false;
|
||||
export let commit: (() => void) | undefined = undefined;
|
||||
@ -101,108 +103,117 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="commit-box__textarea-wrapper text-input">
|
||||
<textarea
|
||||
value={title}
|
||||
placeholder="Commit summary"
|
||||
disabled={aiLoading}
|
||||
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
bind:this={titleTextArea}
|
||||
use:focusTextAreaOnMount
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
commitMessage = concatMessage(e.currentTarget.value, description);
|
||||
useAutoHeight(e.currentTarget);
|
||||
{#if isExpanded}
|
||||
<div
|
||||
class="commit-box__textarea-wrapper text-input"
|
||||
use:useResize={() => {
|
||||
useAutoHeight(titleTextArea);
|
||||
useAutoHeight(descriptionTextArea);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (commit && (e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
descriptionTextArea.focus();
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
|
||||
{#if title.length > 0 || description}
|
||||
>
|
||||
<textarea
|
||||
value={description}
|
||||
value={title}
|
||||
placeholder="Commit summary"
|
||||
disabled={aiLoading}
|
||||
placeholder="Commit description (optional)"
|
||||
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
||||
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
bind:this={descriptionTextArea}
|
||||
bind:this={titleTextArea}
|
||||
use:focusTextAreaOnMount
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
commitMessage = concatMessage(title, e.currentTarget.value);
|
||||
commitMessage = concatMessage(e.currentTarget.value, description);
|
||||
useAutoHeight(e.currentTarget);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (e.key === 'Backspace' && value.length === 0) {
|
||||
if (commit && (e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
titleTextArea.focus();
|
||||
useAutoHeight(e.currentTarget);
|
||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey) && value.length === 0) {
|
||||
// select previous textarea on cmd+a if this textarea is empty
|
||||
e.preventDefault();
|
||||
titleTextArea.select();
|
||||
descriptionTextArea.focus();
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
{/if}
|
||||
|
||||
{#if title.length > 50}
|
||||
{#if title.length > 0 || description}
|
||||
<textarea
|
||||
value={description}
|
||||
disabled={aiLoading}
|
||||
placeholder="Commit description (optional)"
|
||||
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
bind:this={descriptionTextArea}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
commitMessage = concatMessage(title, e.currentTarget.value);
|
||||
useAutoHeight(e.currentTarget);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (e.key === 'Backspace' && value.length === 0) {
|
||||
e.preventDefault();
|
||||
titleTextArea.focus();
|
||||
useAutoHeight(e.currentTarget);
|
||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey) && value.length === 0) {
|
||||
// select previous textarea on cmd+a if this textarea is empty
|
||||
e.preventDefault();
|
||||
titleTextArea.select();
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
{/if}
|
||||
|
||||
{#if title.length > 50}
|
||||
<div
|
||||
transition:fly={{ y: 2, duration: 150 }}
|
||||
class="commit-box__textarea-tooltip"
|
||||
use:tooltip={{
|
||||
text: '50 characters or less is best. Extra info can be added in the description.',
|
||||
delay: 200
|
||||
}}
|
||||
>
|
||||
<Icon name="blitz" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
transition:fly={{ y: 2, duration: 150 }}
|
||||
class="commit-box__textarea-tooltip"
|
||||
use:tooltip={{
|
||||
text: '50 characters or less is best. Extra info can be added in the description.',
|
||||
delay: 200
|
||||
}}
|
||||
class="commit-box__texarea-actions"
|
||||
class:commit-box-actions_expanded={isExpanded}
|
||||
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
||||
? ''
|
||||
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
||||
>
|
||||
<Icon name="blitz" />
|
||||
<DropDownButton
|
||||
style="ghost"
|
||||
outline
|
||||
icon="ai-small"
|
||||
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
||||
loading={aiLoading}
|
||||
menuPosition="top"
|
||||
on:click={async () => await generateCommitMessage($branch.files)}
|
||||
>
|
||||
Generate message
|
||||
<ContextMenu slot="context-menu">
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Extra concise"
|
||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Use emojis 😎"
|
||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
</DropDownButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="commit-box__texarea-actions"
|
||||
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
||||
? ''
|
||||
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
||||
>
|
||||
<DropDownButton
|
||||
style="ghost"
|
||||
outline
|
||||
icon="ai-small"
|
||||
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
||||
loading={aiLoading}
|
||||
menuPosition="top"
|
||||
on:click={async () => await generateCommitMessage($branch.files)}
|
||||
>
|
||||
Generate message
|
||||
<ContextMenu slot="context-menu">
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Extra concise"
|
||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Use emojis 😎"
|
||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
</DropDownButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.commit-box__textarea-wrapper {
|
||||
@ -211,6 +222,12 @@
|
||||
padding: 0 0 48px;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
animation: expand-box 0.2s ease forwards;
|
||||
/* props to animate on mount */
|
||||
/* display: none;
|
||||
max-height: 0;
|
||||
overflow: hidden; */
|
||||
}
|
||||
|
||||
.commit-box__textarea {
|
||||
@ -221,6 +238,7 @@
|
||||
gap: 16px;
|
||||
background: none;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
@ -252,8 +270,45 @@
|
||||
|
||||
.commit-box__texarea-actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
/* props to animate on mount */
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
/* .commit-box_expanded {
|
||||
display: flex;
|
||||
animation: expand-box 0.2s ease forwards;
|
||||
} */
|
||||
|
||||
@keyframes expand-box {
|
||||
from {
|
||||
max-height: 0;
|
||||
padding: 0 0 0;
|
||||
}
|
||||
to {
|
||||
max-height: 600px;
|
||||
padding: 0 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-box-actions_expanded {
|
||||
display: flex;
|
||||
animation: expand-actions 0.25s ease forwards;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
@keyframes expand-actions {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let image: string;
|
||||
export let width: string = '18rem';
|
||||
export let hasBottomShift: boolean = true;
|
||||
export let hasBottomMargin: boolean = true;
|
||||
</script>
|
||||
|
||||
<div class="empty-state-container">
|
||||
<div
|
||||
class="empty-state"
|
||||
style:max-width={width}
|
||||
style:margin-bottom={hasBottomShift ? '48px' : '0'}
|
||||
style:margin-bottom={hasBottomMargin ? '48px' : '0'}
|
||||
>
|
||||
<div class="empty-state__image">
|
||||
{@html image}
|
||||
|
@ -104,6 +104,13 @@
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:contextmenu|preventDefault={async (e) => {
|
||||
if (fileIdSelection.has(file.id, $commit?.id)) {
|
||||
popupMenu.openByMouse(e, { files: await $selectedFiles });
|
||||
} else {
|
||||
popupMenu.openByMouse(e, { files: [file] });
|
||||
}
|
||||
}}
|
||||
use:draggable={{
|
||||
data: $selectedFiles.then(
|
||||
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
||||
@ -112,13 +119,6 @@
|
||||
viewportId: 'board-viewport',
|
||||
selector: '.selected-draggable'
|
||||
}}
|
||||
on:contextmenu|preventDefault={async (e) => {
|
||||
if (fileIdSelection.has(file.id, $commit?.id)) {
|
||||
popupMenu.openByMouse(e, { files: await $selectedFiles });
|
||||
} else {
|
||||
popupMenu.openByMouse(e, { files: [file] });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if showCheckbox}
|
||||
<Checkbox
|
||||
|
@ -4,6 +4,7 @@
|
||||
import FileCard from './FileCard.svelte';
|
||||
import FullviewLoading from './FullviewLoading.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import LazyloadContainer from './LazyloadContainer.svelte';
|
||||
import ScrollableContainer from './ScrollableContainer.svelte';
|
||||
import SnapshotCard from './SnapshotCard.svelte';
|
||||
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
|
||||
@ -140,51 +141,59 @@
|
||||
|
||||
<!-- SNAPSHOTS -->
|
||||
{#if $snapshots.length > 0}
|
||||
<ScrollableContainer on:bottomReached={onLastInView}>
|
||||
<ScrollableContainer>
|
||||
<div class="container">
|
||||
<!-- SNAPSHOTS FEED -->
|
||||
{#each $snapshots as entry, idx (entry.id)}
|
||||
{@const withinRestoreItems = findRestorationRanges($snapshots)}
|
||||
{#if idx === 0 || createdOnDay(entry.createdAt) !== createdOnDay($snapshots[idx - 1].createdAt)}
|
||||
<div class="sideview__date-header">
|
||||
<h4 class="text-base-13 text-semibold">
|
||||
{createdOnDay(entry.createdAt)}
|
||||
</h4>
|
||||
</div>
|
||||
{/if}
|
||||
<LazyloadContainer
|
||||
minTriggerCount={30}
|
||||
ontrigger={() => {
|
||||
console.log('load more snapshots…');
|
||||
onLastInView();
|
||||
}}
|
||||
>
|
||||
{#each $snapshots as entry, idx (entry.id)}
|
||||
{@const withinRestoreItems = findRestorationRanges($snapshots)}
|
||||
{#if idx === 0 || createdOnDay(entry.createdAt) !== createdOnDay($snapshots[idx - 1].createdAt)}
|
||||
<div class="sideview__date-header">
|
||||
<h4 class="text-base-13 text-semibold">
|
||||
{createdOnDay(entry.createdAt)}
|
||||
</h4>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if entry.details}
|
||||
<SnapshotCard
|
||||
isWithinRestore={withinRestoreItems.includes(entry.id)}
|
||||
{entry}
|
||||
on:restoreClick={() => {
|
||||
historyService.restoreSnapshot(project.id, entry.id);
|
||||
// In some cases, restoring the snapshot doesnt update the UI correctly
|
||||
// Until we have that figured out, we need to reload the page.
|
||||
location.reload();
|
||||
}}
|
||||
{selectedFile}
|
||||
on:diffClick={async (filePath) => {
|
||||
const path = filePath.detail;
|
||||
{#if entry.details}
|
||||
<SnapshotCard
|
||||
isWithinRestore={withinRestoreItems.includes(entry.id)}
|
||||
{entry}
|
||||
on:restoreClick={() => {
|
||||
historyService.restoreSnapshot(project.id, entry.id);
|
||||
// In some cases, restoring the snapshot doesnt update the UI correctly
|
||||
// Until we have that figured out, we need to reload the page.
|
||||
location.reload();
|
||||
}}
|
||||
{selectedFile}
|
||||
on:diffClick={async (filePath) => {
|
||||
const path = filePath.detail;
|
||||
|
||||
if (snapshotFilesTempStore?.entryId === entry.id) {
|
||||
if (selectedFile?.path === path) {
|
||||
currentFilePreview = undefined;
|
||||
selectedFile = undefined;
|
||||
if (snapshotFilesTempStore?.entryId === entry.id) {
|
||||
if (selectedFile?.path === path) {
|
||||
currentFilePreview = undefined;
|
||||
selectedFile = undefined;
|
||||
} else {
|
||||
updateFilePreview(entry, path);
|
||||
}
|
||||
} else {
|
||||
snapshotFilesTempStore = {
|
||||
entryId: entry.id,
|
||||
diffs: await historyService.getSnapshotDiff(project.id, entry.id)
|
||||
};
|
||||
updateFilePreview(entry, path);
|
||||
}
|
||||
} else {
|
||||
snapshotFilesTempStore = {
|
||||
entryId: entry.id,
|
||||
diffs: await historyService.getSnapshotDiff(project.id, entry.id)
|
||||
};
|
||||
updateFilePreview(entry, path);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</LazyloadContainer>
|
||||
|
||||
<!-- LOAD MORE -->
|
||||
{#if $loading}
|
||||
|
51
app/src/lib/components/LazyloadContainer.svelte
Normal file
51
app/src/lib/components/LazyloadContainer.svelte
Normal file
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: any;
|
||||
minTriggerCount: number;
|
||||
ontrigger: (lastChild: Element) => void;
|
||||
}
|
||||
|
||||
let { children, minTriggerCount, ontrigger }: Props = $props();
|
||||
|
||||
let lazyContainerEl: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
const containerChildren = lazyContainerEl.children;
|
||||
|
||||
if (containerChildren.length < minTriggerCount) return;
|
||||
|
||||
const iObserver = new IntersectionObserver((entries) => {
|
||||
const lastChild = containerChildren[containerChildren.length - 1];
|
||||
if (entries[0].target === lastChild && entries[0].isIntersecting) {
|
||||
ontrigger(lastChild);
|
||||
}
|
||||
});
|
||||
|
||||
const mObserver = new MutationObserver(() => {
|
||||
const lastChild = containerChildren[containerChildren.length - 1];
|
||||
if (lastChild) {
|
||||
iObserver.observe(lastChild);
|
||||
}
|
||||
});
|
||||
|
||||
iObserver.observe(containerChildren[containerChildren.length - 1]);
|
||||
mObserver.observe(lazyContainerEl, { childList: true });
|
||||
|
||||
return () => {
|
||||
iObserver.disconnect();
|
||||
mObserver.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="lazy-container" bind:this={lazyContainerEl}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lazy-container {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
@ -18,12 +18,9 @@
|
||||
export let shift = '0';
|
||||
export let thickness = '0.563rem';
|
||||
|
||||
// How much of a buffer there should be before we consider the bottom reached
|
||||
export let bottomBuffer = 0;
|
||||
|
||||
let observer: ResizeObserver;
|
||||
|
||||
const dispatch = createEventDispatcher<{ dragging: boolean; bottomReached: boolean }>();
|
||||
const dispatch = createEventDispatcher<{ dragging: boolean }>();
|
||||
|
||||
onMount(() => {
|
||||
observer = new ResizeObserver(() => {
|
||||
@ -46,14 +43,6 @@
|
||||
>
|
||||
<div
|
||||
bind:this={viewport}
|
||||
on:scroll={(e) => {
|
||||
const target = e.currentTarget;
|
||||
scrolled = target.scrollTop !== 0;
|
||||
|
||||
if (target.scrollTop + target.clientHeight + bottomBuffer >= target.scrollHeight) {
|
||||
dispatch('bottomReached', true);
|
||||
}
|
||||
}}
|
||||
class="viewport hide-native-scrollbar"
|
||||
style:height
|
||||
style:overflow-y={scrollable ? 'auto' : 'hidden'}
|
||||
|
@ -42,7 +42,7 @@
|
||||
let filterText: string | undefined = undefined;
|
||||
let filteredItems: Selectable[] = items;
|
||||
|
||||
function filterItems(items: Selectable[], filterText: string | undefined) {
|
||||
const filterItems = throttle((items: Selectable[], filterText: string | undefined) => {
|
||||
if (!filterText) {
|
||||
return items;
|
||||
}
|
||||
@ -52,7 +52,7 @@
|
||||
if (!isStr(property)) return false;
|
||||
return property.includes(filterText);
|
||||
});
|
||||
}
|
||||
}, INPUT_THROTTLE_TIME);
|
||||
|
||||
$: filteredItems = filterItems(items, filterText);
|
||||
|
||||
@ -111,13 +111,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleChar = throttle((char: string) => {
|
||||
function handleChar(char: string) {
|
||||
highlightIndex = undefined;
|
||||
filterText ??= '';
|
||||
filterText += char;
|
||||
}, INPUT_THROTTLE_TIME);
|
||||
}
|
||||
|
||||
const handleDelete = throttle(() => {
|
||||
function handleDelete() {
|
||||
if (filterText === undefined) return;
|
||||
|
||||
if (filterText.length === 1) {
|
||||
@ -126,7 +126,7 @@
|
||||
}
|
||||
|
||||
filterText = filterText.slice(0, -1);
|
||||
}, INPUT_THROTTLE_TIME);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
||||
if (!listOpen) {
|
||||
|
@ -29,7 +29,11 @@
|
||||
return `${createdOnDay(date)}, ${toHumanReadableTime(date)}`;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ restoreClick: void; diffClick: string }>();
|
||||
const dispatch = createEventDispatcher<{
|
||||
restoreClick: void;
|
||||
diffClick: string;
|
||||
visible: void;
|
||||
}>();
|
||||
|
||||
function camelToTitleCase(str: string | undefined) {
|
||||
if (!str) return '';
|
||||
|
@ -48,8 +48,8 @@
|
||||
dispatch('change', e.currentTarget.value);
|
||||
useAutoHeight(e.currentTarget);
|
||||
}}
|
||||
use:useResize={() => {
|
||||
useAutoHeight(textareaElement);
|
||||
use:useResize={(e) => {
|
||||
useAutoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
}}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
style:max-height={maxHeight ? pxToRem(maxHeight) : undefined}
|
||||
|
27
app/src/lib/utils/intersectionObserver.ts
Normal file
27
app/src/lib/utils/intersectionObserver.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export function intersectionObserver(
|
||||
node: Element,
|
||||
{
|
||||
isDisabled,
|
||||
callback,
|
||||
options
|
||||
}: {
|
||||
isDisabled?: boolean;
|
||||
callback: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;
|
||||
options?: IntersectionObserverInit;
|
||||
}
|
||||
) {
|
||||
if (isDisabled) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry], observer) => callback(entry, observer),
|
||||
options
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
export function useResize(
|
||||
element: HTMLElement,
|
||||
callback: (frame: { width: number; height: number }) => void
|
||||
callback: (data: { currentTarget: HTMLElement; frame: { width: number; height: number } }) => void
|
||||
) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { inlineSize, blockSize } = entry.borderBoxSize[0];
|
||||
|
||||
callback({
|
||||
width: Math.round(inlineSize),
|
||||
height: Math.round(blockSize)
|
||||
currentTarget: element,
|
||||
frame: {
|
||||
width: Math.round(inlineSize),
|
||||
height: Math.round(blockSize)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,23 +0,0 @@
|
||||
export function useStickyPinned(
|
||||
element: HTMLElement,
|
||||
callback: (isPinned: boolean, element: HTMLElement) => void
|
||||
) {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
callback(entry.intersectionRatio < 1, element);
|
||||
|
||||
console.log('sticky pinned', element, entry.intersectionRatio);
|
||||
},
|
||||
{
|
||||
threshold: [1]
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user