Skip to content
Open
39 changes: 39 additions & 0 deletions apps/lite/e2e/tests/files-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { expect, test } from "../test.ts";

test.describe("files tree", () => {
test.use({ scenario: "project-in-single-branch-three-branch-stack.sh" });

test("a directory row offers a menu and answers for the files below it", async ({
appWindow,
testEnvironment,
}) => {
const clone = path.join(testEnvironment.workdir, "local-clone");
mkdirSync(path.join(clone, "src", "ui"), { recursive: true });
writeFileSync(path.join(clone, "src", "ui", "row.txt"), "an uncommitted file\n");
await appWindow.reload();
await appWindow.getByRole("main").waitFor();

const uncommittedFiles = appWindow.getByRole("tree", { name: "Uncommitted" });
// A chain of directories holding nothing but the next one arrives as one row.
const directory = uncommittedFiles.getByRole("treeitem", { name: "Directory src/ui" });
await expect(directory).toHaveAttribute("aria-expanded", "true");

// The toolbar shows itself for the selected row, as it does for a file.
await directory.click();
await expect(directory.getByRole("button", { name: "Directory menu" })).toBeVisible();

// Checking the folder checks every file below it.
await uncommittedFiles.getByRole("checkbox", { name: "Check directory src/ui" }).click();
await expect(
uncommittedFiles.getByRole("checkbox", { name: "Check file src/ui/row.txt" }),
).toBeChecked();

await uncommittedFiles.getByRole("button", { name: "Collapse directory src/ui" }).click();
await expect(directory).toHaveAttribute("aria-expanded", "false");
await expect(
uncommittedFiles.getByRole("treeitem", { name: "Addition src/ui/row.txt" }),
).toBeHidden();
});
});
33 changes: 8 additions & 25 deletions apps/lite/ui/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ import {
} from "#ui/pr-description-generation.ts";
import { errorMessageForToast } from "#ui/errors.ts";
import { oversizedFile, toBase64, UPLOAD_SIZE_LIMIT } from "#ui/uploads.ts";
import { createDiffSpec, resolveDiffSpecs } from "#ui/operations/diff-specs.ts";
import { resolveDiffSpecs } from "#ui/operations/diff-specs.ts";
import {
discardChangesToastOptions,
rejectedChangesToastOptions,
} from "#ui/operations/toastOptions.tsx";
import { commitAddress, addressEquals, type FileParent } from "#ui/addresses.ts";
import { commitAddress, type Address, type FileParent } from "#ui/addresses.ts";
import { projectSlice } from "#ui/projects/state.ts";
import { projectAiSettingsQueryOptions } from "#ui/project-ai-settings.ts";
import { type AppDispatch, useAppDispatch, useAppStore } from "#ui/store.ts";
import { type AppDispatch, useAppDispatch } from "#ui/store.ts";
import { formatRelativeTime } from "@gitbutler/ui-react/time.ts";
import { Toast } from "@base-ui/react";
import { Match } from "effect";
Expand Down Expand Up @@ -1145,7 +1145,6 @@ export const useDiscardFileChanges = ({
projectId: string;
fileParent: FileParent;
}) => {
const store = useAppStore();
const queryClient = useQueryClient();
const toastManager = Toast.useToastManager();
const { isPending: isCommitDiscardChangesPending, mutate: commitDiscardChanges } =
Expand Down Expand Up @@ -1176,29 +1175,13 @@ export const useDiscardFileChanges = ({
);

/**
* Discard `change`, extended to the checked files when `extendToCheckedFiles` — a row's menu
* passes its own checked state, as dragging does; a list hotkey passes true, as cut and move do.
* A caller with no row of its own, like the checked-set toolbar, passes a null `change` and
* leans wholly on the checked set.
* Discard the files `sources` names. Which files those are — one row, the files below a
* directory, or the checked set — is the caller's to decide; see `useFileSetSubject`.
*/
const discard = async ({
change,
extendToCheckedFiles,
}: {
change: TreeChange | null;
extendToCheckedFiles: boolean;
}): Promise<void> => {
const sources = projectSlice.selectors.selectCheckedAddresses(store.getState(), projectId);

const areAllFilesUnder = () =>
sources.every(
(address) => address._tag === "File" && addressEquals(address.parent, fileParent),
);

if (!extendToCheckedFiles || sources.length === 0 || !areAllFilesUnder())
return change === null ? undefined : runDiscard([createDiffSpec(change, [])]);
const discard = async (sources: Array<Address>): Promise<void> => {
if (sources.length === 0) return;

// Checked files carry only paths, so their changes have to be looked up.
// The sources carry only paths, so their changes have to be looked up.
try {
const changes = await resolveDiffSpecs({ projectId, queryClient, sources });
// One of them gone stale fails resolution for the whole set — the reconciler is about to
Expand Down
54 changes: 0 additions & 54 deletions apps/lite/ui/src/routes/project/$id/workspace/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { SuspenseQuery } from "@suspensive/react-query";
import {
type PushBeforePublish,
useAddReviewLabels,
useCommitUncommitChanges,
useOpenInProgram,
useRequestReview,
useResolveCommitConflictHunks,
Expand Down Expand Up @@ -60,7 +59,6 @@ import {
type FileAddress,
fileAddress,
hunkAddress,
addressEquals,
type FileParent,
type HunkAddress,
type Address,
Expand Down Expand Up @@ -155,7 +153,6 @@ import {
type LineStats,
} from "#ui/routes/project/$id/workspace/lineStats.ts";
import { FilesTree } from "#ui/routes/project/$id/workspace/FilesTree.tsx";
import { createDiffSpec } from "#ui/operations/diff-specs.ts";
import { TopLeftControls } from "#ui/routes/project/$id/workspace/TopLeftControls.tsx";
import {
changeFileRowItem,
Expand Down Expand Up @@ -469,8 +466,6 @@ const DiffContents: FC<{
pendingFileRef: RefObject<FileAddress | null>;
renderAllFiles: boolean;
minimapFiles: Array<MinimapFile> | null;
canUncommit: boolean;
uncommit: (change: TreeChange, extendToCheckedFiles: boolean) => void;
}> = ({
activeFileItemId,
diffContextKey,
Expand All @@ -495,8 +490,6 @@ const DiffContents: FC<{
pendingFileRef,
renderAllFiles,
minimapFiles,
canUncommit,
uncommit,
}) => {
const dispatch = useAppDispatch();
const newFocusableAnnotationIdRef = useRef<string | null>(null);
Expand Down Expand Up @@ -1771,8 +1764,6 @@ const DiffContents: FC<{
selected={item.id === selectedFileItemId}
setCollapsed={handleSetCollapsed(item.id)}
setReviewed={handleSetReviewed(item.id, file.change.path, version)}
canUncommit={canUncommit}
uncommit={uncommit}
/>
);
}}
Expand Down Expand Up @@ -1971,8 +1962,6 @@ type DiffFileHeaderProps = {
selected: boolean;
setCollapsed: (collapsed: boolean) => void;
setReviewed: (reviewed: boolean) => void;
canUncommit: boolean;
uncommit: (change: TreeChange, extendToCheckedFiles: boolean) => void;
};

const DiffFileHeader: FC<DiffFileHeaderProps> = (p) => {
Expand All @@ -1981,8 +1970,6 @@ const DiffFileHeader: FC<DiffFileHeaderProps> = (p) => {
address: p.address,
path: p.change.path,
change: p.change,
canUncommit: p.canUncommit,
uncommit: p.uncommit,
});

const lastSepIdx = p.change.path.lastIndexOf("/");
Expand Down Expand Up @@ -2333,7 +2320,6 @@ const Diff: FC<{
headerSlot,
}) => {
const focusScopeRef = useRef<HTMLDivElement>(null);
const store = useAppStore();
const dispatch = useAppDispatch();
const { mutate: setFilesReviewed } = useSetFilesReviewed();
const [manualCollapseByItem, setManualCollapseByItem] = useState<Map<string, boolean>>(new Map());
Expand Down Expand Up @@ -2411,42 +2397,6 @@ const Diff: FC<{
[selection],
);

const { isPending: isCommitUncommitChangesPending, mutate: commitUncommitChanges } =
useCommitUncommitChanges();

const uncommit = (change: TreeChange, extendToCheckedFiles: boolean): void => {
if (fileParent._tag !== "Commit") return;

const sources = projectSlice.selectors.selectCheckedAddresses(store.getState(), projectId);

let subjectChanges = [change];
if (
extendToCheckedFiles &&
sources.length > 0 &&
sources.every(
(address) => address._tag === "File" && addressEquals(address.parent, fileParent),
)
) {
const checkedChanges = sources
.values()
.map((source) =>
changes.find((candidate) => source._tag === "File" && candidate.path === source.path),
)
.filter((x) => x != null)
.toArray();
if (checkedChanges.length !== sources.length) return;

subjectChanges = checkedChanges;
}

commitUncommitChanges({
projectId,
commitId: fileParent.commitId,
assignTo: null,
changes: subjectChanges.map((change) => createDiffSpec(change, [])),
dryRun: false,
});
};
const reviewedFilesContextId = weakFileParentIdentityKey(fileParent);
const { data: reviewedFiles } = useSuspenseQuery(
reviewedFilesQueryOptions(projectId, reviewedFilesContextId),
Expand Down Expand Up @@ -2778,8 +2728,6 @@ const Diff: FC<{
addressSpace={filesAddressSpace}
fileParent={fileParent}
reviewedPaths={reviewedFilePaths}
canUncommit={!isCommitUncommitChangesPending}
uncommit={uncommit}
ref={filesTreeRef}
/>
</div>
Expand Down Expand Up @@ -2898,8 +2846,6 @@ const Diff: FC<{
manualCollapseByItem={manualCollapseByItem}
setManualCollapse={setManualCollapse}
setFilesReviewed={setFilesReviewed}
canUncommit={!isCommitUncommitChangesPending}
uncommit={uncommit}
focusScopeRef={focusScopeRef}
viewerRef={viewerRef}
didScrollToViaFileRef={didScrollToViaFileRef}
Expand Down
Loading
Loading