Skip to content

Commit c644934

Browse files
authored
highlight-locking-relationships (#11351)
* make "change-status" icon bolder * fix(ui): simplify-file border logic fix(file-list): pass locked prop simplify hideBorder condition * update filelistitem icons - lock icon - status icon * highlight commits with locking changes * update color tokens usage in order to support upcoming dt changes * chore(deps): upgrade @gitbutler/design-core from 1.3.9 to 1.3.11 across workspace files to pick up bug fixes and updated integrity hashes * remove unused class
1 parent ea90c4a commit c644934

File tree

11 files changed

+190
-44
lines changed

11 files changed

+190
-44
lines changed

apps/desktop/src/components/ChangedFiles.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@
105105
{draggableFiles}
106106
{ancestorMostConflictedCommitId}
107107
{allowUnselect}
108-
hideLastFileBorder={false}
109108
/>
110109
{:else}
111110
<EmptyStatePlaceholder image={emptyFolderSvg} width={180} gap={4}>

apps/desktop/src/components/CommitRow.svelte

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
<!-- svelte-ignore a11y_click_events_have_key_events -->
107107
<div
108108
data-testid={TestId.CommitRow}
109+
data-commit-id={args.disableCommitActions ? undefined : args.commitId}
109110
bind:this={container}
110111
role="button"
111112
tabindex="0"
@@ -240,6 +241,24 @@
240241
color: var(--clr-theme-err-element);
241242
}
242243
}
244+
245+
&:global(.dependency-highlighted) {
246+
animation: dependency-pulse 4s ease forwards;
247+
transition: background-color var(--transition-fast);
248+
}
249+
}
250+
251+
@keyframes dependency-pulse {
252+
0% {
253+
background-color: transparent;
254+
}
255+
5% {
256+
background-color: var(--clr-scale-warn-80);
257+
}
258+
80%,
259+
100% {
260+
background-color: var(--clr-scale-warn-90);
261+
}
243262
}
244263
245264
.commit-row__select-indicator {

apps/desktop/src/components/FileList.svelte

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
import { AI_SERVICE } from '$lib/ai/service';
99
import { projectAiGenEnabled } from '$lib/config/config';
1010
import { conflictEntryHint } from '$lib/conflictEntryPresence';
11+
import {
12+
getLockedCommitIds,
13+
getLockedStackIds,
14+
isFileLocked
15+
} from '$lib/dependencies/dependencies';
16+
import { DEPENDENCY_SERVICE } from '$lib/dependencies/dependencyService.svelte';
1117
import { editPatch } from '$lib/editMode/editPatchUtils';
1218
import { abbreviateFolders, changesToFileTree } from '$lib/files/filetreeV3';
1319
import { type TreeChange, isExecutableStatus } from '$lib/hunks/change';
@@ -35,9 +41,9 @@
3541
conflictEntries?: ConflictEntriesObj;
3642
draggableFiles?: boolean;
3743
ancestorMostConflictedCommitId?: string;
38-
hideLastFileBorder?: boolean;
3944
onselect?: (change: TreeChange) => void;
4045
allowUnselect?: boolean;
46+
showLockedIndicator?: boolean;
4147
};
4248
4349
const {
@@ -50,16 +56,17 @@
5056
conflictEntries,
5157
draggableFiles,
5258
ancestorMostConflictedCommitId,
53-
hideLastFileBorder = true,
5459
onselect,
55-
allowUnselect = true
60+
allowUnselect = true,
61+
showLockedIndicator = false
5662
}: Props = $props();
5763
5864
const focusManager = inject(FOCUS_MANAGER);
5965
const idSelection = inject(FILE_SELECTION_MANAGER);
6066
const aiService = inject(AI_SERVICE);
6167
const actionService = inject(ACTION_SERVICE);
6268
const modeService = injectOptional(MODE_SERVICE, undefined);
69+
const dependencyService = inject(DEPENDENCY_SERVICE);
6370
6471
const [autoCommit] = actionService.autoCommit;
6572
const [branchChanges] = actionService.branchChanges;
@@ -68,6 +75,12 @@
6875
let editPatchModal: EditPatchConfirmModal | undefined = $state();
6976
let selectedFilePath = $state('');
7077
78+
const filePaths = $derived(changes.map((change) => change.path));
79+
const fileDependenciesQuery = $derived(
80+
showLockedIndicator ? dependencyService.filesDependencies(projectId, filePaths) : null
81+
);
82+
const fileDependencies = $derived(fileDependenciesQuery?.result.data || []);
83+
7184
function showEditPatchConfirmation(filePath: string) {
7285
selectedFilePath = filePath;
7386
editPatchModal?.show();
@@ -240,6 +253,13 @@
240253
{#snippet fileTemplate(change: TreeChange, idx: number, depth: number = 0)}
241254
{@const isExecutable = isExecutableStatus(change.status)}
242255
{@const selected = idSelection.has(change.path, selectionId)}
256+
{@const locked = showLockedIndicator && isFileLocked(change.path, fileDependencies)}
257+
{@const lockedCommitIds = showLockedIndicator
258+
? getLockedCommitIds(change.path, fileDependencies)
259+
: []}
260+
{@const lockedStackIds = showLockedIndicator
261+
? getLockedStackIds(change.path, fileDependencies)
262+
: []}
243263
<FileListItemWrapper
244264
{selectionId}
245265
{change}
@@ -249,7 +269,10 @@
249269
{listMode}
250270
{depth}
251271
{active}
252-
hideBorder={hideLastFileBorder && idx === visibleFiles.length - 1}
272+
{locked}
273+
{lockedCommitIds}
274+
{lockedStackIds}
275+
hideBorder={idx === visibleFiles.length - 1}
253276
draggable={draggableFiles}
254277
executable={isExecutable}
255278
showCheckbox={showCheckboxes}

apps/desktop/src/components/FileListItemWrapper.svelte

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import { FILE_SELECTION_MANAGER } from '$lib/selection/fileSelectionManager.svelte';
1111
import { key, type SelectionId } from '$lib/selection/key';
1212
import { UNCOMMITTED_SERVICE } from '$lib/selection/uncommittedService.svelte';
13+
import { getStackName } from '$lib/stacks/stack';
14+
import { STACK_SERVICE } from '$lib/stacks/stackService.svelte';
1315
import { computeChangeStatus } from '$lib/utils/fileStatus';
1416
import { inject } from '@gitbutler/core/context';
1517
import { FileListItem, FileViewHeader, TestId } from '@gitbutler/ui';
@@ -39,6 +41,9 @@
3941
transparent?: boolean;
4042
sticky?: boolean;
4143
active?: boolean;
44+
locked?: boolean;
45+
lockedCommitIds?: string[];
46+
lockedStackIds?: string[];
4247
onclick?: (e: MouseEvent) => void;
4348
onkeydown?: (e: KeyboardEvent) => void;
4449
onCloseClick?: () => void;
@@ -64,17 +69,20 @@
6469
transparent,
6570
focusableOpts,
6671
active,
72+
locked,
73+
lockedCommitIds = [],
74+
lockedStackIds = [],
6775
onclick,
6876
onkeydown,
6977
onCloseClick,
7078
hideBorder,
7179
scrollContainer
7280
}: Props = $props();
73-
7481
const idSelection = inject(FILE_SELECTION_MANAGER);
7582
const uncommittedService = inject(UNCOMMITTED_SERVICE);
7683
const dropzoneRegistry = inject(DROPZONE_REGISTRY);
7784
const dragStateService = inject(DRAG_STATE_SERVICE);
85+
const stackService = inject(STACK_SERVICE);
7886
7987
let contextMenu = $state<ReturnType<typeof ChangedFilesContextMenu>>();
8088
let draggableEl: HTMLDivElement | undefined = $state();
@@ -119,6 +127,35 @@
119127
120128
const conflict = $derived(conflictEntries ? conflictEntries.entries[change.path] : undefined);
121129
const draggableDisabled = $derived(!draggable || showCheckbox);
130+
131+
const lockText = $derived.by(() => {
132+
if (!locked || lockedStackIds.length === 0) return undefined;
133+
134+
const stacks = stackService.stacks(projectId).result.data ?? [];
135+
const stackNames = stacks
136+
.filter((stack) => stack.id && lockedStackIds.includes(stack.id))
137+
.map(getStackName);
138+
139+
return stackNames.length === 1
140+
? `Depends on changes in:\n '${stackNames[0]}'`
141+
: `Depends on changes in:\n ${stackNames.join(', ')}`;
142+
});
143+
144+
function handleLockHover() {
145+
lockedCommitIds.forEach((commitId) => {
146+
const commitRows = document.querySelectorAll(`[data-commit-id="${commitId}"]`);
147+
commitRows.forEach((row) => {
148+
row.classList.add('dependency-highlighted');
149+
});
150+
});
151+
}
152+
153+
function handleLockUnhover() {
154+
const highlighted = document.querySelectorAll('.dependency-highlighted');
155+
highlighted.forEach((row) => {
156+
row.classList.remove('dependency-highlighted');
157+
});
158+
}
122159
</script>
123160

124161
<div
@@ -188,7 +225,10 @@
188225
draggable={!draggableDisabled}
189226
{onkeydown}
190227
{hideBorder}
191-
locked={false}
228+
locked={locked || false}
229+
{lockText}
230+
onlockhover={handleLockHover}
231+
onlockunhover={handleLockUnhover}
192232
conflicted={!!conflict}
193233
conflictHint={conflict ? conflictEntryHint(conflict) : undefined}
194234
{onclick}

apps/desktop/src/components/SnapshotCard.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@
234234
stackId={undefined}
235235
changes={files}
236236
listMode="list"
237-
hideLastFileBorder={false}
238237
onselect={(change) => onDiffClick(change.path)}
239238
allowUnselect={false}
240239
/>

apps/desktop/src/components/WorktreeChanges.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@
103103
{projectId}
104104
{listMode}
105105
{stackId}
106-
hideLastFileBorder={mode !== 'unassigned'}
107106
{onselect}
107+
showLockedIndicator={mode === 'unassigned'}
108108
/>
109109
</div>
110110
{/snippet}

apps/desktop/src/lib/dependencies/dependencies.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,57 @@ export function aggregateFileDependencies(
162162

163163
return [filePaths, fileDependencies];
164164
}
165+
166+
/**
167+
* Checks if a file has any locked dependencies.
168+
*
169+
* @param filePath - The path of the file to check.
170+
* @param fileDependencies - Array of file dependencies to search through.
171+
* @returns `true` if the file has any locks, `false` otherwise.
172+
*/
173+
export function isFileLocked(filePath: string, fileDependencies: FileDependencies[]): boolean {
174+
const deps = fileDependencies.find((dep) => dep.path === filePath);
175+
return deps ? deps.dependencies.some((dep) => dep.locks.length > 0) : false;
176+
}
177+
178+
/**
179+
* Extracts unique commit IDs from a file's dependencies.
180+
*
181+
* @param filePath - The path of the file to get commit IDs for.
182+
* @param fileDependencies - Array of file dependencies to search through.
183+
* @returns Array of unique commit IDs that the file depends on.
184+
*/
185+
export function getLockedCommitIds(
186+
filePath: string,
187+
fileDependencies: FileDependencies[]
188+
): string[] {
189+
const deps = fileDependencies.find((dep) => dep.path === filePath);
190+
if (!deps) return [];
191+
192+
const commitIds = new Set<string>();
193+
deps.dependencies.forEach((dep) => {
194+
dep.locks.forEach((lock) => commitIds.add(lock.commitId));
195+
});
196+
return Array.from(commitIds);
197+
}
198+
199+
/**
200+
* Extracts unique stack IDs from a file's dependencies.
201+
*
202+
* @param filePath - The path of the file to get stack IDs for.
203+
* @param fileDependencies - Array of file dependencies to search through.
204+
* @returns Array of unique stack IDs that the file depends on.
205+
*/
206+
export function getLockedStackIds(
207+
filePath: string,
208+
fileDependencies: FileDependencies[]
209+
): string[] {
210+
const deps = fileDependencies.find((dep) => dep.path === filePath);
211+
if (!deps) return [];
212+
213+
const stackIds = new Set<string>();
214+
deps.dependencies.forEach((dep) => {
215+
dep.locks.forEach((lock) => stackIds.add(lock.stackId));
216+
});
217+
return Array.from(stackIds);
218+
}

apps/web/src/routes/(home)/components/CtaButton.svelte

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,6 @@
196196
pointer-events: none;
197197
}
198198
199-
.canvas-container {
200-
position: absolute;
201-
top: 0;
202-
left: 0;
203-
width: 100%;
204-
height: 100%;
205-
}
206-
207199
.download-btn__canvas-cover {
208200
z-index: 1;
209201
position: absolute;

packages/ui/src/lib/components/file/FileListItem.svelte

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
onresolveclick?: (e: MouseEvent) => void;
4848
onkeydown?: (e: KeyboardEvent) => void;
4949
oncontextmenu?: (e: MouseEvent) => void;
50+
onlockhover?: () => void;
51+
onlockunhover?: () => void;
5052
}
5153
5254
let {
@@ -79,7 +81,9 @@
7981
ondblclick,
8082
onresolveclick,
8183
onkeydown,
82-
oncontextmenu
84+
oncontextmenu,
85+
onlockhover,
86+
onlockunhover
8387
}: Props = $props();
8488
8589
const showIndent = $derived(depth && depth > 0);
@@ -134,14 +138,6 @@
134138
<FileName {filePath} hideFilePath={listMode === 'tree'} />
135139

136140
<div class="file-list-item__details">
137-
{#if locked}
138-
<Tooltip text={lockText}>
139-
<div class="locked">
140-
<Icon name="locked-small" color="warning" />
141-
</div>
142-
</Tooltip>
143-
{/if}
144-
145141
{#if executable}
146142
<ExecutableLabel />
147143
{/if}
@@ -156,6 +152,19 @@
156152
<FileStatusBadge tooltip={fileStatusTooltip} status={fileStatus} style={fileStatusStyle} />
157153
{/if}
158154

155+
{#if locked}
156+
<Tooltip text={lockText}>
157+
<div
158+
class="locked"
159+
role="img"
160+
aria-label="File is locked due to dependencies"
161+
onmouseenter={() => onlockhover?.()}
162+
onmouseleave={() => onlockunhover?.()}
163+
>
164+
<Icon name="locked" />
165+
</div>
166+
</Tooltip>
167+
{/if}
159168
{#if onresolveclick}
160169
{#if !conflicted}
161170
<Tooltip text="Conflict resolved">
@@ -261,10 +270,13 @@
261270
display: flex;
262271
flex-grow: 1;
263272
align-items: center;
264-
gap: 6px;
273+
gap: 4px;
265274
266275
& .locked {
267276
display: flex;
277+
/* border-radius: var(--radius-l);
278+
background-color: var(--clr-theme-warn-soft); */
279+
color: var(--clr-scale-warn-60);
268280
}
269281
}
270282
</style>

0 commit comments

Comments
 (0)