Skip to content

Commit

Permalink
Merge pull request #60 from alirezamirian/2024-06
Browse files Browse the repository at this point in the history
2024 06
  • Loading branch information
alirezamirian authored Jul 2, 2024
2 parents 0e52e44 + 7c3fb4c commit c04c4c6
Show file tree
Hide file tree
Showing 61 changed files with 1,463 additions and 430 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
# Without this, we would need to "finalize" percy build explicitly. More info: https://docs.percy.io/docs/parallel-test-suites
PERCY_PARALLEL_TOTAL: 15 # 10 for component tests, 5 for e2e
# Percy's nonce is supposed to be set to run id by default, but it's not. Plus, appending run_number makes it more reliable in case of rerunning
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
run: yarn run cypress:component --record --parallel --group component

Expand Down Expand Up @@ -147,7 +147,7 @@ jobs:
# Without this, we would need to "finalize" percy build explicitly. More info: https://docs.percy.io/docs/parallel-test-suites
PERCY_PARALLEL_TOTAL: 15 # 10 for component tests, 5 for e2e
# Percy's nonce is supposed to be set to run id by default, but it's not. Plus, appending run_number makes it more reliable in case of rerunning
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
run: |
yarn workspace jui-example-app run serve &
Expand Down
90 changes: 90 additions & 0 deletions packages/example-app/src/Project/actions/NewItemPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Input,
Popup,
PopupLayout,
PositionedTooltipTrigger,
styled,
ValidationTooltip,
} from "@intellij-platform/core";
import React, { ChangeEvent, useState } from "react";

const StyledInput = styled(Input)`
width: 20.5rem;
/**
* To have the validation box shadow not clipped by the popup.
* Maybe it should be an option on input to make sure margin is always in sync with the box-shadow thickness
*/
margin: 3px;
input {
padding-top: 1px;
padding-bottom: 1px;
}
`;
const StyledHeader = styled(Popup.Header)`
border-bottom: 1px solid ${({ theme }) => theme.commonColors.border()};
`;

/**
* A simple Popup with an input, used in actions such as create file, or create directory
*/
export function NewItemPopup({
title,
inputName = "Name",
onSubmit,
validationMessage,
validationType = "error",
value,
onChange,
}: {
inputName?: string;
title: React.ReactNode;
onSubmit: () => void;
value: string;
onChange: (newValue: string) => void;
validationMessage?: string;
validationType?: "error" | "warning";
}) {
const [hideMessage, setHideMessage] = useState(false);
return (
<Popup>
<PopupLayout
header={<StyledHeader>{title}</StyledHeader>}
content={
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
onMouseDown={() => {
setHideMessage(true);
}}
>
<PositionedTooltipTrigger
isOpen={Boolean(validationMessage) && !hideMessage}
placement="top start"
offset={6}
crossOffset={-6}
tooltip={
<ValidationTooltip type={validationType}>
{validationMessage}
</ValidationTooltip>
}
>
<StyledInput
appearance="embedded"
validationState={validationMessage ? validationType : undefined}
placeholder={inputName}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setHideMessage(false);
onChange(e.target.value);
}}
/>
</PositionedTooltipTrigger>
</form>
}
/>
</Popup>
);
}
23 changes: 23 additions & 0 deletions packages/example-app/src/Project/actions/copyAbsolutePath.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { selector } from "recoil";
import { ActionDefinition } from "@intellij-platform/core";

import { activePathExistsState, activePathsState } from "../project.state";
import { projectActionIds } from "../projectActionIds";
import { notImplemented } from "../notImplemented";

export const copyAbsolutePathActionState = selector({
key: `action.${projectActionIds.CopyAbsolutePath}`,
get: ({ get, getCallback }): ActionDefinition => ({
id: projectActionIds.CopyAbsolutePath,
title: "Absolute Path",
isDisabled: !get(activePathExistsState),
useShortcutsOf: "CopyPaths",
actionPerformed: getCallback(({ snapshot }) => async () => {
notImplemented();
const activePaths = snapshot.getLoadable(activePathsState).getValue();
if (activePaths.length === 0) {
return;
}
}),
}),
});
22 changes: 22 additions & 0 deletions packages/example-app/src/Project/actions/copyFilename.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { selector } from "recoil";
import { ActionDefinition } from "@intellij-platform/core";

import { activePathExistsState, activePathsState } from "../project.state";
import { projectActionIds } from "../projectActionIds";
import { notImplemented } from "../notImplemented";

export const copyFilenameActionState = selector({
key: `action.${projectActionIds.CopyFileName}`,
get: ({ get, getCallback }): ActionDefinition => ({
id: projectActionIds.CopyFileName,
title: "File Name",
isDisabled: !get(activePathExistsState),
actionPerformed: getCallback(({ snapshot }) => async () => {
notImplemented();
const activePaths = snapshot.getLoadable(activePathsState).getValue();
if (activePaths.length === 0) {
return;
}
}),
}),
});
135 changes: 135 additions & 0 deletions packages/example-app/src/Project/actions/createDirectoryAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import path from "path";
import { selector, useRecoilCallback } from "recoil";
import React, { useEffect, useState } from "react";
import { ActionDefinition, PlatformIcon } from "@intellij-platform/core";

import { fs } from "../../fs/fs";
import { stat } from "../../fs/fs-utils";
import { DIR_ICON } from "../../file-utils";
import { createDirectoryCallback } from "../fs-operations";
import { useCancelableAsyncCallback } from "../../useCancelableAsyncCallback";
import {
activePathExistsState,
activePathsState,
projectPopupManagerRefState,
} from "../project.state";
import { NewItemPopup } from "./NewItemPopup";
import { projectActionIds } from "../projectActionIds";

// TODO: expand to and select the new directory in the project tree
export const createDirectoryActionState = selector({
key: `action.${projectActionIds.NewDir}`,
get: ({ get, getCallback }): ActionDefinition => ({
id: projectActionIds.NewDir,
icon: <PlatformIcon icon={DIR_ICON} />,
title: "Directory",
description: "Create new directory",
isDisabled: !get(activePathExistsState), // TODO: disable action when multiple paths are selected and none of them are directories
actionPerformed: getCallback(({ snapshot, refresh }) => async () => {
const activePaths = snapshot.getLoadable(activePathsState).getValue();
if (activePaths.length === 0) {
return;
}
const popupManager = snapshot
.getLoadable(projectPopupManagerRefState)
.getValue().current;

// TODO: open a dialog and let the user choose the destination if, multiple dirs are active
const destinationDir = (
await fs.promises.stat(activePaths[0])
).isDirectory()
? activePaths[0]
: path.dirname(activePaths[0]);

if (!popupManager) {
throw new Error("Could not find popup manager");
}

popupManager.show(({ close }) => (
<NewDirectoryPopup close={close} destinationDir={destinationDir} />
));
}),
}),
});

function NewDirectoryPopup({
destinationDir,
close,
}: {
close: () => void;
destinationDir: string;
}) {
const createDirectory = useRecoilCallback(createDirectoryCallback, []);

const [dirName, setDirName] = useState("");

const [validationResult, setValidationResult] = useState<{
type: "error" | "warning";
message: string;
} | null>(null);

const updateValidation = useCancelableAsyncCallback(function* (
destinationDir: string,
dirName: string
) {
setValidationResult(
(yield validate(destinationDir, dirName)) as Awaited<
ReturnType<typeof validate>
>
);
});
useEffect(() => {
updateValidation(destinationDir, dirName);
}, [destinationDir, dirName]);

const submit = async () => {
const error = await validate(destinationDir, dirName);
if (error == null) {
await createDirectory(destinationDir, dirName);
close();
}
};
return (
<NewItemPopup
title="New Directory"
onSubmit={() => {
if (dirName) {
submit(); // error handling?
}
}}
value={dirName}
onChange={setDirName}
validationMessage={validationResult?.message}
validationType={validationResult?.type}
/>
);
}

const validate = async function (
destinationDir: string,
dirName: string
): Promise<{ type: "error" | "warning"; message: string } | null> {
const fullPath = path.join(destinationDir, dirName);
if (!dirName) {
return null;
}
if (dirName.includes(".")) {
return {
type: "warning",
message: `Note: "." in the name is treated as a regular character. Use "/" instead if you mean to create nested directories`,
};
}
let stats = await stat(fullPath);
if (stats?.isFile()) {
return {
type: "error",
message: `A file with the name '${dirName}' already exists`,
};
} else if (stats?.isDirectory()) {
return {
type: "error",
message: `A directory with the name '${dirName}' already exists`,
};
}
return null;
};
Loading

0 comments on commit c04c4c6

Please sign in to comment.