Franknoirot/file tree fixes (#2525)

* Navigate between files with single-click

* Better semantic name for optional event passed into FileTree

* Bug fix: reset modeling state when navigating to a new file

* Add more context to E2E test TODO comment

* Newly-created file tree items are immediately set to renaming mode

* Bug fix: redirect to working file if you delete your current one

* Remove ContextMenu, unrelated branch

* Turn off autocorrect in renaming form

* Gracefully handle renaming a folder that our current file is inside of

* Update cargo.lock

* Fix renaming queue

* Navigate to newly-created files

* Make delete project and delete file/folder share deletion confirmation component

* Bug fix: navigate to project root if we delete our current file's parent directory

* Don't navigate to newly-created directories
This commit is contained in:
Frank Noirot
2024-05-24 18:12:39 -04:00
committed by GitHub
parent c93ed0f306
commit 062abd148f
8 changed files with 296 additions and 252 deletions

View File

@ -3576,6 +3576,10 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
* TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the
* start of the sketch.
* and a separate Safari-only bug that causes the test to fail
* if the pane is open the entire test. The maintainer of CodeMirror
* has pinpointed this to the unusual browser behavior:
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
*/
await codePaneButton.click()

196
src-tauri/Cargo.lock generated
View File

@ -344,7 +344,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -379,7 +379,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -425,7 +425,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -574,7 +574,7 @@ dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"syn_derive",
]
@ -883,7 +883,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1085,7 +1085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1095,7 +1095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1119,7 +1119,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1130,7 +1130,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1177,7 +1177,7 @@ checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"synstructure",
]
@ -1214,7 +1214,7 @@ dependencies = [
"regex",
"serde",
"serde_tokenstream",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1225,7 +1225,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1287,7 +1287,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1319,7 +1319,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1417,7 +1417,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1568,7 +1568,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1684,7 +1684,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1960,7 +1960,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -1988,7 +1988,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -2063,7 +2063,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -2461,15 +2461,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@ -2568,7 +2559,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.57"
version = "0.1.58"
dependencies = [
"anyhow",
"approx",
@ -2591,7 +2582,7 @@ dependencies = [
"kittycad-execution-plan-traits",
"lazy_static",
"mime_guess",
"parse-display 0.9.0",
"parse-display",
"reqwest 0.11.27",
"ropey",
"schemars",
@ -2627,13 +2618,13 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.1"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6e12eb45fd9a28c8e99dbdef54556246b39acee14e4aa6f0fc43636caa62d9"
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
dependencies = [
"anyhow",
"async-trait",
"base64 0.21.7",
"base64 0.22.1",
"bigdecimal",
"bytes",
"chrono",
@ -2642,10 +2633,10 @@ dependencies = [
"format_serde_error",
"futures",
"http 0.2.12",
"itertools 0.10.5",
"itertools",
"log",
"mime_guess",
"parse-display 0.8.2",
"parse-display",
"phonenumber",
"rand 0.8.5",
"reqwest 0.11.27",
@ -2672,7 +2663,7 @@ checksum = "0611fc9b9786175da21d895ffa0f65039e19c9111e94a41b7af999e3b95f045f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -3360,43 +3351,17 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "parse-display"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c"
dependencies = [
"once_cell",
"parse-display-derive 0.8.2",
"regex",
]
[[package]]
name = "parse-display"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
dependencies = [
"parse-display-derive 0.9.0",
"parse-display-derive",
"regex",
"regex-syntax 0.8.3",
]
[[package]]
name = "parse-display-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"regex",
"regex-syntax 0.7.5",
"structmeta 0.2.0",
"syn 2.0.65",
]
[[package]]
name = "parse-display-derive"
version = "0.9.0"
@ -3407,8 +3372,8 @@ dependencies = [
"quote",
"regex",
"regex-syntax 0.8.3",
"structmeta 0.3.0",
"syn 2.0.65",
"structmeta",
"syn 2.0.66",
]
[[package]]
@ -3550,7 +3515,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -3582,14 +3547,14 @@ dependencies = [
[[package]]
name = "phonenumber"
version = "0.3.4+8.13.34"
version = "0.3.5+8.13.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d888d375f2963bf06c5079665fbe53db69860879ff5a78524fe3c93c54fb7b8"
checksum = "f174c8db59b620032bd52b655fc97000458850fec0db35fcd4e802b668517ec0"
dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.12.1",
"itertools",
"lazy_static",
"nom",
"quick-xml",
@ -3618,7 +3583,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -3770,9 +3735,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.81"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
dependencies = [
"unicode-ident",
]
@ -4002,12 +3967,6 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.3"
@ -4456,7 +4415,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4565,7 +4524,7 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4576,7 +4535,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4609,7 +4568,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4630,7 +4589,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4672,7 +4631,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4931,18 +4890,6 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "structmeta"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d"
dependencies = [
"proc-macro2",
"quote",
"structmeta-derive 0.2.0",
"syn 2.0.65",
]
[[package]]
name = "structmeta"
version = "0.3.0"
@ -4951,19 +4898,8 @@ checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
dependencies = [
"proc-macro2",
"quote",
"structmeta-derive 0.3.0",
"syn 2.0.65",
]
[[package]]
name = "structmeta-derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"structmeta-derive",
"syn 2.0.66",
]
[[package]]
@ -4974,7 +4910,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -4996,7 +4932,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5029,9 +4965,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.65"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
@ -5047,7 +4983,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5064,7 +5000,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5281,7 +5217,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"syn 2.0.65",
"syn 2.0.66",
"tauri-utils",
"thiserror",
"time",
@ -5299,7 +5235,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"tauri-codegen",
"tauri-utils",
]
@ -5665,7 +5601,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5754,7 +5690,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5943,7 +5879,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -5972,7 +5908,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -6093,7 +6029,7 @@ source = "git+https://github.com/Aleph-Alpha/ts-rs#f898578d80d3e2a54080c1c046c45
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"termcolor",
]
@ -6306,7 +6242,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -6405,7 +6341,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"wasm-bindgen-shared",
]
@ -6439,7 +6375,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -6580,7 +6516,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -6686,7 +6622,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -6697,7 +6633,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]
@ -7139,7 +7075,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"syn 2.0.66",
]
[[package]]

View File

@ -11,6 +11,7 @@ import {
InterpreterFrom,
Prop,
StateFrom,
assign,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
@ -37,7 +38,7 @@ export const FileMachineProvider = ({
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { project, file } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
context: {
@ -53,8 +54,32 @@ export const FileMachineProvider = ({
context.selectedDirectory + sep() + event.data.name
)}`
)
} else if (
event.data &&
'path' in event.data &&
event.data.path.endsWith(FILE_EXT)
) {
// Don't navigate to newly created directories
navigate(`${paths.FILE}/${encodeURIComponent(event.data.path)}`)
}
},
addFileToRenamingQueue: assign({
itemsBeingRenamed: (context, event) => [
...context.itemsBeingRenamed,
event.data.path,
],
}),
removeFileFromRenamingQueue: assign({
itemsBeingRenamed: (
context,
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
) =>
context.itemsBeingRenamed.filter(
(path) => path !== event.data.oldPath
),
}),
renameToastSuccess: (_, event) => toast.success(event.data.message),
createToastSuccess: (_, event) => toast.success(event.data.message),
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
@ -70,37 +95,56 @@ export const FileMachineProvider = ({
}
},
createFile: async (context, event) => {
let name = event.data.name.trim() || DEFAULT_FILE_NAME
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
await mkdir(await join(context.selectedDirectory.path, name))
createdPath = await join(context.selectedDirectory.path, createdName)
await mkdir(createdPath)
} else {
await create(
createdPath =
context.selectedDirectory.path +
sep() +
name +
(name.endsWith(FILE_EXT) ? '' : FILE_EXT)
)
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
}
return `Successfully created "${name}"`
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
let name = newName ? newName : DEFAULT_FILE_NAME
const name = newName ? newName : DEFAULT_FILE_NAME
const oldPath = await join(context.selectedDirectory.path, oldName)
const newDirPath = await join(context.selectedDirectory.path, name)
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await rename(
await join(context.selectedDirectory.path, oldName),
(await join(context.selectedDirectory.path, name)) +
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT),
{}
)
return (
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
await rename(oldPath, newPath, {})
if (oldPath === file?.path && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(paths.FILE + '/' + encodeURIComponent(newPath))
} else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path
navigate(
paths.FILE +
'/' +
encodeURIComponent(file.path.replace(oldPath, newDirPath))
)
}
return {
message: `Successfully renamed "${oldName}" to "${name}"`,
newPath,
oldPath,
}
},
deleteFile: async (
context: ContextFrom<typeof fileMachine>,
@ -117,6 +161,17 @@ export const FileMachineProvider = ({
console.error('Error deleting file', e)
)
}
// If we just deleted the current file or one of its parent directories,
// navigate to the project root
if (
(event.data.path === file?.path ||
file?.path.includes(event.data.path)) &&
project?.path
) {
navigate(paths.FILE + '/' + encodeURIComponent(project.path))
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`

View File

@ -2,11 +2,11 @@ import type { FileEntry, IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useEffect, useRef, useState } from 'react'
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react'
import { Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
@ -16,6 +16,8 @@ import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -23,11 +25,11 @@ function getIndentationCSS(level: number) {
function RenameForm({
fileOrDir,
setIsRenaming,
onSubmit,
level = 0,
}: {
fileOrDir: FileEntry
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
onSubmit: () => void
level?: number
}) {
const { send } = useFileContext()
@ -35,7 +37,6 @@ function RenameForm({
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsRenaming(false)
send({
type: 'Rename file',
data: {
@ -49,7 +50,7 @@ function RenameForm({
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.stopPropagation()
setIsRenaming(false)
onSubmit()
}
}
@ -61,10 +62,12 @@ function RenameForm({
ref={inputRef}
type="text"
autoFocus
autoCapitalize="off"
autoCorrect="off"
placeholder={fileOrDir.name}
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
onKeyDown={handleKeyDown}
onBlur={() => setIsRenaming(false)}
onBlur={onSubmit}
style={{ paddingInlineStart: getIndentationCSS(level) }}
/>
</label>
@ -75,7 +78,7 @@ function RenameForm({
)
}
function DeleteConfirmationDialog({
function DeleteFileTreeItemDialog({
fileOrDir,
setIsOpen,
}: {
@ -84,48 +87,23 @@ function DeleteConfirmationDialog({
}) {
const { send } = useFileContext()
return (
<Dialog
open={true}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
</Dialog.Title>
<Dialog.Description className="my-6">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined
? ' and all of its contents. '
: '. '}
This action cannot be undone.
</Dialog.Description>
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={async () => {
<DeleteConfirmationDialog
title={`Delete ${fileOrDir.children !== undefined ? 'folder' : 'file'}`}
onDismiss={() => setIsOpen(false)}
onConfirm={() => {
send({ type: 'Delete file', data: fileOrDir })
setIsOpen(false)
}}
iconStart={{
icon: faTrashAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40"
>
Delete
</ActionButton>
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</div>
</Dialog>
<p className="my-4">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined ? ' and all of its contents. ' : '. '}
</p>
<p className="my-4">
Are you sure you want to delete "{fileOrDir.name || 'this file'}
"? This action cannot be undone.
</p>
</DeleteConfirmationDialog>
)
}
@ -133,35 +111,57 @@ const FileTreeItem = ({
project,
currentFile,
fileOrDir,
onDoubleClick,
onNavigateToFile,
level = 0,
}: {
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry
onDoubleClick?: () => void
onNavigateToFile?: () => void
level?: number
}) => {
const { send, context } = useFileContext()
const { send: fileSend, context: fileContext } = useFileContext()
const { onFileOpen, onFileClose } = useLspContext()
const navigate = useNavigate()
const [isRenaming, setIsRenaming] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const isCurrentFile = fileOrDir.path === currentFile?.path
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback(
() =>
fileSend({
type: 'assign',
data: {
itemsBeingRenamed: fileContext.itemsBeingRenamed.filter(
(path) => path !== fileOrDir.path
),
},
}),
[fileContext.itemsBeingRenamed, fileOrDir.path, fileSend]
)
const addCurrentItemToRenaming = useCallback(() => {
fileSend({
type: 'assign',
data: {
itemsBeingRenamed: [...fileContext.itemsBeingRenamed, fileOrDir.path],
},
})
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
setIsConfirmingDelete(true)
} else if (e.key === 'Enter') {
// Show the renaming form
setIsRenaming(true)
addCurrentItemToRenaming()
} else if (e.code === 'Space') {
handleDoubleClick()
handleClick()
}
}
function handleDoubleClick() {
function handleClick() {
if (fileOrDir.children !== undefined) return // Don't open directories
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
@ -181,7 +181,7 @@ const FileTreeItem = ({
// Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
}
onDoubleClick?.()
onNavigateToFile?.()
}
return (
@ -199,8 +199,10 @@ const FileTreeItem = ({
<button
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
style={{ paddingInlineStart: getIndentationCSS(level) }}
onDoubleClick={handleDoubleClick}
onClick={(e) => e.currentTarget.focus()}
onClick={(e) => {
e.currentTarget.focus()
handleClick()
}}
onKeyUp={handleKeyUp}
>
<CustomIcon
@ -212,7 +214,7 @@ const FileTreeItem = ({
) : (
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
onSubmit={removeCurrentItemFromRenaming}
level={level}
/>
)}
@ -225,17 +227,23 @@ const FileTreeItem = ({
<Disclosure.Button
className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
(context.selectedDirectory.path.includes(fileOrDir.path)
(fileContext.selectedDirectory.path.includes(fileOrDir.path)
? ' ui-open:bg-primary/10'
: '')
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
fileSend({
type: 'Set selected directory',
data: fileOrDir,
})
}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
fileSend({
type: 'Set selected directory',
data: fileOrDir,
})
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
@ -263,7 +271,7 @@ const FileTreeItem = ({
/>
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
onSubmit={removeCurrentItemFromRenaming}
level={-1}
/>
</div>
@ -279,10 +287,16 @@ const FileTreeItem = ({
<ul
className="m-0 p-0"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: fileOrDir })
fileSend({
type: 'Set selected directory',
data: fileOrDir,
})
}}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
fileSend({
type: 'Set selected directory',
data: fileOrDir,
})
}
>
{fileOrDir.children?.map((child) => (
@ -290,7 +304,7 @@ const FileTreeItem = ({
fileOrDir={child}
project={project}
currentFile={currentFile}
onDoubleClick={onDoubleClick}
onNavigateToFile={onNavigateToFile}
level={level + 1}
key={level + '-' + child.path}
/>
@ -302,7 +316,7 @@ const FileTreeItem = ({
</Disclosure>
)}
{isConfirmingDelete && (
<DeleteConfirmationDialog
<DeleteFileTreeItemDialog
fileOrDir={fileOrDir}
setIsOpen={setIsConfirmingDelete}
/>
@ -314,7 +328,7 @@ const FileTreeItem = ({
interface FileTreeProps {
className?: string
file?: IndexLoaderData['file']
closePanel: (
onNavigateToFile: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
@ -371,30 +385,34 @@ export const FileTreeMenu = () => {
)
}
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
export const FileTree = ({
className = '',
onNavigateToFile: closePanel,
}: FileTreeProps) => {
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<FileTreeMenu />
</div>
<FileTreeInner onDoubleClick={closePanel} />
<FileTreeInner onNavigateToFile={closePanel} />
</div>
)
}
export const FileTreeInner = ({
onDoubleClick,
onNavigateToFile,
}: {
onDoubleClick?: () => void
onNavigateToFile?: () => void
}) => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { send, context } = useFileContext()
const { send: fileSend, context: fileContext } = useFileContext()
const { send: modelingSend } = useModelingContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
fileSend({ type: 'Refresh' })
}, [documentHasFocus])
return (
@ -402,15 +420,22 @@ export const FileTreeInner = ({
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: context.project })
fileSend({
type: 'Set selected directory',
data: fileContext.project,
})
}}
>
{sortProject(context.project.children || []).map((fileOrDir) => (
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
<FileTreeItem
project={context.project}
project={fileContext.project}
currentFile={loaderData?.file}
fileOrDir={fileOrDir}
onDoubleClick={onDoubleClick}
onNavigateToFile={() => {
// Reset modeling state when navigating to a new file
modelingSend({ type: 'Cancel' })
onNavigateToFile?.()
}}
key={fileOrDir.path}
/>
))}

View File

@ -1,33 +1,26 @@
import { Dialog } from '@headlessui/react'
import { ActionButton } from 'components/ActionButton'
interface DeleteProjectDialogProps {
projectName: string
interface DeleteConfirmationDialogProps extends React.PropsWithChildren<{}> {
title: string
onConfirm: () => void
onDismiss: () => void
}
export function DeleteProjectDialog({
projectName,
export function DeleteConfirmationDialog({
title,
onConfirm,
onDismiss,
}: DeleteProjectDialogProps) {
children,
}: DeleteConfirmationDialogProps) {
return (
<Dialog open={true} onClose={onDismiss} className="relative z-50">
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
Delete File
{title}
</Dialog.Title>
<Dialog.Description>
This will permanently delete "{projectName || 'this file'}
".
</Dialog.Description>
<p className="my-4">
Are you sure you want to delete "{projectName || 'this file'}
"? This action cannot be undone.
</p>
<Dialog.Description>{children}</Dialog.Description>
<div className="flex justify-between">
<ActionButton

View File

@ -5,7 +5,7 @@ import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteProjectDialog } from './DeleteProjectDialog'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'wasm-lib/kcl/bindings/Project'
@ -160,14 +160,23 @@ function ProjectCard({
</div>
)}
{isConfirmingDelete && (
<DeleteProjectDialog
projectName={project.name}
<DeleteConfirmationDialog
title="Delete Project"
onConfirm={async () => {
await handleDeleteProject(project)
setIsConfirmingDelete(false)
}}
onDismiss={() => setIsConfirmingDelete(false)}
/>
>
<p className="my-4">
This will permanently delete "{project.name || 'this file'}
".
</p>
<p className="my-4">
Are you sure you want to delete "{project.name || 'this file'}
"? This action cannot be undone.
</p>
</DeleteConfirmationDialog>
)}
</li>
)

View File

@ -158,7 +158,7 @@ function ProjectMenuPopover({
<FileTree
file={file}
className="overflow-hidden border-0 border-y border-chalkboard-30 dark:border-chalkboard-80"
closePanel={close}
onNavigateToFile={close}
/>
) : (
<div className="flex-1 p-4 text-sm overflow-hidden">

View File

@ -4,7 +4,7 @@ import { Project } from 'wasm-lib/kcl/bindings/Project'
export const fileMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiAKwBGcQDoALACYAHAE4AzGoUA2JXK1yANCACeiabOmSlGpkqsqAvg6NpMuQiXIyAEtSykuLAAzDDgKAGEAJzA8XmwQzGY2JBBuPgEhFLEESTVxWS1xBWVVcTU5PXEjUwRNFRkFWwB2FQVxJia5JnE5JxdQ92IyMB8-BLCAJTBSPBx40KThNNR+QWFslSVZJS1u3TUVSR1xJurEXSYZJslipismbTklPpBXbHwhr19YYNDYCOisXmiVYSx4Kwy6zMeQKRRKKjKFUKZwQPXqkjkmjUTCYWi0TQOCheb0GnhG31+mH+ABEwJg4pSwIsUstVplQNlcvkZIVikpSuVKijdFoZOItJJpEomtcYUTnK8Bh8yaMfuN-gB5DjTRnMzjgtlQnIwnlw-kIwXIkyIdSSGRKHF5XFqSwdYlKjzDVWM-4AZTAvCwsDpYAIcQgWAgqGiYa4kWMetSBshWTMNyaMkOuV0ChUBJUEpRnUuux6WmxGkkTF6CpJyq9URi-FIUEZFAgghGZAAblwANYjAiAuIAWnGidZKY50O5vPhiKF1oQcnF9q0myYCN2FSa4ndbnrXkbsTIrfGFDAkUicZkHHQsSCcZwMiHTbAY4WoJZybWqeNq82ddygUaQHiqJc7BkTRcwUOQjmKHR133d5PS8KYZhwU82w7Lwe37EZogw99xy-fV0l-adalzeQVForY1Ada4ni0FE5CaJRM0UAlmm6LRkNJL10NmLDz0va9Ilve9eEfSJn0I2ZiM-ZIyIhCjREQOoaLospGIxHYUQY0U8VaVoJUdB5+MPEZaXpETQnbTsZDwgcZAgENRxI5Sk3I9l1P-UVci6cUbg0JRlBRcRNkzHRdwUGVaPEOxLNQ6z3LszALyvG87wfJ9XPcxSQS8yc1M5PIMz0aU7gYuQCyOIsegaaUCTCwpnT42sPU+EYpjwKMWx9BzcNIXsXMBCAPypCcf187I7DUGQqweWDGgJNR8SLJ55AY9ibgLPJy2S7qZF6-qzz+IauxG-CZHGya4AYSRipmo15sWnFikUDoNA2pcXVkBQbFo7FuMkJojpVU70rCMTsqkmS5JiCb1WmnzXtyd7lq+tbfpqYoFEzSL9DqNofo6-oDxSigpiCaJYCIVHVNmxAtEaBpyxuZQ8iOaQUTBjNpWJxo2l3TpnheAI3PgFI6xSsE0b-EcWKXJWZBxHF2hAip0ySzrKeOikAh9eWmaNSVriappqy2CpWkkAzqLUJp8Q5h4DgRGsKZQg2xj+E3DT-c2OIlGVdwqFc4rUYUuntB0wesaQmhAvc9e9lVj2bc7MH9qc-OkNjMwFfkygLWqIpxe0cTsWCOiOE4IcE6ZhIG8Yc9KxBFFYlp5EkWirFB6Q1AbrwbIDaG2+ZnIegzTYLWLg59BUYUmAWwHKo3B51HlL2BLQpHoellSA8oooOOsSLGiRdowdY2r7RWu5OhdQLycVfWVS1aZx+-BXKPzmei70VLkvJcKhbBijKHicq3QQLiycEAA */
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiAKwBGcQDoALACYAHAE4AzGoUA2JXK1yANCACeiabOmSlGpkqsqAvg6NpMuQiXIUASmABmAE5wRMxsSCDcfAJC4WIIWgpMMtpqkgrKalJa0kamCJIA7AUySgX6mkwK4gXVckpOLhjY+MRkYDIAEtRYpFxYfk2wFADCQXi82AOYocKRqPyCwnGSmbJa4ulKquJqcnriuYiaKsm2BSpVTAVyTOJyDSCuzR5tnd1TcD5gpHg4k00zcJzBYxUBxFRKWRKLS3XRqFSSHTVQ4IXRJAppRJWSr6erOR5NdytchvWD9QYjMYTcnTVizHjzaJLMyrGTrTbbXb7FF3E6SOSaNRMJhaLQFeEKB5PImedpdMkfIYAETAmGpH0BnAZIOZ+VZ7OUnL26xRui0MnE2WkpQxq0l+OlLVlpJpnwA8hxvq7NRFtUzYizxGsNoaVDtjQcTIh1JISsLMiLUlIrlLCU7XvLXUMAMpgXhYWCqsAECYQLAQVBBEtcALGH3A-1gsxpYoIla6BQqcUqbIo65JGF3LRCjSSJj3B1pl4k0ZgcZkKCuigQQTtMgANy4AGt2gQqWAALQaulAv2LAP5ZQnaoQuR3K5KJg5KMIeFJJSJfQFeM7NSQ1NuOmM5UguS5gAEAQ1jIHDoOMfg1jgMh7nOExHgCJ5alE55NnqloyBCWjqIo0iVJGeR2DImidgociIukOiEQBzzEu0vg-DgoEfMuq4yBu27tEE7GHseYSYYy2GiEcTBqPIVQ1FcEL8toKIJOaaQXPY2TWOIeKNIB06sd8vycU0FDgZBATQbBvDwQEiGCb8wnoaJvpYaCkmvp28gqD5kJ-lc-LQiif7mqKFwXNk8aVExMqvCqaomZg3EknxO4yBARaoSJ9JubqKx4SsNyWmkGgfkoPIQvhOg1AoRQ+TpkgxUB7TxXmiWUOZUEwXBCHpZlTm0i5DYScsmTFHopRPn+cg9oifZ3MkNp-to4iJloTUGTIvh4BWpCLoqyVrqQm5pWMEBoZgsD1me7lxHYMljpUNGJOKahin2dTyH+BR2J2w6WmoG0sVtc67ftFIrilx38TIZ0XXADCSENN26vdMiPekihXBo70vkmyQ2D5Qrik+BRA8621g1mZkQV11m2fZoPw1dGGueJt2IGjGPPdjb0FCi6QKPh4g9gK1G-qKTj4r0GXwOEjoGTl7O6gomgWtcVR3kVH6GC+B5QuUlphtJKhaxOenMc6ma9FmSs6hekiFLGyjfnUdwzQokjBV5ahlGUqSVPCYbmwS+nA5mip242HmOz9bKFEU7t3rVaimjcJSPoU1jSAUnviOTryzvOe2ulHI1mHcraclsOyiyoPLCnGthpDcGLrGTk5hxTRkcSXHxlxzCDKEktSa-eOk0aajslLstyu9J1j2hbsUkq1-B900A95ZX+HV35demtJBMTRCwqdpoBckpT7Vy2J9s4RsSgzyLiQRqTKJ7CcOtYtcqSFetndLavA9N8dqW8HY7whGGP8+99D1xfCoWwFodiijGrcT2eInBAA */
id: 'File machine',
initial: 'Reading files',
@ -12,6 +12,7 @@ export const fileMachine = createMachine(
context: {
project: {} as Project,
selectedDirectory: {} as FileEntry,
itemsBeingRenamed: [] as string[],
},
on: {
@ -65,7 +66,11 @@ export const fileMachine = createMachine(
onDone: [
{
target: 'Reading files',
actions: ['toastSuccess'],
actions: [
'createToastSuccess',
'addFileToRenamingQueue',
'navigateToFile',
],
},
],
onError: [
@ -84,7 +89,7 @@ export const fileMachine = createMachine(
onDone: [
{
target: '#File machine.Reading files',
actions: ['toastSuccess'],
actions: ['renameToastSuccess'],
},
],
onError: [
@ -94,6 +99,8 @@ export const fileMachine = createMachine(
},
],
},
exit: 'removeFileFromRenamingQueue',
},
'Deleting file': {
@ -157,6 +164,21 @@ export const fileMachine = createMachine(
type: 'done.invoke.read-files'
data: Project
}
| {
type: 'done.invoke.rename-file'
data: {
message: string
oldPath: string
newPath: string
}
}
| {
type: 'done.invoke.create-file'
data: {
message: string
path: string
}
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },
},