mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-22 00:51:38 +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
|
# https://github.com/actions/labeler#basic-examples
|
||||||
rust: ["crates/**/*"]
|
|
||||||
svelte: ["app/**/*"]
|
rust:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: crates/**/*
|
||||||
|
|
||||||
|
svelte:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: app/**/*
|
||||||
|
15
.github/workflows/pr-labeler.yml
vendored
15
.github/workflows/pr-labeler.yml
vendored
@ -1,16 +1,19 @@
|
|||||||
# https://github.com/actions/labeler#create-workflow
|
# https://github.com/actions/labeler#create-workflow
|
||||||
|
|
||||||
name: Label Pull Requests
|
name: Label Pull Requests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prs:
|
labeler:
|
||||||
name: Triage
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repository: "gitbutlerapp/gitbutler"
|
||||||
configuration-path: ".github/pr-labeler.yml"
|
- uses: actions/labeler@v5
|
||||||
|
with:
|
||||||
|
configuration-path: '.github/pr-labeler.yml'
|
||||||
|
@ -147,12 +147,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let branchFiles: BranchFiles | undefined;
|
|
||||||
|
|
||||||
function onBottomReached() {
|
|
||||||
branchFiles?.loadMore();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $isLaneCollapsed}
|
{#if $isLaneCollapsed}
|
||||||
@ -181,8 +175,6 @@
|
|||||||
top: 12,
|
top: 12,
|
||||||
bottom: 12
|
bottom: 12
|
||||||
}}
|
}}
|
||||||
bottomBuffer={300}
|
|
||||||
on:bottomReached={onBottomReached}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
bind:this={rsViewport}
|
bind:this={rsViewport}
|
||||||
@ -243,7 +235,6 @@
|
|||||||
{isUnapplied}
|
{isUnapplied}
|
||||||
showCheckboxes={$commitBoxOpen}
|
showCheckboxes={$commitBoxOpen}
|
||||||
allowMultiple
|
allowMultiple
|
||||||
bind:this={branchFiles}
|
|
||||||
/>
|
/>
|
||||||
{#if branch.active && branch.conflicted}
|
{#if branch.active && branch.conflicted}
|
||||||
<div class="card-notifications">
|
<div class="card-notifications">
|
||||||
@ -264,6 +255,7 @@
|
|||||||
<CommitDialog
|
<CommitDialog
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
expanded={commitBoxOpen}
|
expanded={commitBoxOpen}
|
||||||
|
hasSectionsAfter={branch.commits.length > 0}
|
||||||
on:action={(e) => {
|
on:action={(e) => {
|
||||||
if (e.detail === 'generate-branch-name') {
|
if (e.detail === 'generate-branch-name') {
|
||||||
generateBranchName();
|
generateBranchName();
|
||||||
@ -283,7 +275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="no-changes" data-dnd-ignore>
|
<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"
|
<svelte:fragment slot="caption"
|
||||||
>No uncommitted changes on this branch</svelte:fragment
|
>No uncommitted changes on this branch</svelte:fragment
|
||||||
>
|
>
|
||||||
@ -292,10 +284,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card-commits">
|
||||||
<CommitList {isUnapplied} />
|
<CommitList {isUnapplied} />
|
||||||
<BranchFooter {isUnapplied} />
|
<BranchFooter {isUnapplied} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
<div class="divider-line">
|
<div class="divider-line">
|
||||||
<Resizer
|
<Resizer
|
||||||
@ -355,13 +349,18 @@
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
/* overflow: hidden; */
|
||||||
|
/* border: 1px solid var(--clr-border-2);
|
||||||
|
border-radius: var(--radius-m); */
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-card__files {
|
.branch-card__files {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
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 {
|
.card-notifications {
|
||||||
|
@ -18,12 +18,6 @@
|
|||||||
function unselectAllFiles() {
|
function unselectAllFiles() {
|
||||||
fileIdSelection.clear();
|
fileIdSelection.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
let branchFilesList: BranchFilesList | undefined;
|
|
||||||
|
|
||||||
export function loadMore() {
|
|
||||||
branchFilesList?.loadMore();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -38,21 +32,12 @@
|
|||||||
on:click={unselectAllFiles}
|
on:click={unselectAllFiles}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#if files.length > 0}
|
||||||
<BranchFilesList
|
<BranchFilesList {allowMultiple} {readonly} {files} {showCheckboxes} {isUnapplied} />
|
||||||
bind:this={branchFilesList}
|
|
||||||
{allowMultiple}
|
|
||||||
{readonly}
|
|
||||||
{files}
|
|
||||||
{showCheckboxes}
|
|
||||||
{isUnapplied}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.branch-files {
|
.branch-files {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--clr-bg-1);
|
|
||||||
/* padding: 0 14px 14px; */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -71,6 +71,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
}
|
}
|
||||||
.header__title {
|
.header__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import BranchFilesHeader from './BranchFilesHeader.svelte';
|
import BranchFilesHeader from './BranchFilesHeader.svelte';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import FileListItem from './FileListItem.svelte';
|
import FileListItem from './FileListItem.svelte';
|
||||||
|
import LazyloadContainer from './LazyloadContainer.svelte';
|
||||||
import TextBox from '$lib/components/TextBox.svelte';
|
import TextBox from '$lib/components/TextBox.svelte';
|
||||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||||
import { getContext } from '$lib/utils/context';
|
import { getContext } from '$lib/utils/context';
|
||||||
@ -64,11 +65,21 @@
|
|||||||
style="ghost"
|
style="ghost"
|
||||||
outline
|
outline
|
||||||
on:mousedown={() => copyToClipboard(mergeDiffCommand + $commit.id.slice(0, 7))}
|
on:mousedown={() => copyToClipboard(mergeDiffCommand + $commit.id.slice(0, 7))}
|
||||||
></Button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#each displayedFiles as file (file.id)}
|
{#each displayedFiles as file (file.id)}
|
||||||
<FileListItem
|
<FileListItem
|
||||||
{file}
|
{file}
|
||||||
@ -85,6 +96,8 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
</LazyloadContainer>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.merge-commit-error {
|
.merge-commit-error {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { PromptService } from '$lib/backend/prompt';
|
import { PromptService } from '$lib/backend/prompt';
|
||||||
import { project } from '$lib/testing/fixtures';
|
import { project } from '$lib/testing/fixtures';
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
|
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { getLocalCommits, getRemoteCommits, getUnknownCommits } from '$lib/vbranches/contexts';
|
import { getLocalCommits, getRemoteCommits, getUnknownCommits } from '$lib/vbranches/contexts';
|
||||||
import { Branch } from '$lib/vbranches/types';
|
import { Branch } from '$lib/vbranches/types';
|
||||||
@ -25,6 +26,7 @@
|
|||||||
const unknownCommits = getUnknownCommits();
|
const unknownCommits = getUnknownCommits();
|
||||||
|
|
||||||
let isLoading: boolean;
|
let isLoading: boolean;
|
||||||
|
let isInViewport = false;
|
||||||
|
|
||||||
$: canBePushed = $localCommits.length !== 0 || $unknownCommits.length !== 0;
|
$: canBePushed = $localCommits.length !== 0 || $unknownCommits.length !== 0;
|
||||||
$: hasUnknownCommits = $unknownCommits.length > 0;
|
$: hasUnknownCommits = $unknownCommits.length > 0;
|
||||||
@ -33,7 +35,25 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !isUnapplied && hasCommits}
|
{#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 canBePushed}
|
||||||
{#if $prompt}
|
{#if $prompt}
|
||||||
<PassphraseBox prompt={$prompt} error={$promptError} />
|
<PassphraseBox prompt={$prompt} error={$promptError} />
|
||||||
@ -77,13 +97,14 @@
|
|||||||
.actions {
|
.actions {
|
||||||
background: var(--clr-bg-1);
|
background: var(--clr-bg-1);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
border-top: 1px solid var(--clr-border-2);
|
||||||
|
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EMPTY STATE */
|
/* EMPTY STATE */
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* justify-content: space-between; */
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
@ -96,4 +117,16 @@
|
|||||||
color: var(--clr-text-3);
|
color: var(--clr-text-3);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MODIFIERS */
|
||||||
|
.sticky {
|
||||||
|
z-index: var(--z-lifted);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-in-viewport {
|
||||||
|
border-radius: 0;
|
||||||
|
/* background-color: aquamarine; */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
use:useResize={(frame) => {
|
use:useResize={(e) => {
|
||||||
inputWidth = `${Math.round(frame.width)}px`;
|
inputWidth = `${Math.round(e.frame.width)}px`;
|
||||||
}}
|
}}
|
||||||
class="branch-name-mesure-el text-base-14 text-bold"
|
class="branch-name-mesure-el text-base-14 text-bold"
|
||||||
bind:this={mesureEl}>{name}</span
|
bind:this={mesureEl}>{name}</span
|
||||||
|
@ -103,7 +103,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={commitMessageModal} width="small">
|
<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)}
|
{#snippet controls(close)}
|
||||||
<Button style="ghost" outline on:click={close}>Cancel</Button>
|
<Button style="ghost" outline on:click={close}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import CommitMessageInput from '$lib/components/CommitMessageInput.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 { getContext, getContextStore } from '$lib/utils/context';
|
||||||
|
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { Ownership } from '$lib/vbranches/ownership';
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
import { Branch } from '$lib/vbranches/types';
|
import { Branch } from '$lib/vbranches/types';
|
||||||
import { quintOut } from 'svelte/easing';
|
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
export let projectId: string;
|
export let projectId: string;
|
||||||
export let expanded: Writable<boolean>;
|
export let expanded: Writable<boolean>;
|
||||||
|
export let hasSectionsAfter: boolean;
|
||||||
|
|
||||||
const branchController = getContext(BranchController);
|
const branchController = getContext(BranchController);
|
||||||
const selectedOwnership = getContextStore(Ownership);
|
const selectedOwnership = getContextStore(Ownership);
|
||||||
@ -21,8 +21,8 @@
|
|||||||
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
||||||
|
|
||||||
let isCommitting = false;
|
let isCommitting = false;
|
||||||
|
|
||||||
let commitMessageValid = false;
|
let commitMessageValid = false;
|
||||||
|
let isInViewport = false;
|
||||||
|
|
||||||
async function commit() {
|
async function commit() {
|
||||||
const message = $commitMessage;
|
const message = $commitMessage;
|
||||||
@ -41,17 +41,32 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
<div
|
||||||
{#if $expanded}
|
class="commit-box"
|
||||||
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
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
|
<CommitMessageInput
|
||||||
bind:commitMessage={$commitMessage}
|
bind:commitMessage={$commitMessage}
|
||||||
bind:valid={commitMessageValid}
|
bind:valid={commitMessageValid}
|
||||||
|
isExpanded={$expanded}
|
||||||
{commit}
|
{commit}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div class="actions" class:commit-box__actions-expanded={$expanded}>
|
||||||
{/if}
|
|
||||||
<div class="actions">
|
|
||||||
{#if $expanded && !isCommitting}
|
{#if $expanded && !isCommitting}
|
||||||
<Button
|
<Button
|
||||||
style="ghost"
|
style="ghost"
|
||||||
@ -87,27 +102,31 @@
|
|||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.commit-box {
|
.commit-box {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
background: var(--clr-bg-1);
|
background: var(--clr-bg-1);
|
||||||
border-top: 1px solid var(--clr-border-2);
|
border-top: 1px solid var(--clr-border-2);
|
||||||
transition: background-color var(--transition-medium);
|
transition: background-color var(--transition-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-box__expander {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-box__expanded {
|
/* MODIFIERS */
|
||||||
background-color: var(--clr-bg-2);
|
.not-in-viewport {
|
||||||
|
z-index: var(--z-ground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-sections-after {
|
||||||
|
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -264,7 +264,7 @@
|
|||||||
<!-- BASE -->
|
<!-- BASE -->
|
||||||
<div class="base-row-container" class:base-row-container_unfolded={baseIsUnfolded}>
|
<div class="base-row-container" class:base-row-container_unfolded={baseIsUnfolded}>
|
||||||
<div
|
<div
|
||||||
class="commit-group base-row"
|
class="base-row"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
on:click|stopPropagation={() => (baseIsUnfolded = !baseIsUnfolded)}
|
on:click|stopPropagation={() => (baseIsUnfolded = !baseIsUnfolded)}
|
||||||
@ -303,7 +303,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--clr-bg-2);
|
background-color: var(--clr-bg-2);
|
||||||
border-top: 1px solid var(--clr-border-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-top-margin: 8px;
|
||||||
--base-icon-top: 16px;
|
--base-icon-top: 16px;
|
||||||
@ -313,10 +313,10 @@
|
|||||||
--avatar-top: 16px;
|
--avatar-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-group {
|
/* .commit-group {
|
||||||
/* padding-right: 14px;
|
padding-right: 14px;
|
||||||
padding-left: 8px; */
|
padding-left: 8px;
|
||||||
}
|
} */
|
||||||
|
|
||||||
/* BASE ROW */
|
/* BASE ROW */
|
||||||
|
|
||||||
|
@ -19,11 +19,13 @@
|
|||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||||
|
import { useResize } from '$lib/utils/useResize';
|
||||||
import { Ownership } from '$lib/vbranches/ownership';
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
import { Branch, LocalFile } from '$lib/vbranches/types';
|
import { Branch, LocalFile } from '$lib/vbranches/types';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let isExpanded: boolean;
|
||||||
export let commitMessage: string;
|
export let commitMessage: string;
|
||||||
export let valid: boolean = false;
|
export let valid: boolean = false;
|
||||||
export let commit: (() => void) | undefined = undefined;
|
export let commit: (() => void) | undefined = undefined;
|
||||||
@ -101,7 +103,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="commit-box__textarea-wrapper text-input">
|
{#if isExpanded}
|
||||||
|
<div
|
||||||
|
class="commit-box__textarea-wrapper text-input"
|
||||||
|
use:useResize={() => {
|
||||||
|
useAutoHeight(titleTextArea);
|
||||||
|
useAutoHeight(descriptionTextArea);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
value={title}
|
value={title}
|
||||||
placeholder="Commit summary"
|
placeholder="Commit summary"
|
||||||
@ -169,6 +178,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="commit-box__texarea-actions"
|
class="commit-box__texarea-actions"
|
||||||
|
class:commit-box-actions_expanded={isExpanded}
|
||||||
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
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'}
|
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
||||||
@ -203,6 +213,7 @@
|
|||||||
</DropDownButton>
|
</DropDownButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.commit-box__textarea-wrapper {
|
.commit-box__textarea-wrapper {
|
||||||
@ -211,6 +222,12 @@
|
|||||||
padding: 0 0 48px;
|
padding: 0 0 48px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
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 {
|
.commit-box__textarea {
|
||||||
@ -221,6 +238,7 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
background: none;
|
background: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -252,8 +270,45 @@
|
|||||||
|
|
||||||
.commit-box__texarea-actions {
|
.commit-box__texarea-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
|
||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: 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>
|
</style>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let image: string;
|
export let image: string;
|
||||||
export let width: string = '18rem';
|
export let width: string = '18rem';
|
||||||
export let hasBottomShift: boolean = true;
|
export let hasBottomMargin: boolean = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="empty-state-container">
|
<div class="empty-state-container">
|
||||||
<div
|
<div
|
||||||
class="empty-state"
|
class="empty-state"
|
||||||
style:max-width={width}
|
style:max-width={width}
|
||||||
style:margin-bottom={hasBottomShift ? '48px' : '0'}
|
style:margin-bottom={hasBottomMargin ? '48px' : '0'}
|
||||||
>
|
>
|
||||||
<div class="empty-state__image">
|
<div class="empty-state__image">
|
||||||
{@html image}
|
{@html image}
|
||||||
|
@ -104,6 +104,13 @@
|
|||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
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={{
|
use:draggable={{
|
||||||
data: $selectedFiles.then(
|
data: $selectedFiles.then(
|
||||||
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
||||||
@ -112,13 +119,6 @@
|
|||||||
viewportId: 'board-viewport',
|
viewportId: 'board-viewport',
|
||||||
selector: '.selected-draggable'
|
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}
|
{#if showCheckbox}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import FileCard from './FileCard.svelte';
|
import FileCard from './FileCard.svelte';
|
||||||
import FullviewLoading from './FullviewLoading.svelte';
|
import FullviewLoading from './FullviewLoading.svelte';
|
||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
|
import LazyloadContainer from './LazyloadContainer.svelte';
|
||||||
import ScrollableContainer from './ScrollableContainer.svelte';
|
import ScrollableContainer from './ScrollableContainer.svelte';
|
||||||
import SnapshotCard from './SnapshotCard.svelte';
|
import SnapshotCard from './SnapshotCard.svelte';
|
||||||
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
|
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
|
||||||
@ -140,9 +141,16 @@
|
|||||||
|
|
||||||
<!-- SNAPSHOTS -->
|
<!-- SNAPSHOTS -->
|
||||||
{#if $snapshots.length > 0}
|
{#if $snapshots.length > 0}
|
||||||
<ScrollableContainer on:bottomReached={onLastInView}>
|
<ScrollableContainer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- SNAPSHOTS FEED -->
|
<!-- SNAPSHOTS FEED -->
|
||||||
|
<LazyloadContainer
|
||||||
|
minTriggerCount={30}
|
||||||
|
ontrigger={() => {
|
||||||
|
console.log('load more snapshots…');
|
||||||
|
onLastInView();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#each $snapshots as entry, idx (entry.id)}
|
{#each $snapshots as entry, idx (entry.id)}
|
||||||
{@const withinRestoreItems = findRestorationRanges($snapshots)}
|
{@const withinRestoreItems = findRestorationRanges($snapshots)}
|
||||||
{#if idx === 0 || createdOnDay(entry.createdAt) !== createdOnDay($snapshots[idx - 1].createdAt)}
|
{#if idx === 0 || createdOnDay(entry.createdAt) !== createdOnDay($snapshots[idx - 1].createdAt)}
|
||||||
@ -185,6 +193,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
</LazyloadContainer>
|
||||||
|
|
||||||
<!-- LOAD MORE -->
|
<!-- LOAD MORE -->
|
||||||
{#if $loading}
|
{#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 shift = '0';
|
||||||
export let thickness = '0.563rem';
|
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;
|
let observer: ResizeObserver;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ dragging: boolean; bottomReached: boolean }>();
|
const dispatch = createEventDispatcher<{ dragging: boolean }>();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
observer = new ResizeObserver(() => {
|
observer = new ResizeObserver(() => {
|
||||||
@ -46,14 +43,6 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
bind:this={viewport}
|
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"
|
class="viewport hide-native-scrollbar"
|
||||||
style:height
|
style:height
|
||||||
style:overflow-y={scrollable ? 'auto' : 'hidden'}
|
style:overflow-y={scrollable ? 'auto' : 'hidden'}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
let filterText: string | undefined = undefined;
|
let filterText: string | undefined = undefined;
|
||||||
let filteredItems: Selectable[] = items;
|
let filteredItems: Selectable[] = items;
|
||||||
|
|
||||||
function filterItems(items: Selectable[], filterText: string | undefined) {
|
const filterItems = throttle((items: Selectable[], filterText: string | undefined) => {
|
||||||
if (!filterText) {
|
if (!filterText) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
if (!isStr(property)) return false;
|
if (!isStr(property)) return false;
|
||||||
return property.includes(filterText);
|
return property.includes(filterText);
|
||||||
});
|
});
|
||||||
}
|
}, INPUT_THROTTLE_TIME);
|
||||||
|
|
||||||
$: filteredItems = filterItems(items, filterText);
|
$: filteredItems = filterItems(items, filterText);
|
||||||
|
|
||||||
@ -111,13 +111,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChar = throttle((char: string) => {
|
function handleChar(char: string) {
|
||||||
highlightIndex = undefined;
|
highlightIndex = undefined;
|
||||||
filterText ??= '';
|
filterText ??= '';
|
||||||
filterText += char;
|
filterText += char;
|
||||||
}, INPUT_THROTTLE_TIME);
|
}
|
||||||
|
|
||||||
const handleDelete = throttle(() => {
|
function handleDelete() {
|
||||||
if (filterText === undefined) return;
|
if (filterText === undefined) return;
|
||||||
|
|
||||||
if (filterText.length === 1) {
|
if (filterText.length === 1) {
|
||||||
@ -126,7 +126,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterText = filterText.slice(0, -1);
|
filterText = filterText.slice(0, -1);
|
||||||
}, INPUT_THROTTLE_TIME);
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
||||||
if (!listOpen) {
|
if (!listOpen) {
|
||||||
|
@ -29,7 +29,11 @@
|
|||||||
return `${createdOnDay(date)}, ${toHumanReadableTime(date)}`;
|
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) {
|
function camelToTitleCase(str: string | undefined) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
|
@ -48,8 +48,8 @@
|
|||||||
dispatch('change', e.currentTarget.value);
|
dispatch('change', e.currentTarget.value);
|
||||||
useAutoHeight(e.currentTarget);
|
useAutoHeight(e.currentTarget);
|
||||||
}}
|
}}
|
||||||
use:useResize={() => {
|
use:useResize={(e) => {
|
||||||
useAutoHeight(textareaElement);
|
useAutoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||||
}}
|
}}
|
||||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||||
style:max-height={maxHeight ? pxToRem(maxHeight) : undefined}
|
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(
|
export function useResize(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
callback: (frame: { width: number; height: number }) => void
|
callback: (data: { currentTarget: HTMLElement; frame: { width: number; height: number } }) => void
|
||||||
) {
|
) {
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const { inlineSize, blockSize } = entry.borderBoxSize[0];
|
const { inlineSize, blockSize } = entry.borderBoxSize[0];
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
|
currentTarget: element,
|
||||||
|
frame: {
|
||||||
width: Math.round(inlineSize),
|
width: Math.round(inlineSize),
|
||||||
height: Math.round(blockSize)
|
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