Refactor branch lane layout

- whole lane scrollable rather than individual sections
- new upstream commit section
- fixed header at the top
- fixes scroll lock issue with nested scroll containers
- known issue with grey padding on drag
This commit is contained in:
Mattias Granlund 2024-01-19 13:22:19 +01:00
parent 18294abc20
commit 965a51e1e8
22 changed files with 523 additions and 437 deletions

View File

@ -5,6 +5,7 @@
export let icon: keyof typeof iconsJson;
export let size: 's' | 'm' | 'l' = 'l';
export let loading = false;
export let border = false;
let className = '';
let selected = false;
@ -15,6 +16,7 @@
<button
class="icon-btn {className}"
class:selected
class:border
class:small={size == 's'}
class:medium={size == 'm'}
class:large={size == 'l'}
@ -37,6 +39,9 @@
color: var(--clr-theme-scale-ntrl-40);
}
}
.border {
border: 1px solid var(--clr-theme-container-outline-light);
}
.selected {
background-color: var(--clr-theme-container-sub);
}

View File

@ -27,4 +27,4 @@
});
</script>
<img src={imgSrc} alt="Decorative Art" />
<img src={imgSrc} alt="Decorative Art" class="inline-block" />

View File

@ -42,13 +42,13 @@
}}
class="viewport hide-native-scrollbar"
style:height
style:overflow-y={scrollable ? 'scroll' : 'hidden'}
style:overflow-y={scrollable ? 'auto' : 'hidden'}
>
<div bind:this={contents} class="contents">
<slot />
</div>
</div>
<Scrollbar {viewport} {contents} thickness="0.4rem" {initiallyVisible} />
<Scrollbar {viewport} {contents} thickness="0.375rem" {initiallyVisible} />
</div>
<style lang="postcss">
@ -59,7 +59,6 @@
overflow: hidden;
}
.viewport {
overscroll-behavior: none;
height: 100%;
width: 100%;
}

View File

@ -107,6 +107,17 @@ export class BranchController {
}
}
async setSelectedForChanges(branchId: string) {
try {
await invoke<void>('update_virtual_branch', {
projectId: this.projectId,
branch: { id: branchId, selected_for_changes: true }
});
} catch (err) {
toasts.error('Failed to set as target');
}
}
async updateBranchOrder(branchId: string, order: number) {
try {
await invoke<void>('update_virtual_branch', {

View File

@ -77,9 +77,11 @@ export class Branch {
isMergeable!: Promise<boolean>;
@Transform((obj) => new Date(obj.value))
updatedAt!: Date;
// Indicates that branch is default target for new changes
selectedForChanges!: boolean;
}
export type CommitStatus = 'local' | 'remote' | 'integrated';
export type CommitStatus = 'local' | 'remote' | 'integrated' | 'upstream';
export class Commit {
id!: string;

View File

@ -53,7 +53,7 @@
<button on:click={() => httpsWarningBannerDismissed.set(true)}>Dismiss</button>
</div>
{/if}
<div class="relative h-full flex-grow overscroll-none">
<div class="relative h-full flex-grow">
<div class="scroll-viewport hide-native-scrollbar" bind:this={viewport}>
<div class="scroll-contents" bind:this={contents}>
<Board
@ -76,7 +76,6 @@
<style lang="postcss">
.scroll-viewport {
overflow-x: scroll;
overscroll-behavior: none;
height: 100%;
width: 100%;
}

View File

@ -53,7 +53,7 @@
// We account for the NewBranchDropZone by subtracting 2
for (let i = 0; i < children.length - 2; i++) {
const pos = children[i].getBoundingClientRect();
if (e.clientX > pos.left + dragged.offsetWidth / 2) {
if (e.clientX > pos.right + dragged.offsetWidth / 2) {
dropPosition = i + 1; // Note that this is declared in the <script>
} else {
break;
@ -193,8 +193,7 @@
flex-shrink: 1;
align-items: flex-start;
height: 100%;
padding: var(--space-16);
gap: var(--space-12);
padding: 0 var(--space-8);
}
.loading {
display: flex;

View File

@ -72,15 +72,15 @@
<span class="text-base-body-13 new-branch-caption"
>Drag and drop files<br />to create a new branch</span
>
<div class="new-branch-button">
<Button
color="neutral"
kind="outlined"
icon="plus-small"
on:click={() => branchController.createBranch({})}>New branch</Button
>
</div>
</div>
<div class="new-branch-button">
<Button
wide
color="neutral"
kind="outlined"
icon="plus-small"
on:click={() => branchController.createBranch({})}>New branch</Button
>
</div>
</div>
</div>
@ -89,19 +89,18 @@
.canvas-dropzone {
user-select: none;
display: flex;
height: 100%;
padding: var(--space-16) var(--space-16) var(--space-16) var(--space-4);
}
.new-virtual-branch {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 24rem;
height: 100%;
border-radius: var(--radius-m);
border: 1px dashed color-mix(in srgb, var(--clr-theme-container-outline-pale) 50%, transparent);
background-color: transparent;
padding: var(--space-20);
gap: var(--space-20);
outline-color: color-mix(in srgb, var(--clr-theme-scale-pop-40) 0%, transparent);
outline-style: dashed;
@ -120,8 +119,9 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-16);
gap: var(--space-12);
transition: transform var(--transition-medium);
padding: var(--space-20) var(--space-24) var(--space-16) var(--space-24);
}
/* ILLUSTRATION */

View File

@ -20,7 +20,6 @@
import type { GitHubService } from '$lib/github/service';
import { isDraggableRemoteCommit, type DraggableRemoteCommit } from '$lib/draggables';
import BranchHeader from './BranchHeader.svelte';
import UpstreamCommits from './UpstreamCommits.svelte';
import BranchFiles from './BranchFiles.svelte';
import { projectAiGenEnabled } from '$lib/config/config';
import { persisted } from '$lib/persisted/persisted';
@ -30,6 +29,7 @@
import ImgThemed from '$lib/components/ImgThemed.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
export let branch: Branch;
export let readonly = false;
@ -37,7 +37,6 @@
export let base: BaseBranch | undefined | null;
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let maximized = false;
export let branchCount = 1;
export let user: User | undefined;
export let selectedFiles: Writable<File[]>;
@ -50,12 +49,14 @@
const aiGenEnabled = projectAiGenEnabled(project.id);
let rsViewport: HTMLElement;
let commitsScrollable = false;
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
const defaultBranchWidthRem = persisted<number | undefined>(24, 'defaulBranchWidth' + project.id);
const laneWidthKey = 'laneWidth_';
let laneWidth: number;
let headerHeight: number | undefined;
$: console.log(headerHeight);
$: {
// On refresh we need to check expansion status from localStorage
@ -143,195 +144,190 @@
}
</script>
<div bind:this={rsViewport} class="resize-viewport">
<div class="branch-card" style:width={`${laneWidth || $defaultBranchWidthRem}rem`}>
<div class="flex flex-col">
<BranchHeader
{readonly}
{branchController}
{branch}
{base}
{githubService}
projectId={project.id}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
<div bind:this={rsViewport} class="branch-card resize-viewport">
<BranchHeader
{readonly}
{branchController}
{branch}
{base}
{githubService}
bind:height={headerHeight}
projectId={project.id}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
<ScrollableContainer>
<div
style:width={`${laneWidth || $defaultBranchWidthRem}rem`}
class="branch-card__contents"
style:padding-top={`${headerHeight}px`}
>
<div
class="relative flex flex-grow flex-col gap-1 overflow-y-hidden"
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked
}}
/>
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop
}}
>
<!-- DROPZONES -->
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
{#if branch.upstream?.commits.length && branch.upstream?.commits.length > 0 && !branch.conflicted}
<UpstreamCommits
upstream={branch.upstream}
branchId={branch.id}
{#if branch.files?.length > 0}
<div class="card">
<BranchFiles
{branch}
{readonly}
{selectedOwnership}
{selectedFiles}
showCheckboxes={$commitBoxOpen}
/>
{#if branch.active}
<CommitDialog
projectId={project.id}
{branchController}
{branch}
{cloud}
{selectedOwnership}
{user}
bind:expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
{/if}
</div>
{:else if branch.commits.length == 0}
<div class="new-branch card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-new-light.webp',
dark: '/images/lane-new-dark.webp'
}}
/>
</div>
<h2 class="new-branch__title text-base-body-15 text-semibold">
This is a new branch.<br />Let's start creating!
</h2>
<p class="new-branch__caption text-base-body-13">
Get some work done,<br />then throw some files my way
</p>
</div>
</div>
{:else}
<!-- attention: these markers have custom css at the bottom of thise file -->
<div class="no-changes card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-no-changes-light.webp',
dark: '/images/lane-no-changes-dark.webp'
}}
/>
</div>
<h2 class="new-branch__caption text-base-body-13">
No uncommitted changes<br />on this branch
</h2>
</div>
</div>
{/if}
<BranchCommits
{base}
{branch}
{project}
{githubService}
{branchController}
{branchCount}
projectId={project.id}
{base}
/>
{/if}
</div>
<div
class="relative flex flex-grow flex-col overflow-y-hidden"
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop
}}
>
<!-- DROPZONES -->
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
{#if branch.files?.length > 0}
<BranchFiles
{branch}
{readonly}
{selectedOwnership}
{selectedFiles}
showCheckboxes={$commitBoxOpen}
forceResizable={commitsScrollable}
enableResizing={branch.commits.length > 0}
/>
{#if branch.active}
<CommitDialog
projectId={project.id}
{branchController}
{branch}
{cloud}
{selectedOwnership}
{user}
bind:expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
{/if}
{:else if branch.commits.length == 0}
<div class="new-branch" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-new-light.webp',
dark: '/images/lane-new-dark.webp'
}}
/>
</div>
<h2 class="new-branch__title text-base-body-15 text-semibold">
This is a new branch.<br />Let's start creating!
</h2>
<p class="new-branch__caption text-base-body-13">
Get some work done,<br />then throw some files my way
</p>
</div>
</div>
{:else}
<!-- attention: these markers have custom css at the bottom of thise file -->
<div class="no-changes" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-no-changes-light.webp',
dark: '/images/lane-no-changes-dark.webp'
}}
/>
</div>
<h2 class="new-branch__title-caption text-base-body-13">
No uncommitted changes<br />on this branch
</h2>
</div>
</div>
{/if}
<BranchCommits
{base}
{branch}
{project}
{githubService}
{branchController}
{readonly}
bind:scrollable={commitsScrollable}
/>
</div>
</div>
</div>
{#if !maximized}
<Resizer
viewport={rsViewport}
direction="right"
inside={$selectedFiles.length > 0}
minWidth={320}
on:width={(e) => {
laneWidth = e.detail / (16 * $userSettings.zoom);
lscache.set(laneWidthKey + branch.id, laneWidth, 7 * 1440); // 7 day ttl
$defaultBranchWidthRem = laneWidth;
}}
/>
{/if}
</ScrollableContainer>
<Resizer
viewport={rsViewport}
direction="right"
inside={$selectedFiles.length > 0}
minWidth={320}
on:width={(e) => {
laneWidth = e.detail / (16 * $userSettings.zoom);
lscache.set(laneWidthKey + branch.id, laneWidth, 7 * 1440); // 7 day ttl
$defaultBranchWidthRem = laneWidth;
}}
/>
</div>
<style lang="postcss">
.resize-viewport {
height: 100%;
position: relative;
display: flex;
}
.branch-card {
display: flex;
flex-grow: 1;
flex-direction: column;
cursor: default;
overflow-x: hidden;
background: var(--clr-theme-container-light);
}
.branch-card__contents {
padding: var(--space-16) var(--space-8) var(--space-16) var(--space-8);
}
.resize-viewport {
position: relative;
}
.new-branch,
.no-changes {
user-select: none;
display: flex;
flex-grow: 1;
flex-direction: column;
color: var(--clr-theme-scale-ntrl-60);
background: var(--clr-theme-container-light);
justify-content: center;
align-items: center;
padding: var(--space-24) var(--space-40);
padding: var(--space-48) 0;
border-radius: var(--radius-m);
}
.new-branch__content {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-8);
max-width: 14rem;
}
.new-branch__image {
width: 7.5rem;
margin-bottom: var(--space-12);
}
.new-branch__title {
.no-changes {
color: var(--clr-theme-scale-ntrl-40);
text-align: center;
}
.new-branch__caption,
.new-branch__title-caption {
.new-branch__title {
color: var(--clr-theme-scale-ntrl-40);
}
.new-branch__caption {
color: var(--clr-theme-scale-ntrl-50);
opacity: 0.6;
}
.new-branch__caption,
.new-branch__title {
text-align: center;
opacity: 0.6;
}
.new-branch__image {
margin-bottom: var(--space-20);
}
/* hunks drop zone */
:global(.lane-dz-active .lane-dz-marker) {
display: flex;

View File

@ -1,6 +1,5 @@
<script lang="ts">
import type { Project } from '$lib/backend/projects';
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
@ -12,40 +11,45 @@
export let githubService: GitHubService;
export let branchController: BranchController;
export let readonly: boolean;
// Intended for 2 way binding.
export let scrollable: boolean | undefined = undefined;
export let branchCount: number;
</script>
{#if branch.commits.length > 0}
<!-- Note that 11.25rem min height is just observational, it might need updating -->
<ScrollableContainer bind:scrollable minHeight="9rem" showBorderWhenScrolled>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="local"
/>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="remote"
/>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="integrated"
/>
</ScrollableContainer>
{#if branch.commits.length > 0 || (branch.upstream && branch.upstream.commits.length > 0)}
<CommitList
{branch}
{base}
{project}
{branchController}
{branchCount}
{githubService}
{readonly}
type="upstream"
/>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="local"
/>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="remote"
/>
<CommitList
{branch}
{base}
{project}
{branchController}
{githubService}
{readonly}
type="integrated"
/>
{/if}

View File

@ -7,8 +7,6 @@
import SegmentedControl from '$lib/components/SegmentControl/SegmentedControl.svelte';
import Segment from '$lib/components/SegmentControl/Segment.svelte';
import FileTree from './FileTree.svelte';
import Resizer from '$lib/components/Resizer.svelte';
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
import Checkbox from '$lib/components/Checkbox.svelte';
import BranchFilesList from './BranchFilesList.svelte';
@ -16,18 +14,10 @@
export let readonly: boolean;
export let selectedOwnership: Writable<Ownership>;
export let selectedFiles: Writable<File[]>;
export let forceResizable = false;
export let enableResizing = false;
export let showCheckboxes = false;
let selectedListMode: string;
let scrollViewport: HTMLDivElement | undefined;
let rsViewport: HTMLElement;
let scrollable: boolean | undefined;
let height: number | undefined = undefined;
let maxHeight: number | undefined;
let headerElement: HTMLDivElement;
function isAllChecked(selectedOwnership: Ownership): boolean {
@ -73,90 +63,57 @@
</div>
{/if}
<div class="header" bind:this={headerElement}>
<div class="header__left">
{#if showCheckboxes && selectedListMode == 'list' && branch.files.length > 1}
<Checkbox
small
{checked}
{indeterminate}
on:change={(e) => {
if (e.detail) {
selectAll(selectedOwnership, branch.files);
} else {
selectedOwnership.update((ownership) => ownership.clear());
}
}}
/>
{/if}
<div class="header__title text-base-13 text-semibold">
<span>Changes</span>
<Badge count={branch.files.length} />
</div>
</div>
<SegmentedControl bind:selected={selectedListMode} selectedIndex={0}>
<Segment id="list" icon="list-view" />
<Segment id="tree" icon="tree-view" />
</SegmentedControl>
</div>
<div
class="resize-viewport flex-grow"
class:flex-shrink-0={(scrollable || forceResizable) && branch.commits.length > 0 && height}
style:min-height={scrollable || forceResizable ? `${headerElement.offsetHeight}px` : undefined}
style:height={scrollable || forceResizable ? `${height}px` : undefined}
style:max-height={forceResizable && maxHeight ? maxHeight + 'px' : undefined}
bind:this={rsViewport}
>
{#if branch.files.length > 0}
<ScrollableContainer
showBorderWhenScrolled
bind:viewport={scrollViewport}
bind:maxHeight
bind:scrollable
>
<div class="scroll-container">
<!-- TODO: This is an experiment in file sorting. Accept or reject! -->
{#if selectedListMode == 'list'}
<BranchFilesList
{branch}
{selectedOwnership}
{selectedFiles}
{showCheckboxes}
{readonly}
/>
{:else}
<FileTree
node={filesToFileTree(branch.files)}
{showCheckboxes}
branchId={branch.id}
isRoot={true}
{selectedOwnership}
{selectedFiles}
{readonly}
/>
{/if}
<div class="branch-files">
<div class="header" bind:this={headerElement}>
<div class="header__left">
{#if showCheckboxes && selectedListMode == 'list' && branch.files.length > 1}
<Checkbox
small
{checked}
{indeterminate}
on:change={(e) => {
if (e.detail) {
selectAll(selectedOwnership, branch.files);
} else {
selectedOwnership.update((ownership) => ownership.clear());
}
}}
/>
{/if}
<div class="header__title text-base-13 text-semibold">
<span>Changes</span>
<Badge count={branch.files.length} />
</div>
</ScrollableContainer>
{/if}
<!-- Resizing makes no sense if there are no branch commits. -->
{#if (forceResizable || scrollable) && enableResizing}
<Resizer
inside
direction="down"
viewport={rsViewport}
on:height={(e) => {
height = e.detail;
}}
/>
</div>
<SegmentedControl bind:selected={selectedListMode} selectedIndex={0}>
<Segment id="list" icon="list-view" />
<Segment id="tree" icon="tree-view" />
</SegmentedControl>
</div>
{#if branch.files.length > 0}
<div class="scroll-container">
<!-- TODO: This is an experiment in file sorting. Accept or reject! -->
{#if selectedListMode == 'list'}
<BranchFilesList {branch} {selectedOwnership} {selectedFiles} {showCheckboxes} {readonly} />
{:else}
<FileTree
node={filesToFileTree(branch.files)}
{showCheckboxes}
branchId={branch.id}
isRoot={true}
{selectedOwnership}
{selectedFiles}
{readonly}
/>
{/if}
</div>
{/if}
</div>
<style lang="postcss">
.resize-viewport {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
.branch-files {
background: var(--clr-theme-container-light);
border-radius: var(--radius-m) var(--radius-m) 0 0;
}
.scroll-container {
display: flex;

View File

@ -3,21 +3,22 @@
import Icon from '$lib/icons/Icon.svelte';
import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
import { fade } from 'svelte/transition';
import BranchLabel from './BranchLabel.svelte';
import BranchLanePopupMenu from './BranchLanePopupMenu.svelte';
import { clickOutside } from '$lib/clickOutside';
import Tag from './Tag.svelte';
import { branchUrl } from './commitList';
import type { GitHubService } from '$lib/github/service';
import BranchIcon from '../navigation/BranchIcon.svelte';
import { open } from '@tauri-apps/api/shell';
import Button from '$lib/components/Button.svelte';
import { onMount } from 'svelte';
export let readonly = false;
export let branch: Branch;
export let base: BaseBranch | undefined | null;
export let branchController: BranchController;
export let projectId: string;
export let height: number | undefined;
export let githubService: GitHubService;
$: pr$ = githubService.get(branch.upstreamName);
@ -25,125 +26,168 @@
let meatballButton: HTMLDivElement;
let visible = false;
let observer: ResizeObserver;
let container: HTMLDivElement;
onMount(() => {
observer = new ResizeObserver(() => {
if (container) {
height = container.offsetHeight;
}
});
return observer.observe(container);
});
function handleBranchNameChange() {
branchController.updateBranchName(branch.id, branch.name);
}
function normalizeBranchName(value: string) {
return value.toLowerCase().replace(/[^0-9a-z/_]/g, '-');
}
$: hasIntegratedCommits = branch.commits?.some((b) => b.isIntegrated);
</script>
<div class="card__header">
{#if !readonly}
<div class="draggable" data-drag-handle>
<Icon name="draggable-narrow" />
</div>
{/if}
<div class="header__content">
<div class="header__row">
<div class="header__label">
<BranchLabel bind:name={branch.name} on:change={handleBranchNameChange} />
</div>
<div class="flex items-center gap-x-1" transition:fade={{ duration: 150 }}>
{#if !readonly}
<div bind:this={meatballButton}>
<IconButton icon="kebab" size="m" on:click={() => (visible = !visible)} />
</div>
<div
class="branch-popup-menu"
use:clickOutside={{
trigger: meatballButton,
handler: () => (visible = false)
}}
>
<BranchLanePopupMenu {branchController} {branch} {projectId} bind:visible on:action />
</div>
{/if}
</div>
</div>
{#if branch.upstreamName}
<div class="header__remote-branch text-base-body-11">
{#if !branch.upstream}
{#if hasIntegratedCommits}
<div class="status-tag deleted">deleted</div>
<div class="wrapper" bind:this={container}>
<div class="concealer">
<div class="header card">
<div class="header__info">
<div class="header__label">
<BranchLabel bind:name={branch.name} on:change={handleBranchNameChange} />
</div>
<div class="header__remote-branch text-base-body-11">
{#if !branch.upstream}
{#if hasIntegratedCommits}
<div class="status-tag deleted"><Icon name="remote-branch-small" /> deleted</div>
{:else}
<div class="status-tag pending"><Icon name="remote-branch-small" /> new</div>
{/if}
<div class="text-semibold pending-name text-base-11">
origin/{branch.upstreamName ? branch.upstreamName : normalizeBranchName(branch.name)}
</div>
{:else}
<div class="status-tag pending">pending</div>
<div class="status-tag remote"><Icon name="remote-branch-small" /> remote</div>
<Tag
icon="open-link"
color="ghost"
border
clickable
on:click={(e) => {
const url = branchUrl(base, branch.upstream?.name);
if (url) open(url);
e.preventDefault();
e.stopPropagation();
}}
>
origin/{branch.upstreamName}
</Tag>
{#if $pr$?.htmlUrl}
<Tag
icon="pr-small"
color="ghost"
border
clickable
on:click={(e) => {
const url = $pr$?.htmlUrl;
if (url) open(url);
e.preventDefault();
e.stopPropagation();
}}
>
View PR
</Tag>
{/if}
{/if}
{/if}
<div>origin/{branch.upstreamName}</div>
</div>
{/if}
{#if branch.upstream}
<div class="header__links">
<BranchIcon name="remote-branch" color="neutral" />
<Tag
icon="open-link"
color="ghost"
border
clickable
on:click={(e) => {
const url = branchUrl(base, branch.upstream?.name);
if (url) open(url);
e.preventDefault();
e.stopPropagation();
}}
>
View remote branch
</Tag>
{#if $pr$?.htmlUrl}
<Tag
icon="pr-small"
color="ghost"
border
clickable
on:click={(e) => {
const url = $pr$?.htmlUrl;
if (url) open(url);
e.preventDefault();
e.stopPropagation();
}}
>
View PR
</Tag>
</div>
{#if !readonly}
<div class="draggable" data-drag-handle>
<Icon name="draggable-narrow" />
</div>
{/if}
</div>
{/if}
<div class="header__actions">
<div class="header__buttons">
{#if branch.selectedForChanges}
<Button icon="target">Target branch</Button>
{:else}
<Button
icon="target"
kind="outlined"
on:click={async () => {
await branchController.setSelectedForChanges(branch.id);
}}
>
Make target
</Button>
{/if}
</div>
{#if !readonly}
<div class="relative" bind:this={meatballButton}>
<IconButton border icon="kebab" size="m" on:click={() => (visible = !visible)} />
<div
class="branch-popup-menu"
use:clickOutside={{
trigger: meatballButton,
handler: () => (visible = false)
}}
>
<BranchLanePopupMenu {branchController} {branch} {projectId} bind:visible on:action />
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<style lang="postcss">
.card__header {
.wrapper {
padding: 0 var(--space-8) var(--space-16) var(--space-8);
position: absolute;
z-index: 10;
width: 100%;
}
.concealer {
background: var(--clr-theme-container-pale);
border-radius: 0 0 var(--radius-m) var(--radius-m);
padding-top: var(--space-16);
}
.header {
user-select: none;
position: relative;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-12);
top: 0;
&:hover {
& .header__content {
margin-left: var(--space-6);
}
& .draggable {
opacity: 1;
}
}
}
.header__content {
.header__info {
display: flex;
flex-direction: column;
transition: margin var(--transition-slow);
padding: var(--space-12);
gap: var(--space-10);
}
.header__row {
width: 100%;
.header__actions {
display: flex;
gap: var(--space-4);
background: var(--clr-theme-container-pale);
padding: var(--space-12);
justify-content: space-between;
gap: var(--space-8);
overflow-x: hidden;
border-radius: 0 0 var(--radius-m) var(--radius-m);
}
.header__buttons {
display: flex;
position: relative;
gap: var(--space-4);
}
.header__label {
overflow-x: hidden;
display: flex;
flex-grow: 1;
align-items: center;
@ -162,7 +206,7 @@
.draggable {
position: absolute;
left: var(--space-4);
right: var(--space-4);
top: var(--space-6);
opacity: 0;
display: flex;
@ -183,8 +227,8 @@
.branch-popup-menu {
position: absolute;
top: calc(var(--space-2) + var(--space-40));
right: var(--space-12);
top: calc(100% + var(--space-4));
right: 0;
z-index: 10;
}
@ -192,6 +236,7 @@
color: var(--clr-theme-scale-ntrl-50);
padding-left: var(--space-4);
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
text-overflow: ellipsis;
overflow-x: hidden;
@ -200,18 +245,35 @@
}
.status-tag {
padding: var(--space-2) var(--space-4);
display: flex;
gap: var(--space-2);
padding: var(--space-2) var(--space-6) var(--space-2) var(--space-4);
border-radius: var(--radius-s);
margin-right: var(--space-2);
}
.pending {
color: var(--clr-theme-scale-ntrl-40);
background: var(--clr-theme-container-sub);
color: var(--clr-theme-scale-pop-30);
background: var(--clr-theme-scale-pop-80);
}
.pending-name {
background: color-mix(in srgb, var(--clr-theme-scale-ntrl-50) 10%, transparent);
border-radius: var(--radius-m);
line-height: 120%;
height: var(--space-20);
display: flex;
align-items: center;
padding: 0 var(--space-6);
}
.deleted {
color: var(--clr-theme-scale-warn-30);
background: var(--clr-theme-warn-container-dim);
}
.remote {
color: var(--clr-theme-scale-ntrl-100);
background: var(--clr-theme-scale-ntrl-40);
}
</style>

View File

@ -15,7 +15,6 @@
export let base: BaseBranch | undefined | null;
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let maximized = false;
export let branchCount = 1;
export let user: User | undefined;
export let projectPath: string;
@ -36,7 +35,7 @@
}
</script>
<div class="wrapper card">
<div class="wrapper">
<BranchCard
{branch}
{readonly}
@ -46,7 +45,6 @@
{branchController}
{selectedOwnership}
bind:commitBoxOpen
{maximized}
{branchCount}
{user}
{selectedFiles}
@ -73,11 +71,10 @@
<style lang="postcss">
.wrapper {
display: flex;
height: 100%;
align-items: self-start;
flex-shrink: 0;
}
.card {
flex-direction: row;
position: relative;
}
</style>

View File

@ -228,6 +228,7 @@
background: var(--clr-theme-container-light);
border-top: 1px solid var(--clr-theme-container-outline-light);
transition: background-color var(--transition-medium);
border-radius: 0 0 var(--radius-m) var(--radius-m);
}
.commit-box__expander {
display: flex;

View File

@ -14,33 +14,48 @@
export let type: CommitStatus;
export let githubService: GitHubService;
export let readonly: boolean;
export let branchCount: number = 0;
let headerHeight: number;
$: headCommit = branch.commits[0];
$: commits = branch.commits.filter((c) => c.status == type);
$: upstreamCommitCount = branch.upstream?.commits.length;
$: commits =
type == 'upstream' ? branch.upstream?.commits : branch.commits.filter((c) => c.status == type);
let expanded = true;
</script>
{#if commits.length > 0}
<div class="commit-list" style:min-height={expanded ? `${2 * headerHeight}px` : undefined}>
<CommitListHeader bind:expanded {type} bind:height={headerHeight} />
{#if commits && commits.length > 0}
<div
class="commit-list card"
class:upstream={type == 'upstream'}
style:min-height={expanded ? `${2 * headerHeight}px` : undefined}
>
<CommitListHeader {type} {upstreamCommitCount} bind:expanded bind:height={headerHeight} />
{#if expanded}
<div class="commit-list__content">
<div class="commits">
{#each commits as commit, idx (commit.id)}
<CommitListItem
{branch}
{branchController}
{commit}
{base}
{project}
{readonly}
isChained={idx != commits.length - 1}
isHeadCommit={commit.id === headCommit?.id}
/>
{/each}
{#if commits}
{#each commits as commit, idx (commit.id)}
<CommitListItem
{branch}
{branchController}
{commit}
{base}
{project}
{readonly}
isChained={idx != commits.length - 1}
isHeadCommit={commit.id === headCommit?.id}
/>
{/each}
{/if}
</div>
{#if type == 'upstream' && branchCount > 1}
<div class="upstream-message text-base-body-11">
You have {branchCount} active branches. To merge upstream work, we will unapply all other
branches.
</div>{/if}
<CommitListFooter
{branchController}
{branch}
@ -57,10 +72,13 @@
<style lang="postcss">
.commit-list {
&.upstream {
background-color: var(--clr-theme-container-pale);
}
background-color: var(--clr-theme-container-light);
display: flex;
flex-direction: column;
border-top: 1px solid var(--clr-theme-container-outline-light);
/* border-top: 1px solid var(--clr-theme-container-outline-light); */
position: relative;
flex-shrink: 0;
}
@ -70,4 +88,11 @@
padding: 0 var(--space-16) var(--space-20) var(--space-16);
gap: var(--space-8);
}
.upstream-message {
color: var(--clr-theme-scale-warn-30);
border-radius: var(--radius-m);
background: var(--clr-theme-scale-warn-80);
padding: var(--space-12);
margin-left: var(--space-16);
}
</style>

View File

@ -20,6 +20,7 @@
$: pr$ = githubService.get(branch.upstreamName);
let isPushing: boolean;
let isMerging: boolean;
async function push() {
isPushing = true;
@ -97,6 +98,24 @@
Push
{/if}
</Button>
{:else if type == 'upstream'}
<Button
wide
color="warn"
loading={isMerging}
on:click={async () => {
isMerging = true;
try {
await branchController.mergeUpstream(branch.id);
} catch (err) {
toast.error('Failed to merge upstream commits');
} finally {
isMerging = false;
}
}}
>
Merge upstream commits
</Button>
{/if}
</div>
{/if}

View File

@ -6,6 +6,7 @@
export let expanded: boolean;
export let type: CommitStatus;
export let height: number | undefined;
export let upstreamCommitCount = 0;
let element: HTMLButtonElement | undefined = undefined;
@ -31,6 +32,9 @@
Remote branch
{:else if type == 'integrated'}
Integrated
{:else if type == 'upstream'}
{upstreamCommitCount} upstream {upstreamCommitCount == 1 ? 'commit' : 'commits'}
<Icon name="warning" color="warn" />
{/if}
</div>
<div class="expander">

View File

@ -10,7 +10,7 @@
} from '$lib/draggables';
import { dropzone } from '$lib/utils/draggable';
import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranch, Branch, Commit } from '$lib/vbranches/types';
import { RemoteCommit, type BaseBranch, type Branch, type Commit } from '$lib/vbranches/types';
import { get } from 'svelte/store';
import CommitCard from './CommitCard.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
@ -18,14 +18,17 @@
export let branch: Branch;
export let project: Project;
export let commit: Commit;
export let commit: Commit | RemoteCommit;
export let base: BaseBranch | undefined | null;
export let isHeadCommit: boolean;
export let isChained: boolean;
export let readonly = false;
export let branchController: BranchController;
function acceptAmend(commit: Commit) {
function acceptAmend(commit: Commit | RemoteCommit) {
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: any) => {
if (!project.ok_with_force_push && commit.isRemote) {
return false;
@ -60,7 +63,10 @@
}
}
function acceptSquash(commit: Commit) {
function acceptSquash(commit: Commit | RemoteCommit) {
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: any) => {
if (!isDraggableCommit(data)) return false;
if (data.branchId != branch.id) return false;
@ -79,7 +85,10 @@
};
}
function onSquash(commit: Commit) {
function onSquash(commit: Commit | RemoteCommit) {
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: DraggableCommit) => {
if (data.commit.isParentOf(commit)) {
branchController.squashBranchCommit(data.branchId, commit.id);

View File

@ -116,13 +116,9 @@
...draggableFile(branchId, file, writable([file])),
disabled: readonly
}}
style:width={`${fileWidth || $defaultFileWidthRem}rem`}
>
<div
id={`file-${file.id}`}
class="file-card"
style:width={`${fileWidth || $defaultFileWidthRem}rem`}
class:opacity-80={isFileLocked}
>
<div id={`file-${file.id}`} class="file-card card">
<FileCardHeader {file} {isFileLocked} on:close />
{#if conflicted}
<div class="mb-2 bg-red-500 px-2 py-0 font-bold text-white">
@ -251,12 +247,20 @@
</div>
<style lang="postcss">
.resize-viewport {
position: relative;
display: flex;
height: 100%;
align-items: self-start;
overflow: hidden;
padding: var(--space-16) var(--space-6);
}
.file-card {
background: var(--clr-theme-container-light);
border-left: 1px solid var(--clr-theme-container-outline-light);
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 100%;
}
.hunks {
display: flex;
@ -291,7 +295,6 @@
border-radius: var(--radius-s);
border: 1px solid var(--clr-theme-container-outline-light);
overflow-x: hidden;
overscroll-behavior: none;
transition: border-color var(--transition-fast);
}
.hunk__inner_inner {
@ -312,10 +315,6 @@
.removed {
color: #ff3e00;
}
.resize-viewport {
position: relative;
display: flex;
}
@keyframes wiggle {
0% {

View File

@ -84,7 +84,6 @@
base={$baseBranch$}
{cloud}
project={$project$}
maximized={false}
readonly={true}
user={$user$}
projectPath={$project$.path}

View File

@ -4,7 +4,6 @@
border: 1px solid var(--clr-theme-container-outline-light);
border-radius: var(--radius-m);
background: var(--clr-theme-container-light);
overflow: hidden;
}
.card__header {

View File

@ -190,7 +190,7 @@
--clr-theme-warn-container-dim: var(--clr-core-warn-80);
--clr-theme-warn-element: var(--clr-core-warn-50);
--clr-theme-warn-element-dark: var(--clr-core-warn-40);
--clr-theme-warn-element-dim: var(--clr-core-warn-50);
--clr-theme-warn-element-dim: var(--clr-core-warn-45);
--clr-theme-warn-on-container: var(--clr-core-warn-40);
--clr-theme-warn-on-element: var(--clr-core-warn-95);
--clr-theme-warn-outline: var(--clr-core-warn-45);