-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from alirezamirian/2024-06
2024 06
- Loading branch information
Showing
61 changed files
with
1,463 additions
and
430 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
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,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
23
packages/example-app/src/Project/actions/copyAbsolutePath.tsx
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,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; | ||
} | ||
}), | ||
}), | ||
}); |
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,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
135
packages/example-app/src/Project/actions/createDirectoryAction.tsx
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,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; | ||
}; |
Oops, something went wrong.