Skip to content

Commit

Permalink
Render uncommitted file list
Browse files Browse the repository at this point in the history
- distinguish old FileList items from new with ChangeList name
- rewritten id selection mechanism
- no "ownership" support yet for committing
  • Loading branch information
mtsgrd committed Jan 24, 2025
1 parent 59b1c2d commit 65f96ac
Show file tree
Hide file tree
Showing 20 changed files with 919 additions and 61 deletions.
170 changes: 170 additions & 0 deletions apps/desktop/src/components/ChangeContextMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<!-- This is a V3 replacement for `FileContextMenu.svelte` -->
<script lang="ts">
import { BranchController } from '$lib/branches/branchController';
import { LocalFile } from '$lib/files/file';
import { Project } from '$lib/project/project';
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { getContextStoreBySymbol } from '@gitbutler/shared/context';
import { getContext } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte';
import ContextMenu from '@gitbutler/ui/ContextMenu.svelte';
import ContextMenuItem from '@gitbutler/ui/ContextMenuItem.svelte';
import ContextMenuSection from '@gitbutler/ui/ContextMenuSection.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
import * as toasts from '@gitbutler/ui/toasts';
import { join } from '@tauri-apps/api/path';
import type { Writable } from 'svelte/store';
interface Props {
isUnapplied: boolean;
branchId?: string;
trigger?: HTMLElement;
isBinary?: boolean;
}
const { branchId, trigger, isUnapplied, isBinary = false }: Props = $props();
const branchController = getContext(BranchController);
const project = getContext(Project);
const userSettings = getContextStoreBySymbol<Settings, Writable<Settings>>(SETTINGS);
let confirmationModal: ReturnType<typeof Modal> | undefined;
let contextMenu: ReturnType<typeof ContextMenu>;
function isDeleted(item: any): boolean {
if (!item.files || !Array.isArray(item.files)) return false;
return item.files.some((f: unknown) => {
if (!(typeof f === 'string')) return false;
return true;
// return computeChangeStatus(f) === 'D';
});
}
function confirmDiscard(item: any) {
if (!branchId) {
console.error('Branch ID is not set');
toasts.error('Failed to discard changes');
return;
}
branchController.unapplyFiles(branchId, item.files);
close();
}
export function open(e: MouseEvent, item: any) {
contextMenu.open(e, item);
}
</script>

<ContextMenu bind:this={contextMenu} rightClickTrigger={trigger}>
{#snippet children(item)}
<ContextMenuSection>
{#if item.files && item.files.length > 0}
{@const files = item.files}
{#if files[0] instanceof LocalFile && !isUnapplied && !isBinary}
<ContextMenuItem
label="Discard changes"
onclick={() => {
confirmationModal?.show(item);
contextMenu.close();
}}
/>
{/if}
{#if files.length === 1}
<ContextMenuItem
label="Copy Path"
onclick={async () => {
try {
if (!project) return;
const absPath = await join(project.path, item.files[0].path);
navigator.clipboard.writeText(absPath);
contextMenu.close();
// dismiss();
} catch (err) {
console.error('Failed to copy path', err);
toasts.error('Failed to copy path');
}
}}
/>
<ContextMenuItem
label="Copy Relative Path"
onclick={() => {
try {
if (!project) return;
navigator.clipboard.writeText(item.files[0].path);
contextMenu.close();
} catch (err) {
console.error('Failed to copy relative path', err);
toasts.error('Failed to copy relative path');
}
}}
/>
{/if}
<ContextMenuItem
label="Open in {$userSettings.defaultCodeEditor.displayName}"
disabled={isDeleted(item)}
onclick={async () => {
try {
if (!project) return;
for (let file of item.files) {
const path = getEditorUri({
schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
path: [project.vscodePath, file.path]
});
openExternalUrl(path);
}
contextMenu.close();
} catch {
console.error('Failed to open in editor');
toasts.error('Failed to open in editor');
}
}}
/>
{/if}
</ContextMenuSection>
{/snippet}
</ContextMenu>

<Modal
width="small"
type="warning"
title="Discard changes"
bind:this={confirmationModal}
onSubmit={confirmDiscard}
>
{#snippet children(item)}
{#if item.files.length < 10}
<p class="discard-caption">
Are you sure you want to discard the changes<br />to the following files:
</p>
<ul class="file-list">
{#each item.files as file}
<FileListItem filePath={file.path} fileStatus={file.status} clickable={false} />
{/each}
</ul>
{:else}
Discard the changes to all <span class="text-bold">
{item.files.length} files
</span>?
{/if}
{/snippet}
{#snippet controls(close, item)}
<Button kind="outline" onclick={close}>Cancel</Button>
<Button style="error" type="submit" onclick={() => confirmDiscard(item)}>Confirm</Button>
{/snippet}
</Modal>

<style lang="postcss">
.discard-caption {
color: var(--clr-text-2);
}
.file-list {
padding: 4px 0;
border-radius: var(--radius-m);
overflow: hidden;
background-color: var(--clr-bg-2);
margin-top: 12px;
}
</style>
74 changes: 74 additions & 0 deletions apps/desktop/src/components/ChangeList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!-- This is a V3 replacement for `BranchFileList.svelte` -->
<script lang="ts">
import ChangeListItemWrapper from './ChangeListItemWrapper.svelte';
import LazyloadContainer from '$components/LazyloadContainer.svelte';
import { IdSelection } from '$lib/selection/idSelection.svelte';
import { selectFilesInList, updateSelection } from '$lib/selection/idSelectionUtils';
import { chunk } from '$lib/utils/array';
import { sortLikeFileTree } from '$lib/worktree/changeTree';
import { getContext } from '@gitbutler/shared/context';
import type { TreeChange } from '$lib/hunks/change';
interface Props {
changes: TreeChange[];
projectId: string;
/** The commit ID these changes belong to, if any. */
commitId?: string;
}
const { changes: files, projectId, commitId }: Props = $props();
let currentDisplayIndex = $state(0);
const fileChunks: TreeChange[][] = $derived(chunk(sortLikeFileTree(files), 100));
const visibleFiles: TreeChange[] = $derived(fileChunks.slice(0, currentDisplayIndex + 1).flat());
const idSelection = getContext(IdSelection);
function handleKeyDown(e: KeyboardEvent) {
updateSelection({
allowMultiple: true,
metaKey: e.metaKey,
shiftKey: e.shiftKey,
key: e.key,
targetElement: e.currentTarget as HTMLElement,
files: visibleFiles,
selectedFileIds: idSelection.values(),
fileIdSelection: idSelection,
commitId,
preventDefault: () => e.preventDefault()
});
}
function loadMore() {
if (currentDisplayIndex + 1 >= fileChunks.length) return;
currentDisplayIndex += 1;
}
</script>

{#if visibleFiles.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();
}}
role="listbox"
onkeydown={handleKeyDown}
>
{#each visibleFiles as change (change.path)}
<ChangeListItemWrapper
{change}
{projectId}
selected={idSelection.has(change.path, commitId)}
onclick={(e) => {
selectFilesInList(e, change, visibleFiles, idSelection, true, commitId);
}}
/>
{/each}
</LazyloadContainer>
{/if}

<style lang="postcss">
</style>
112 changes: 112 additions & 0 deletions apps/desktop/src/components/ChangeListItemWrapper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!-- This is a V3 replacement for `FileListItemWrapper.svelte` -->
<script lang="ts">
import ChangeContextMenu from './ChangeContextMenu.svelte';
import { BranchStack } from '$lib/branches/branch';
import { draggableChips, type DraggableConfig } from '$lib/dragging/draggable';
import { ChangeDropData } from '$lib/dragging/draggables';
import { getFilename } from '$lib/files/utils';
import { IdSelection } from '$lib/selection/idSelection.svelte';
import { key } from '$lib/selection/key';
import { computeChangeStatus } from '$lib/utils/fileStatus';
import { getContext, maybeGetContextStore } from '@gitbutler/shared/context';
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
import type { TreeChange } from '$lib/hunks/change';
interface Props {
change: TreeChange;
commitId?: string;
projectId: string;
selected: boolean;
showCheckbox?: boolean;
onclick: (e: MouseEvent) => void;
onkeydown?: (e: KeyboardEvent) => void;
}
const {
change: change,
commitId,
projectId,
selected,
showCheckbox,
onclick,
onkeydown
}: Props = $props();
const stack = maybeGetContextStore(BranchStack);
const stackId = $derived($stack?.id);
const idSelection = getContext(IdSelection);
let contextMenu = $state<ReturnType<typeof ChangeContextMenu>>();
let draggableEl: HTMLDivElement | undefined = $state();
let indeterminate = $state(false);
let checked = $state(false);
// TODO: Refactor to use this as a Svelte action, e.g. `use:draggableChips()`.
let chips:
| {
update: (opts: DraggableConfig) => void;
destroy: () => void;
}
| undefined;
// Manage the lifecycle of the draggable chips.
$effect(() => {
if (draggableEl) {
const dropData = new ChangeDropData(stackId || '', change, idSelection, commitId);
const config: DraggableConfig = {
label: getFilename(change.path),
filePath: change.path,
data: dropData,
viewportId: 'board-viewport',
selector: '.selected-draggable'
};
if (chips) {
chips.update(config);
} else {
chips = draggableChips(draggableEl, config);
}
} else {
chips?.destroy();
}
return () => {
chips?.destroy();
};
});
</script>

<ChangeContextMenu
bind:this={contextMenu}
trigger={draggableEl}
isUnapplied={false}
branchId={$stack?.id}
isBinary={false}
/>

<FileListItem
id={key(change.path, commitId)}
bind:ref={draggableEl}
filePath={change.path}
fileStatus={computeChangeStatus(change)}
{selected}
{showCheckbox}
{checked}
{indeterminate}
draggable={true}
{onclick}
{onkeydown}
locked={false}
conflicted={false}
oncontextmenu={(e) => {
const changes = idSelection.treeChanges(projectId);
if (idSelection.has(change.path, commitId)) {
contextMenu?.open(e, { files: changes });
} else {
contextMenu?.open(e, { files: [change] });
}
}}
/>

<style lang="postcss">
/* blah */
</style>
1 change: 1 addition & 0 deletions apps/desktop/src/components/FileContextMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- TODO: Delete this file after V3 has shipped. -->
<script lang="ts">
import { BranchController } from '$lib/branches/branchController';
import { LocalFile } from '$lib/files/file';
Expand Down
Loading

0 comments on commit 65f96ac

Please sign in to comment.