@hydraharness/harness-client-ui-layout 0.1.1-rc.8 → 0.1.1-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/client.js +129 -114
- package/lib/types/client/DesktopFilesPanel.d.ts +8 -0
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -14,9 +14,11 @@ AppFrame always mounts the conversation and details columns; a connected Session
|
|
|
14
14
|
|
|
15
15
|
In Electron, `Ctrl+P` opens the Files tab and focuses its filter for the current registered Workspace; without one, Files stays empty. The Explorer features a resizable splitter (160px–480px) and three collapsible sections: Open Editors with dirty indicators and Save All/Close All actions, Workspace Files with folders first in natural name order, color-coded extension icons, search filter, inline creation, and drag-and-drop moves, and an Outline section extracting code symbols (functions, classes, interfaces, types, headings) with click-to-jump line navigation.
|
|
16
16
|
|
|
17
|
-
Selecting an eligible UTF-8 text file opens the in-panel editor with multi-document tabs and breadcrumbs path navigation. The editor provides a line numbers gutter with active line highlighting, Word Wrap toggling (`Alt+Z`), Go to Line (`Ctrl+G`), Diff with disk view, Prettier formatting (`Shift+Alt+F`), atomic version-checked saves (`Ctrl+S`), and a status bar displaying cursor coordinates, selection count, line count, indentation, encoding, line endings, and language mode. Drafts above 50,000 characters stay plain to keep editing responsive, and opening another file, closing a tab, or switching Workspace guards unsaved changes.
|
|
17
|
+
Selecting an eligible UTF-8 text file opens the in-panel editor with multi-document tabs and breadcrumbs path navigation. The editor provides a line numbers gutter with active line highlighting, Word Wrap toggling (`Alt+Z`), Go to Line (`Ctrl+G`), Diff with disk view, Prettier formatting (`Shift+Alt+F`), atomic version-checked saves (`Ctrl+S`), and a status bar displaying cursor coordinates, selection count, line count, indentation, encoding, line endings, and language mode. Drafts above 50,000 characters stay plain to keep editing responsive, and opening another file, closing a tab, or switching Workspace guards unsaved changes. Save All acknowledges each successful file separately, preserves newer typing and closed tabs, and stops on the first failure with remaining drafts intact.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Context-menu creation uses the addressed folder. Renaming or moving an entry updates descendant editor paths, expanded folders, and the creation target; deleting an entry removes its cached subtree and returns selection to its parent. A relocated file advances the editor's disk version only when it matches the version immediately before relocation; external edits still cause a save conflict. Rename, move, and delete run one at a time and wait for saving, formatting, and creation to finish. Responses from a previous Workspace cannot update the current tree or errors. A successful rename, move, or delete clears the file filter to display the updated tree.
|
|
20
|
+
|
|
21
|
+
The renderer sends only an opaque Workspace id. Electron re-resolves that id through the Host, validates the IPC sender, canonicalizes and confines every list, search, create, rename, move, delete, read, format, and save path to the registered root, excludes symlink traversal, accepts only one validated leaf name for creation, and creates without overwriting an existing entry. Editable content must be valid non-binary UTF-8 and no larger than 1 MB. Confinement and version checks reject stable outside paths and stale states; they are not an operating-system sandbox or an atomic compare-and-swap against an external process that replaces a path during the final filesystem operation. This remains a bounded textarea editor: it does not provide Monaco, language diagnostics, or persisted edit buffers.
|
|
20
22
|
|
|
21
23
|
The `/client` exports are the plugin body (`apply`/`inject`), `LayoutController`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
|
|
22
24
|
|
package/lib/client.js
CHANGED
|
@@ -420,7 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
420
420
|
const savingRef = (0, react.useRef)(false);
|
|
421
421
|
const formattingRef = (0, react.useRef)(false);
|
|
422
422
|
const creatingRef = (0, react.useRef)(false);
|
|
423
|
-
const
|
|
423
|
+
const mutatingRef = (0, react.useRef)(false);
|
|
424
424
|
const mountedRef = (0, react.useRef)(true);
|
|
425
425
|
const rootWorkspaceRef = (0, react.useRef)();
|
|
426
426
|
const documentRef = (0, react.useRef)();
|
|
@@ -475,11 +475,19 @@ window.__ModuleLoader__.load({
|
|
|
475
475
|
const [creating, setCreating] = (0, react.useState)(false);
|
|
476
476
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
477
477
|
const [formatting, setFormatting] = (0, react.useState)(false);
|
|
478
|
-
const [
|
|
478
|
+
const [mutating, setMutating] = (0, react.useState)(false);
|
|
479
479
|
const [draggedPath, setDraggedPath] = (0, react.useState)();
|
|
480
480
|
const [dropTargetPath, setDropTargetPath] = (0, react.useState)();
|
|
481
481
|
const deferredDraft = (0, react.useDeferredValue)(document?.draft);
|
|
482
482
|
const dirty = documentIsDirty(document);
|
|
483
|
+
const publishDocuments = (0, react.useCallback)((nextList) => {
|
|
484
|
+
openDocumentsRef.current = nextList;
|
|
485
|
+
setOpenDocuments(nextList);
|
|
486
|
+
const nextActiveDoc = nextList.find((d) => d.path === activePathRef.current);
|
|
487
|
+
documentRef.current = nextActiveDoc;
|
|
488
|
+
setDocument(nextActiveDoc);
|
|
489
|
+
props.onDirtyChange?.(nextList.some((d) => documentIsDirty(d)));
|
|
490
|
+
}, [props.onDirtyChange]);
|
|
483
491
|
const updateDocument = (0, react.useCallback)((update) => {
|
|
484
492
|
if (!mountedRef.current) return;
|
|
485
493
|
const next = update(openDocumentsRef.current.find((d) => d.path === activePathRef.current));
|
|
@@ -498,13 +506,32 @@ window.__ModuleLoader__.load({
|
|
|
498
506
|
activePathRef.current = next.path;
|
|
499
507
|
setActivePath(next.path);
|
|
500
508
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
509
|
+
publishDocuments(nextList);
|
|
510
|
+
}, [publishDocuments]);
|
|
511
|
+
const remapEntryPaths = (oldPath, newPath, version) => {
|
|
512
|
+
const remap = (path) => samePath(path, oldPath) || pathContains(oldPath, path) ? `${newPath}${path.slice(oldPath.length)}` : path;
|
|
513
|
+
if (activePathRef.current !== void 0) {
|
|
514
|
+
activePathRef.current = remap(activePathRef.current);
|
|
515
|
+
setActivePath(activePathRef.current);
|
|
516
|
+
}
|
|
517
|
+
publishDocuments(openDocumentsRef.current.map((d) => ({
|
|
518
|
+
...d,
|
|
519
|
+
path: remap(d.path),
|
|
520
|
+
version: samePath(d.path, oldPath) && version !== void 0 && d.version === version.before ? version.after : d.version
|
|
521
|
+
})));
|
|
522
|
+
setChildren((current) => Object.fromEntries(Object.entries(current).map(([path, entries]) => [remap(path), entries.map((entry) => ({
|
|
523
|
+
...entry,
|
|
524
|
+
path: remap(entry.path),
|
|
525
|
+
name: fileName(remap(entry.path))
|
|
526
|
+
}))])));
|
|
527
|
+
setExpanded((current) => new Set([...current].map(remap)));
|
|
528
|
+
setSelectedDirectory((current) => current === void 0 ? current : remap(current));
|
|
529
|
+
setCreateDraft((current) => current === void 0 ? current : {
|
|
530
|
+
...current,
|
|
531
|
+
parent: remap(current.parent)
|
|
532
|
+
});
|
|
533
|
+
setQuery("");
|
|
534
|
+
};
|
|
508
535
|
const loadDirectory = (0, react.useCallback)(async (path) => {
|
|
509
536
|
if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || rootWorkspaceRef.current !== workspaceId || !mountedRef.current) return;
|
|
510
537
|
const generation = listGeneration.current;
|
|
@@ -668,7 +695,7 @@ window.__ModuleLoader__.load({
|
|
|
668
695
|
if (opening && children[path] === void 0) loadDirectory(path);
|
|
669
696
|
};
|
|
670
697
|
const openFile = (0, react.useCallback)(async (path) => {
|
|
671
|
-
if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || savingRef.current || !mountedRef.current) return;
|
|
698
|
+
if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || savingRef.current || mutatingRef.current || !mountedRef.current) return;
|
|
672
699
|
const previous = documentRef.current;
|
|
673
700
|
if (previous?.path === path) return;
|
|
674
701
|
if (documentIsDirty(previous) && !window.confirm("Discard unsaved changes and open another file?")) return;
|
|
@@ -748,18 +775,18 @@ window.__ModuleLoader__.load({
|
|
|
748
775
|
document,
|
|
749
776
|
updateDocument
|
|
750
777
|
]);
|
|
751
|
-
const startCreate = (kind) => {
|
|
752
|
-
if (
|
|
778
|
+
const startCreate = (kind, parent = selectedDirectory ?? rootPath) => {
|
|
779
|
+
if (parent === void 0 || creatingRef.current || mutatingRef.current) return;
|
|
753
780
|
setQuery("");
|
|
754
781
|
setError(void 0);
|
|
755
782
|
setCreateName("");
|
|
756
783
|
setCreateDraft({
|
|
757
|
-
parent
|
|
784
|
+
parent,
|
|
758
785
|
kind
|
|
759
786
|
});
|
|
760
787
|
};
|
|
761
788
|
const submitCreate = async () => {
|
|
762
|
-
if (api === void 0 || createDraft === void 0 || workspaceId === void 0 || createName.trim() === "" || creatingRef.current) return;
|
|
789
|
+
if (api === void 0 || createDraft === void 0 || workspaceId === void 0 || createName.trim() === "" || creatingRef.current || mutatingRef.current) return;
|
|
763
790
|
const draft = createDraft;
|
|
764
791
|
const name = createName.trim();
|
|
765
792
|
const generation = listGeneration.current;
|
|
@@ -786,103 +813,89 @@ window.__ModuleLoader__.load({
|
|
|
786
813
|
}
|
|
787
814
|
};
|
|
788
815
|
const startRename = (entry) => {
|
|
816
|
+
if (mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
|
|
789
817
|
setContextMenu(void 0);
|
|
790
818
|
setRenamingPath(entry.path);
|
|
791
819
|
setRenameName(entry.name);
|
|
792
820
|
};
|
|
793
821
|
const submitRename = async (entry) => {
|
|
822
|
+
if (mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
|
|
794
823
|
if (api?.rename === void 0 || workspaceId === void 0 || rootPath === void 0 || renameName.trim() === "" || renameName.trim() === entry.name) {
|
|
795
824
|
setRenamingPath(void 0);
|
|
796
825
|
return;
|
|
797
826
|
}
|
|
798
827
|
const newName = renameName.trim();
|
|
828
|
+
const generation = listGeneration.current;
|
|
829
|
+
mutatingRef.current = true;
|
|
830
|
+
setMutating(true);
|
|
831
|
+
setError(void 0);
|
|
832
|
+
readRequest.current += 1;
|
|
833
|
+
setReading(false);
|
|
799
834
|
try {
|
|
800
835
|
const result = await api.rename(entry.path, newName, workspaceId);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
...d,
|
|
804
|
-
path: result.path
|
|
805
|
-
};
|
|
806
|
-
if (entry.directory && (d.path.startsWith(`${entry.path}/`) || d.path.startsWith(`${entry.path}\\`))) {
|
|
807
|
-
const sub = d.path.slice(entry.path.length);
|
|
808
|
-
return {
|
|
809
|
-
...d,
|
|
810
|
-
path: `${result.path}${sub}`
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
return d;
|
|
814
|
-
});
|
|
815
|
-
openDocumentsRef.current = nextList;
|
|
816
|
-
setOpenDocuments(nextList);
|
|
817
|
-
if (activePathRef.current === entry.path) {
|
|
818
|
-
activePathRef.current = result.path;
|
|
819
|
-
setActivePath(result.path);
|
|
820
|
-
const nextDoc = nextList.find((d) => d.path === result.path);
|
|
821
|
-
documentRef.current = nextDoc;
|
|
822
|
-
setDocument(nextDoc);
|
|
823
|
-
}
|
|
836
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
837
|
+
remapEntryPaths(entry.path, result.path, result.version);
|
|
824
838
|
await loadDirectory(parentDirectory(rootPath, entry.path));
|
|
825
839
|
} catch (reason) {
|
|
826
|
-
setError(String(reason));
|
|
840
|
+
if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
|
|
827
841
|
} finally {
|
|
828
|
-
|
|
842
|
+
mutatingRef.current = false;
|
|
843
|
+
if (mountedRef.current) {
|
|
844
|
+
setMutating(false);
|
|
845
|
+
if (listGeneration.current === generation) setRenamingPath(void 0);
|
|
846
|
+
}
|
|
829
847
|
}
|
|
830
848
|
};
|
|
831
849
|
const handleMove = async (sourcePath, destinationPath) => {
|
|
832
850
|
setDropTargetPath(void 0);
|
|
833
851
|
setDraggedPath(void 0);
|
|
834
|
-
if (api?.move === void 0 || workspaceId === void 0 || rootPath === void 0 ||
|
|
835
|
-
|
|
836
|
-
|
|
852
|
+
if (api?.move === void 0 || workspaceId === void 0 || rootPath === void 0 || mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current || samePath(parentDirectory(rootPath, sourcePath), destinationPath) || samePath(sourcePath, destinationPath) || pathContains(sourcePath, destinationPath)) return;
|
|
853
|
+
const generation = listGeneration.current;
|
|
854
|
+
mutatingRef.current = true;
|
|
855
|
+
setMutating(true);
|
|
837
856
|
setError(void 0);
|
|
857
|
+
readRequest.current += 1;
|
|
858
|
+
setReading(false);
|
|
838
859
|
try {
|
|
839
860
|
const result = await api.move(sourcePath, destinationPath, workspaceId);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const nextList = openDocumentsRef.current.map((d) => {
|
|
843
|
-
if (d.path === sourcePath) return {
|
|
844
|
-
...d,
|
|
845
|
-
path: result.path
|
|
846
|
-
};
|
|
847
|
-
if (pathContains(sourcePath, d.path)) {
|
|
848
|
-
const suffix = d.path.slice(sourcePath.length);
|
|
849
|
-
return {
|
|
850
|
-
...d,
|
|
851
|
-
path: `${result.path}${suffix}`
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
return d;
|
|
855
|
-
});
|
|
856
|
-
openDocumentsRef.current = nextList;
|
|
857
|
-
setOpenDocuments(nextList);
|
|
858
|
-
if (activeNextPath !== activePath) {
|
|
859
|
-
activePathRef.current = activeNextPath;
|
|
860
|
-
setActivePath(activeNextPath);
|
|
861
|
-
const nextDocument = nextList.find((d) => d.path === activeNextPath);
|
|
862
|
-
documentRef.current = nextDocument;
|
|
863
|
-
setDocument(nextDocument);
|
|
864
|
-
}
|
|
861
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
862
|
+
remapEntryPaths(sourcePath, result.path, result.version);
|
|
865
863
|
await Promise.all([loadDirectory(parentDirectory(rootPath, sourcePath)), loadDirectory(destinationPath)]);
|
|
866
864
|
} catch (reason) {
|
|
867
|
-
setError(String(reason));
|
|
865
|
+
if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
|
|
868
866
|
} finally {
|
|
869
|
-
|
|
870
|
-
if (mountedRef.current)
|
|
867
|
+
mutatingRef.current = false;
|
|
868
|
+
if (mountedRef.current) setMutating(false);
|
|
871
869
|
}
|
|
872
870
|
};
|
|
873
871
|
const handleDelete = async (entry) => {
|
|
874
872
|
setContextMenu(void 0);
|
|
875
|
-
if (api?.delete === void 0 || workspaceId === void 0 || rootPath === void 0) return;
|
|
873
|
+
if (api?.delete === void 0 || workspaceId === void 0 || rootPath === void 0 || mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
|
|
876
874
|
if (!window.confirm(`Are you sure you want to delete "${entry.name}"?`)) return;
|
|
875
|
+
const generation = listGeneration.current;
|
|
876
|
+
mutatingRef.current = true;
|
|
877
|
+
setMutating(true);
|
|
878
|
+
setError(void 0);
|
|
879
|
+
readRequest.current += 1;
|
|
880
|
+
setReading(false);
|
|
877
881
|
try {
|
|
878
882
|
await api.delete(entry.path, workspaceId);
|
|
883
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
879
884
|
if (entry.directory) {
|
|
880
885
|
const inside = openDocumentsRef.current.filter((d) => d.path.startsWith(`${entry.path}/`) || d.path.startsWith(`${entry.path}\\`));
|
|
881
886
|
for (const d of inside) closeTab(d.path, true);
|
|
882
887
|
} else closeTab(entry.path, true);
|
|
883
|
-
|
|
888
|
+
const parent = parentDirectory(rootPath, entry.path);
|
|
889
|
+
setSelectedDirectory((current) => current !== void 0 && (samePath(current, entry.path) || pathContains(entry.path, current)) ? parent : current);
|
|
890
|
+
setExpanded((current) => new Set([...current].filter((path) => !samePath(path, entry.path) && !pathContains(entry.path, path))));
|
|
891
|
+
setChildren((current) => Object.fromEntries(Object.entries(current).filter(([path]) => !samePath(path, entry.path) && !pathContains(entry.path, path))));
|
|
892
|
+
setQuery("");
|
|
893
|
+
await loadDirectory(parent);
|
|
884
894
|
} catch (reason) {
|
|
885
|
-
setError(String(reason));
|
|
895
|
+
if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
|
|
896
|
+
} finally {
|
|
897
|
+
mutatingRef.current = false;
|
|
898
|
+
if (mountedRef.current) setMutating(false);
|
|
886
899
|
}
|
|
887
900
|
};
|
|
888
901
|
const handleReveal = async (path) => {
|
|
@@ -982,33 +995,28 @@ window.__ModuleLoader__.load({
|
|
|
982
995
|
props.onDirtyChange?.(false);
|
|
983
996
|
}, [props.onDirtyChange]);
|
|
984
997
|
const saveAllFiles = (0, react.useCallback)(async () => {
|
|
985
|
-
if (api === void 0 || workspaceId === void 0 || savingRef.current) return;
|
|
986
|
-
|
|
998
|
+
if (api === void 0 || workspaceId === void 0 || savingRef.current || formattingRef.current || mutatingRef.current) return;
|
|
999
|
+
const dirtyDocs = openDocumentsRef.current.filter((d) => documentIsDirty(d));
|
|
1000
|
+
if (dirtyDocs.length === 0) return;
|
|
1001
|
+
const generation = listGeneration.current;
|
|
987
1002
|
savingRef.current = true;
|
|
988
1003
|
setSaving(true);
|
|
989
1004
|
props.onSavingChange?.(true);
|
|
990
1005
|
setError(void 0);
|
|
991
1006
|
try {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1007
|
+
for (const doc of dirtyDocs) {
|
|
1008
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
1009
|
+
if (!openDocumentsRef.current.some((current) => current.path === doc.path)) continue;
|
|
1010
|
+
const saved = await api.save(doc.path, doc.draft, doc.version, workspaceId);
|
|
1011
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
1012
|
+
publishDocuments(openDocumentsRef.current.map((current) => current.path === doc.path ? {
|
|
1013
|
+
...current,
|
|
1014
|
+
savedContent: doc.draft,
|
|
1015
|
+
version: saved.version
|
|
1016
|
+
} : current));
|
|
1003
1017
|
}
|
|
1004
|
-
openDocumentsRef.current = updatedList;
|
|
1005
|
-
setOpenDocuments(updatedList);
|
|
1006
|
-
const currentActive = updatedList.find((d) => d.path === activePathRef.current);
|
|
1007
|
-
documentRef.current = currentActive;
|
|
1008
|
-
setDocument(currentActive);
|
|
1009
|
-
props.onDirtyChange?.(false);
|
|
1010
1018
|
} catch (reason) {
|
|
1011
|
-
if (mountedRef.current) setError(String(reason));
|
|
1019
|
+
if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
|
|
1012
1020
|
} finally {
|
|
1013
1021
|
savingRef.current = false;
|
|
1014
1022
|
if (mountedRef.current) {
|
|
@@ -1018,26 +1026,28 @@ window.__ModuleLoader__.load({
|
|
|
1018
1026
|
}
|
|
1019
1027
|
}, [
|
|
1020
1028
|
api,
|
|
1021
|
-
|
|
1029
|
+
publishDocuments,
|
|
1022
1030
|
props.onSavingChange,
|
|
1023
1031
|
workspaceId
|
|
1024
1032
|
]);
|
|
1025
1033
|
const saveFile = (0, react.useCallback)(async () => {
|
|
1026
|
-
if (api === void 0 || document === void 0 || workspaceId === void 0 || !dirty || savingRef.current || formattingRef.current) return;
|
|
1034
|
+
if (api === void 0 || document === void 0 || workspaceId === void 0 || !dirty || savingRef.current || formattingRef.current || mutatingRef.current) return;
|
|
1027
1035
|
const snapshot = document;
|
|
1036
|
+
const generation = listGeneration.current;
|
|
1028
1037
|
savingRef.current = true;
|
|
1029
1038
|
setSaving(true);
|
|
1030
1039
|
props.onSavingChange?.(true);
|
|
1031
1040
|
setError(void 0);
|
|
1032
1041
|
try {
|
|
1033
1042
|
const saved = await api.save(snapshot.path, snapshot.draft, snapshot.version, workspaceId);
|
|
1034
|
-
|
|
1043
|
+
if (!mountedRef.current || listGeneration.current !== generation) return;
|
|
1044
|
+
publishDocuments(openDocumentsRef.current.map((current) => current.path === snapshot.path ? {
|
|
1035
1045
|
...current,
|
|
1036
1046
|
savedContent: snapshot.draft,
|
|
1037
1047
|
version: saved.version
|
|
1038
|
-
} : current);
|
|
1048
|
+
} : current));
|
|
1039
1049
|
} catch (reason) {
|
|
1040
|
-
if (mountedRef.current &&
|
|
1050
|
+
if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
|
|
1041
1051
|
} finally {
|
|
1042
1052
|
savingRef.current = false;
|
|
1043
1053
|
if (mountedRef.current) {
|
|
@@ -1050,11 +1060,11 @@ window.__ModuleLoader__.load({
|
|
|
1050
1060
|
dirty,
|
|
1051
1061
|
document,
|
|
1052
1062
|
props.onSavingChange,
|
|
1053
|
-
|
|
1063
|
+
publishDocuments,
|
|
1054
1064
|
workspaceId
|
|
1055
1065
|
]);
|
|
1056
1066
|
const formatFile = (0, react.useCallback)(async () => {
|
|
1057
|
-
if (api === void 0 || document === void 0 || workspaceId === void 0 || formattingRef.current || savingRef.current) return;
|
|
1067
|
+
if (api === void 0 || document === void 0 || workspaceId === void 0 || formattingRef.current || savingRef.current || mutatingRef.current) return;
|
|
1058
1068
|
const snapshot = document;
|
|
1059
1069
|
formattingRef.current = true;
|
|
1060
1070
|
setFormatting(true);
|
|
@@ -1220,6 +1230,7 @@ window.__ModuleLoader__.load({
|
|
|
1220
1230
|
autoFocus: true,
|
|
1221
1231
|
className: DesktopFilesPanel_module_css_default.renameInput,
|
|
1222
1232
|
"aria-label": `Rename ${entry.name}`,
|
|
1233
|
+
disabled: mutating,
|
|
1223
1234
|
value: renameName,
|
|
1224
1235
|
onChange: (e) => {
|
|
1225
1236
|
setRenameName(e.currentTarget.value);
|
|
@@ -1232,11 +1243,13 @@ window.__ModuleLoader__.load({
|
|
|
1232
1243
|
type: "submit",
|
|
1233
1244
|
"aria-label": "Confirm rename",
|
|
1234
1245
|
className: DesktopFilesPanel_module_css_default.renameBtn,
|
|
1246
|
+
disabled: mutating,
|
|
1235
1247
|
children: (0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconCheckOutline14, {})
|
|
1236
1248
|
}),
|
|
1237
1249
|
(0, react_jsx_runtime.jsx)("button", {
|
|
1238
1250
|
type: "button",
|
|
1239
1251
|
"aria-label": "Cancel rename",
|
|
1252
|
+
disabled: mutating,
|
|
1240
1253
|
className: DesktopFilesPanel_module_css_default.renameBtn,
|
|
1241
1254
|
onClick: () => {
|
|
1242
1255
|
setRenamingPath(void 0);
|
|
@@ -1247,7 +1260,7 @@ window.__ModuleLoader__.load({
|
|
|
1247
1260
|
}) : (0, react_jsx_runtime.jsxs)("button", {
|
|
1248
1261
|
type: "button",
|
|
1249
1262
|
className: DesktopFilesPanel_module_css_default.row,
|
|
1250
|
-
draggable: !
|
|
1263
|
+
draggable: !mutating && !saving && !formatting && !creating,
|
|
1251
1264
|
"data-drop-target": dropTargetPath === entry.path ? "true" : void 0,
|
|
1252
1265
|
style: { paddingLeft: 8 + depth * 14 },
|
|
1253
1266
|
"data-selected": entry.directory ? selectedDirectory === entry.path : document?.path === entry.path || void 0,
|
|
@@ -1265,7 +1278,7 @@ window.__ModuleLoader__.load({
|
|
|
1265
1278
|
});
|
|
1266
1279
|
},
|
|
1267
1280
|
onDragStart: (event) => {
|
|
1268
|
-
if (
|
|
1281
|
+
if (mutating || saving || formatting || creating) return;
|
|
1269
1282
|
setDraggedPath(entry.path);
|
|
1270
1283
|
event.dataTransfer.effectAllowed = "move";
|
|
1271
1284
|
event.dataTransfer.setData("text/plain", entry.path);
|
|
@@ -1275,7 +1288,7 @@ window.__ModuleLoader__.load({
|
|
|
1275
1288
|
setDropTargetPath(void 0);
|
|
1276
1289
|
},
|
|
1277
1290
|
onDragOver: (event) => {
|
|
1278
|
-
if (!entry.directory || draggedPath === void 0 || samePath(draggedPath, entry.path) || pathContains(draggedPath, entry.path) ||
|
|
1291
|
+
if (!entry.directory || draggedPath === void 0 || samePath(draggedPath, entry.path) || pathContains(draggedPath, entry.path) || mutating) return;
|
|
1279
1292
|
event.preventDefault();
|
|
1280
1293
|
event.dataTransfer.dropEffect = "move";
|
|
1281
1294
|
setDropTargetPath(entry.path);
|
|
@@ -1309,7 +1322,7 @@ window.__ModuleLoader__.load({
|
|
|
1309
1322
|
role: error === void 0 ? "status" : "alert",
|
|
1310
1323
|
children: error ?? (workspaceId === void 0 ? "Open a workspace to browse its files." : rootLoading ? "Loading workspace files…" : "Workspace files are unavailable.")
|
|
1311
1324
|
});
|
|
1312
|
-
const status = saving ? "Saving…" : formatting ? "Formatting…" :
|
|
1325
|
+
const status = saving ? "Saving…" : formatting ? "Formatting…" : mutating ? "Updating files…" : creating ? "Creating…" : searching ? "Searching…" : loading || reading ? "Loading…" : void 0;
|
|
1313
1326
|
const highlightedDraft = document !== void 0 && deferredDraft === document.draft && document.draft.length <= HIGHLIGHT_MAX_CHARS ? deferredDraft : void 0;
|
|
1314
1327
|
const lineCount = document ? document.draft.split("\n").length : 1;
|
|
1315
1328
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1356,7 +1369,7 @@ window.__ModuleLoader__.load({
|
|
|
1356
1369
|
className: DesktopFilesPanel_module_css_default.sectionActionBtn,
|
|
1357
1370
|
title: "Save all files",
|
|
1358
1371
|
"aria-label": "Save all files",
|
|
1359
|
-
disabled: saving || !openDocuments.some((d) => documentIsDirty(d)),
|
|
1372
|
+
disabled: saving || formatting || mutating || !openDocuments.some((d) => documentIsDirty(d)),
|
|
1360
1373
|
onClick: () => {
|
|
1361
1374
|
saveAllFiles();
|
|
1362
1375
|
},
|
|
@@ -1464,7 +1477,7 @@ window.__ModuleLoader__.load({
|
|
|
1464
1477
|
className: DesktopFilesPanel_module_css_default.sectionActionBtn,
|
|
1465
1478
|
"aria-label": "New file",
|
|
1466
1479
|
title: "New file",
|
|
1467
|
-
disabled: creating,
|
|
1480
|
+
disabled: creating || mutating,
|
|
1468
1481
|
onClick: () => {
|
|
1469
1482
|
startCreate("file");
|
|
1470
1483
|
},
|
|
@@ -1475,7 +1488,7 @@ window.__ModuleLoader__.load({
|
|
|
1475
1488
|
className: DesktopFilesPanel_module_css_default.sectionActionBtn,
|
|
1476
1489
|
"aria-label": "New folder",
|
|
1477
1490
|
title: "New folder",
|
|
1478
|
-
disabled: creating,
|
|
1491
|
+
disabled: creating || mutating,
|
|
1479
1492
|
onClick: () => {
|
|
1480
1493
|
startCreate("directory");
|
|
1481
1494
|
},
|
|
@@ -1496,7 +1509,7 @@ window.__ModuleLoader__.load({
|
|
|
1496
1509
|
className: DesktopFilesPanel_module_css_default.sectionActionBtn,
|
|
1497
1510
|
"aria-label": "Refresh files",
|
|
1498
1511
|
title: "Refresh files",
|
|
1499
|
-
disabled: creating,
|
|
1512
|
+
disabled: creating || mutating,
|
|
1500
1513
|
onClick: () => {
|
|
1501
1514
|
loadDirectory(rootPath);
|
|
1502
1515
|
},
|
|
@@ -1523,10 +1536,10 @@ window.__ModuleLoader__.load({
|
|
|
1523
1536
|
className: DesktopFilesPanel_module_css_default.tree,
|
|
1524
1537
|
role: "tree",
|
|
1525
1538
|
"aria-label": "Workspace files",
|
|
1526
|
-
"aria-busy": loading || searching || creating ||
|
|
1539
|
+
"aria-busy": loading || searching || creating || mutating || void 0,
|
|
1527
1540
|
"data-drop-target": dropTargetPath === rootPath ? "true" : void 0,
|
|
1528
1541
|
onDragOver: (event) => {
|
|
1529
|
-
if (event.target !== event.currentTarget || draggedPath === void 0 ||
|
|
1542
|
+
if (event.target !== event.currentTarget || draggedPath === void 0 || mutating) return;
|
|
1530
1543
|
event.preventDefault();
|
|
1531
1544
|
event.dataTransfer.dropEffect = "move";
|
|
1532
1545
|
setDropTargetPath(rootPath);
|
|
@@ -1826,7 +1839,7 @@ window.__ModuleLoader__.load({
|
|
|
1826
1839
|
className: DesktopFilesPanel_module_css_default.editorActionBtn,
|
|
1827
1840
|
"aria-label": "Format",
|
|
1828
1841
|
"aria-keyshortcuts": "Shift+Alt+F",
|
|
1829
|
-
disabled: formatting || saving,
|
|
1842
|
+
disabled: formatting || saving || mutating,
|
|
1830
1843
|
title: "Format document (Shift+Alt+F)",
|
|
1831
1844
|
onMouseDown: (event) => {
|
|
1832
1845
|
event.preventDefault();
|
|
@@ -1841,7 +1854,7 @@ window.__ModuleLoader__.load({
|
|
|
1841
1854
|
className: `${DesktopFilesPanel_module_css_default.editorActionBtn} ${dirty ? DesktopFilesPanel_module_css_default.editorActionBtnDirty : ""}`,
|
|
1842
1855
|
"aria-label": "Save",
|
|
1843
1856
|
"aria-keyshortcuts": "Control+S",
|
|
1844
|
-
disabled: !dirty || saving || formatting,
|
|
1857
|
+
disabled: !dirty || saving || formatting || mutating,
|
|
1845
1858
|
title: "Save (Ctrl+S)",
|
|
1846
1859
|
onMouseDown: (event) => {
|
|
1847
1860
|
event.preventDefault();
|
|
@@ -2199,7 +2212,7 @@ window.__ModuleLoader__.load({
|
|
|
2199
2212
|
onClick: () => {
|
|
2200
2213
|
setContextMenu(void 0);
|
|
2201
2214
|
setSelectedDirectory(contextMenu.entry.path);
|
|
2202
|
-
startCreate("file");
|
|
2215
|
+
startCreate("file", contextMenu.entry.path);
|
|
2203
2216
|
},
|
|
2204
2217
|
children: [(0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconPlusOutline16, { size: 13 }), "New File"]
|
|
2205
2218
|
}),
|
|
@@ -2210,7 +2223,7 @@ window.__ModuleLoader__.load({
|
|
|
2210
2223
|
onClick: () => {
|
|
2211
2224
|
setContextMenu(void 0);
|
|
2212
2225
|
setSelectedDirectory(contextMenu.entry.path);
|
|
2213
|
-
startCreate("directory");
|
|
2226
|
+
startCreate("directory", contextMenu.entry.path);
|
|
2214
2227
|
},
|
|
2215
2228
|
children: [(0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconProjectAddOutline16, { size: 13 }), "New Folder"]
|
|
2216
2229
|
}),
|
|
@@ -2220,6 +2233,7 @@ window.__ModuleLoader__.load({
|
|
|
2220
2233
|
type: "button",
|
|
2221
2234
|
className: DesktopFilesPanel_module_css_default.contextMenuItem,
|
|
2222
2235
|
role: "menuitem",
|
|
2236
|
+
disabled: api?.rename === void 0 || mutating || saving || formatting || creating,
|
|
2223
2237
|
onClick: () => {
|
|
2224
2238
|
startRename(contextMenu.entry);
|
|
2225
2239
|
},
|
|
@@ -2229,6 +2243,7 @@ window.__ModuleLoader__.load({
|
|
|
2229
2243
|
type: "button",
|
|
2230
2244
|
className: DesktopFilesPanel_module_css_default.contextMenuItem,
|
|
2231
2245
|
role: "menuitem",
|
|
2246
|
+
disabled: api?.delete === void 0 || mutating || saving || formatting || creating,
|
|
2232
2247
|
onClick: () => {
|
|
2233
2248
|
handleDelete(contextMenu.entry);
|
|
2234
2249
|
},
|
|
@@ -18,12 +18,20 @@ export interface DesktopFilesApi {
|
|
|
18
18
|
path: string;
|
|
19
19
|
name: string;
|
|
20
20
|
directory?: boolean;
|
|
21
|
+
version?: {
|
|
22
|
+
before: string;
|
|
23
|
+
after: string;
|
|
24
|
+
};
|
|
21
25
|
}>;
|
|
22
26
|
move?(path: string, destinationDirectory: string, workspaceId: string): Promise<{
|
|
23
27
|
oldPath: string;
|
|
24
28
|
path: string;
|
|
25
29
|
name: string;
|
|
26
30
|
directory?: boolean;
|
|
31
|
+
version?: {
|
|
32
|
+
before: string;
|
|
33
|
+
after: string;
|
|
34
|
+
};
|
|
27
35
|
}>;
|
|
28
36
|
delete?(path: string, workspaceId: string): Promise<{
|
|
29
37
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydraharness/harness-client-ui-layout",
|
|
3
3
|
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
|
|
4
|
-
"version": "0.1.1-rc.
|
|
4
|
+
"version": "0.1.1-rc.9",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
|
|
11
11
|
"directory": "packages/client/ui-layout"
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
@@ -49,25 +49,25 @@
|
|
|
49
49
|
"@xterm/xterm": "^6.0.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@hydraharness/harness-client-locale": "^0.1.1-rc.
|
|
53
|
-
"@hydraharness/harness-client-ui-settings": "^0.1.1-rc.
|
|
54
|
-
"@hydraharness/harness-client-
|
|
55
|
-
"@hydraharness/harness-
|
|
56
|
-
"@hydraharness/
|
|
57
|
-
"@hydraharness/
|
|
52
|
+
"@hydraharness/harness-client-locale": "^0.1.1-rc.9",
|
|
53
|
+
"@hydraharness/harness-client-ui-settings": "^0.1.1-rc.9",
|
|
54
|
+
"@hydraharness/harness-client-runtime": "^0.1.1-rc.9",
|
|
55
|
+
"@hydraharness/harness-client-ui-theme": "^0.1.1-rc.9",
|
|
56
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.9",
|
|
57
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/react": "~18.3.1",
|
|
61
61
|
"react": "^18.2.0",
|
|
62
|
-
"@hydraharness/harness-client-
|
|
63
|
-
"@hydraharness/harness-client-
|
|
64
|
-
"@hydraharness/harness-client-
|
|
65
|
-
"@hydraharness/harness-client-ui-
|
|
66
|
-
"@hydraharness/harness-client-ui-theme": "^0.1.1-rc.
|
|
67
|
-
"@hydraharness/harness-client-runtime": "^0.1.1-rc.
|
|
68
|
-
"@hydraharness/
|
|
69
|
-
"@hydraharness/harness-client-ui-primitives": "^0.1.1-rc.
|
|
70
|
-
"@hydraharness/
|
|
62
|
+
"@hydraharness/harness-client-test-runtime": "^0.1.1-rc.9",
|
|
63
|
+
"@hydraharness/harness-client-ui-settings": "^0.1.1-rc.9",
|
|
64
|
+
"@hydraharness/harness-client-locale": "^0.1.1-rc.9",
|
|
65
|
+
"@hydraharness/harness-client-ui-slots": "^0.1.1-rc.9",
|
|
66
|
+
"@hydraharness/harness-client-ui-theme": "^0.1.1-rc.9",
|
|
67
|
+
"@hydraharness/harness-client-runtime": "^0.1.1-rc.9",
|
|
68
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.9",
|
|
69
|
+
"@hydraharness/harness-client-ui-primitives": "^0.1.1-rc.9",
|
|
70
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"lib/index.js",
|