-
Notifications
You must be signed in to change notification settings - Fork 570
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- distinguish old FileList items from new with ChangeList name - rewritten id selection mechanism - no "ownership" support yet for committing
- Loading branch information
Showing
20 changed files
with
919 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
112
apps/desktop/src/components/ChangeListItemWrapper.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.