in packages/online-editor/src/editor/Toolbar/FileSwitcher.tsx [109:418]
export function FileSwitcher(props: {
workspace: ActiveWorkspace;
gitStatusProps?: GitStatusProps;
workspaceFile: WorkspaceFile;
onDeletedWorkspaceFile: () => void;
}) {
const workspaces = useWorkspaces();
const workspaceFileNameRef = useRef<HTMLInputElement>(null);
const [newFileNameValid, setNewFileNameValid] = useState<boolean>(true);
const resetWorkspaceFileName = useCallback(() => {
if (workspaceFileNameRef.current) {
workspaceFileNameRef.current.value = props.workspaceFile.nameWithoutExtension;
setNewFileNameValid(true);
}
}, [props.workspaceFile]);
const checkNewFileName = useCallback(
async (_event: React.FormEvent<HTMLInputElement>, newFileNameWithoutExtension: string) => {
const trimmedNewFileNameWithoutExtension = newFileNameWithoutExtension.trim();
if (trimmedNewFileNameWithoutExtension === props.workspaceFile.nameWithoutExtension) {
setNewFileNameValid(true);
return;
}
const newRelativePath = join(
props.workspaceFile.relativeDirPath,
`${trimmedNewFileNameWithoutExtension}.${props.workspaceFile.extension}`
);
const hasConflictingFileName = await workspaces.existsFile({
workspaceId: props.workspaceFile.workspaceId,
relativePath: newRelativePath,
});
const hasForbiddenCharacters = !/^[\w\d_.'\-()\s]+$/gi.test(newFileNameWithoutExtension);
setNewFileNameValid(!hasConflictingFileName && !hasForbiddenCharacters);
},
[props.workspaceFile, workspaces]
);
const renameWorkspaceFile = useCallback(
async (newFileName: string | undefined) => {
const trimmedNewFileName = newFileName?.trim();
if (!trimmedNewFileName || !newFileNameValid) {
resetWorkspaceFileName();
return;
}
if (trimmedNewFileName === props.workspaceFile.nameWithoutExtension) {
resetWorkspaceFileName();
return;
}
await workspaces.renameFile({
file: props.workspaceFile,
newFileNameWithoutExtension: trimmedNewFileName.trim(),
});
},
[props.workspaceFile, workspaces, resetWorkspaceFileName, newFileNameValid]
);
const handleWorkspaceFileNameKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (newFileNameValid && e.keyCode === 13 /* Enter */) {
e.currentTarget.blur();
setPopoverVisible(false);
} else if (e.keyCode === 27 /* ESC */) {
resetWorkspaceFileName();
e.currentTarget.blur();
setPopoverVisible(false);
}
},
[newFileNameValid, resetWorkspaceFileName]
);
useEffect(resetWorkspaceFileName, [resetWorkspaceFileName]);
const [isFilesDropdownOpen, setFilesDropdownOpen] = useState(false);
const [isPopoverVisible, setPopoverVisible] = useState(false);
const [menuDrilledIn, setMenuDrilledIn] = useState<string[]>([]);
const [drilldownPath, setDrilldownPath] = useState<string[]>([]);
const [menuHeights, setMenuHeights] = useState<{ [key: string]: number }>({});
const [activeMenu, setActiveMenu] = useState(ROOT_MENU_ID);
const [filesMenuMode, setFilesMenuMode] = useState(FilesMenuMode.LIST);
useEffect(() => {
setMenuHeights({});
}, [props.workspace, filesMenuMode, activeMenu]);
useEffect(() => {
if (isFilesDropdownOpen) {
return;
}
setMenuDrilledIn([ROOT_MENU_ID]);
setDrilldownPath([props.workspace.descriptor.workspaceId]);
setActiveMenu(`dd${props.workspace.descriptor.workspaceId}`);
}, [isFilesDropdownOpen, props.workspace.descriptor.workspaceId]);
const drillIn = useCallback((_event, fromMenuId, toMenuId, pathId) => {
setMenuDrilledIn((prev) => [...prev, fromMenuId]);
setDrilldownPath((prev) => [...prev, pathId]);
setActiveMenu(toMenuId);
}, []);
const drillOut = useCallback((_event, toMenuId) => {
setMenuDrilledIn((prev) => prev.slice(0, prev.length - 1));
setDrilldownPath((prev) => prev.slice(0, prev.length - 1));
setActiveMenu(toMenuId);
}, []);
const setHeight = useCallback((menuId: string, height: number) => {
setMenuHeights((prev) => {
if (prev[menuId] === undefined || (menuId !== ROOT_MENU_ID && prev[menuId] !== height)) {
return { ...prev, [menuId]: height };
}
return prev;
});
}, []);
const workspacesMenuItems = useMemo(() => {
if (activeMenu === `dd${props.workspace.descriptor.workspaceId}`) {
return <></>;
}
return (
<WorkspacesMenuItems
activeMenu={activeMenu}
currentWorkspace={props.workspace}
onSelectFile={() => setFilesDropdownOpen(false)}
filesMenuMode={filesMenuMode}
setFilesMenuMode={setFilesMenuMode}
/>
);
}, [activeMenu, filesMenuMode, props.workspace]);
return (
<>
<Flex
alignItems={{ default: "alignItemsCenter" }}
flexWrap={{ default: "nowrap" }}
className={"kie-sandbox--file-switcher"}
>
<FlexItem style={{ display: "flex", alignItems: "baseline", minWidth: 0 }}>
<Dropdown
style={{ position: "relative" }}
position={"left"}
className={"kie-tools--masthead-hoverable"}
isOpen={isFilesDropdownOpen}
isPlain={true}
toggle={
<Toggle
onToggle={(_event, isOpen) =>
setFilesDropdownOpen((prev) => {
if (workspaceFileNameRef.current === document.activeElement) {
return prev;
} else {
return isOpen;
}
})
}
id={"editor-page-masthead-files-dropdown-toggle"}
>
<Flex
flexWrap={{ default: "nowrap" }}
alignItems={{ default: "alignItemsCenter" }}
gap={{ default: "gapMd" }}
>
<FlexItem />
<FlexItem>
<b>
<FileLabel extension={props.workspaceFile.extension} />
</b>
</FlexItem>
<FlexItem style={{ minWidth: 0 }}>
<Popover
hasAutoWidth={true}
distance={15}
showClose={false}
shouldClose={() => setPopoverVisible(false)}
hideOnOutsideClick={true}
enableFlip={false}
withFocusTrap={false}
bodyContent={
<>
<FolderIcon />
{props.workspaceFile.relativeDirPath.split("/").join(" > ")}
</>
}
isVisible={isPopoverVisible}
position={"bottom-start"}
>
<div
data-testid={"toolbar-title"}
className={`kogito--editor__toolbar-name-container ${newFileNameValid ? "" : "invalid"}`}
style={{ width: "100%" }}
>
<Title
aria-label={"EmbeddedEditorFile name"}
headingLevel={"h3"}
size={"2xl"}
style={{ fontWeight: "bold" }}
>
{props.workspaceFile.nameWithoutExtension}
</Title>
<Tooltip
content={
<Text component={TextVariants.p}>
{`A file already exists at this location or this name has invalid characters. Please choose a different name.`}
</Text>
}
position={"bottom"}
trigger={"manual"}
isVisible={!newFileNameValid}
className="kogito--editor__light-tooltip"
>
<TextInput
style={{ fontWeight: "bold" }}
onClick={(e) => {
e.stopPropagation();
//FIXME: Change this when it is possible to move a file.
if (props.workspaceFile.relativePath !== props.workspaceFile.name) {
setPopoverVisible(true);
}
}}
onKeyDown={handleWorkspaceFileNameKeyDown}
onChange={checkNewFileName}
ref={workspaceFileNameRef}
type={"text"}
aria-label={"Edit file name"}
className={"kogito--editor__toolbar-title"}
onBlur={(e) => renameWorkspaceFile(e.target.value)}
/>
</Tooltip>
</div>
</Popover>
</FlexItem>
<FlexItem>
<CaretDownIcon color={"rgb(21, 21, 21)"} />
</FlexItem>
</Flex>
</Toggle>
}
>
<Menu
style={{
boxShadow: "none",
minWidth: `${MIN_FILE_SWITCHER_PANEL_WIDTH_IN_PX}px`,
}}
id={ROOT_MENU_ID}
containsDrilldown={true}
drilldownItemPath={drilldownPath}
drilledInMenus={menuDrilledIn}
activeMenu={activeMenu}
onDrillIn={drillIn}
onDrillOut={drillOut}
onGetMenuHeight={setHeight}
className={"kie-sandbox--files-menu"}
>
<MenuContent
// MAGIC NUMBER ALERT
//
// 204px is the exact number that allows the menu to grow to
// the maximum size of the screen without adding scroll to the page.
maxMenuHeight={MENU_HEIGHT_MAX_LIMIT_CSS}
menuHeight={activeMenu === ROOT_MENU_ID ? undefined : `${menuHeights[activeMenu]}px`}
>
<MenuList style={{ padding: 0 }}>
<MenuItem
itemId={props.workspace.descriptor.workspaceId}
direction={"down"}
drilldownMenu={
<DrilldownMenu id={`dd${props.workspace.descriptor.workspaceId}`}>
<FilesMenuItems
shouldFocusOnSearch={activeMenu.startsWith(`dd${props.workspace.descriptor.workspaceId}`)}
filesMenuMode={filesMenuMode}
setFilesMenuMode={setFilesMenuMode}
workspace={props.workspace}
currentWorkspaceFile={props.workspaceFile}
onSelectFile={() => setFilesDropdownOpen(false)}
currentWorkspaceGitStatusProps={props.gitStatusProps}
onDeletedWorkspaceFile={props.onDeletedWorkspaceFile}
/>
</DrilldownMenu>
}
>
Current
</MenuItem>
<MenuGroup
style={{
maxHeight: `calc(${MENU_HEIGHT_MAX_LIMIT_CSS} - ${DRILLDOWN_NAVIGATION_MENU_ITEM_HEIGHT_IN_PX}px)` /* height of menu minus the height of Current item*/,
overflowY: "auto",
}}
>
{workspacesMenuItems}
</MenuGroup>
</MenuList>
</MenuContent>
</Menu>
</Dropdown>
</FlexItem>
</Flex>
</>
);
}