Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { Box, Button, FormHelperText, Grid, Stack, Tooltip, Typography } from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import { useCallback, useMemo, useState } from 'react';
import { DirectoryItemSelector, DirectoryItemSelectorProps, TreeViewFinderNodeProps } from '@gridsuite/commons-ui';
import { useController } from 'react-hook-form';
import { UUID } from 'node:crypto';
import { FolderOutlined } from '@mui/icons-material';
import { DIRECTORY_ITEM_FULL_PATH, DIRECTORY_ITEM_ID } from '../../field-constants';

Check failure on line 15 in src/components/utils/rhf-inputs/directory-item-input/directory-item-input.tsx

View workflow job for this annotation

GitHub Actions / build / build

Module '"../../field-constants"' has no exported member 'DIRECTORY_ITEM_ID'.

Check failure on line 15 in src/components/utils/rhf-inputs/directory-item-input/directory-item-input.tsx

View workflow job for this annotation

GitHub Actions / build / build

Module '"../../field-constants"' has no exported member 'DIRECTORY_ITEM_FULL_PATH'.
import {
DirectoryItemSchema,
getAbsenceLabelKeyFromType,
getBreadcrumbFromFullPath,
getFullPathFromTreeNode,
} from './directory-item-utils';

export interface DirectoryItemSelectorInputProps extends Omit<DirectoryItemSelectorProps, 'onClose' | 'open'> {
name: string;
}

export function DirectoryItemInput({ name, types, ...props }: Readonly<DirectoryItemSelectorInputProps>) {
const [isOpen, setIsOpen] = useState<boolean>(false);

const {
field: { onChange, value },
fieldState: { error },
} = useController({ name });

const nodeInfos: DirectoryItemSchema | undefined | null = value;
const intl = useIntl();

const breadcrumb = useMemo(() => {
return nodeInfos?.[DIRECTORY_ITEM_FULL_PATH]
? getBreadcrumbFromFullPath(nodeInfos[DIRECTORY_ITEM_FULL_PATH], 48)
: undefined;
}, [nodeInfos]);

const onNodeChanged = useCallback(
(nodes: TreeViewFinderNodeProps[]) => {
if (nodes.length > 0) {
const fullPath = getFullPathFromTreeNode(nodes[0]);
const nodeId: UUID | null = nodes[0]?.id;
if (nodeId) {
const nodeInfos = {
[DIRECTORY_ITEM_ID]: nodeId,
[DIRECTORY_ITEM_FULL_PATH]: fullPath,
};
onChange(nodeInfos);
}
}
setIsOpen(false);
},
[onChange]
);

return (
<Box>
<Stack direction={'row'} alignItems="center" justifyContent="space-between">
<Grid container alignItems="center">
<Grid item paddingTop={1}>
<FolderOutlined />
</Grid>
<Grid item paddingTop={1} paddingLeft={1}>
<Tooltip
title={nodeInfos?.[DIRECTORY_ITEM_FULL_PATH] ?? ''}
componentsProps={{
tooltip: {
sx: {
maxWidth: 'none', // to override the background of text is auto cut
},
},
}}
>
<Typography fontWeight={breadcrumb ? undefined : 'bold'} noWrap>
{breadcrumb || <FormattedMessage id={getAbsenceLabelKeyFromType(types?.[0])} />}
</Typography>
</Tooltip>
</Grid>
<Grid item paddingTop={1} paddingLeft={1}>
{error?.message && (
<FormHelperText error>{intl.formatMessage({ id: error?.message })}</FormHelperText>
)}
</Grid>
</Grid>
<Button onClick={() => setIsOpen(true)} variant="contained" color="primary" component="label">
<FormattedMessage id={breadcrumb ? 'edit' : 'Select'} />
</Button>
</Stack>

<DirectoryItemSelector open={isOpen} onClose={onNodeChanged} types={types} {...props} />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { ElementType, TreeViewFinderNodeProps } from '@gridsuite/commons-ui';
import yup from '../../yup-config';
import { DIRECTORY_ITEM_FULL_PATH, DIRECTORY_ITEM_ID } from '../../field-constants';

Check failure on line 10 in src/components/utils/rhf-inputs/directory-item-input/directory-item-utils.ts

View workflow job for this annotation

GitHub Actions / build / build

Module '"../../field-constants"' has no exported member 'DIRECTORY_ITEM_ID'.

Check failure on line 10 in src/components/utils/rhf-inputs/directory-item-input/directory-item-utils.ts

View workflow job for this annotation

GitHub Actions / build / build

Module '"../../field-constants"' has no exported member 'DIRECTORY_ITEM_FULL_PATH'.

const SEPARATOR = '/';

export function getAbsenceLabelKeyFromType(elementType: string) {
switch (elementType) {
case ElementType.DIRECTORY:
return 'NoFolder';
case ElementType.CASE:
return 'NoCase';
case ElementType.STUDY:
return 'NoStudy';
default:
return 'NoItem';
}
}

/**
* Returns the complete path from root to the input node, separated by "/"
* @param node The TreeViewFinderNodeProps object
* @returns A string representing the full path
*/
export function getFullPathFromTreeNode(node: TreeViewFinderNodeProps): string {
const parentPath = node.parents?.map((parent: TreeViewFinderNodeProps) => parent.name).join(SEPARATOR);
return parentPath ? SEPARATOR + parentPath + SEPARATOR + node.name : SEPARATOR + node.name;
}

/**
* Shortens a full path string based on priority rules to fit within a maximum length.
* Rules:
* 1. Intermediate folders are replaced by "..."
* 2. Root folder name is truncated at the end if so long
* 3. Element name is truncated at the start if so long
* @param fullPath The full path string (e.g., "/root/sub1/sub2/study")
* @param maxLength Maximum allowed characters (default 48)
* @returns Shortened breadcrumb string
*/
export function getBreadcrumbFromFullPath(fullPath: string, maxLength: number = 48): string {
if (fullPath.length <= maxLength) {
return fullPath;
}

const parts = fullPath.split(SEPARATOR).filter(Boolean);

// Safety check: if there's only one part or empty
if (parts.length === 0) return fullPath;
if (parts.length === 1) {
return fullPath.length > maxLength ? `...${fullPath.slice(-(maxLength - 3))}` : fullPath;
}

const rootFolder = parts[0];
const itemName = parts[parts.length - 1];

// Priority 1: Replace intermediate folders with "..."
// Format: root/.../item
let result = `${rootFolder}${SEPARATOR}...${SEPARATOR}${itemName}`;
if (result.length <= maxLength) {
return result;
}

// Priority 2: Truncate root folder name
// Format: /truncatedRoot.../item
// We try to keep as much of the root as possible while keeping the full itemName
const ellipsisSeparator = `...${SEPARATOR}`;
const availableForRoot = maxLength - itemName.length - ellipsisSeparator.length;

if (availableForRoot > 3) {
// 3 for "..." or minimum readable
const truncatedRoot = `${rootFolder.slice(0, availableForRoot - 3)}`;
result = `${truncatedRoot}${ellipsisSeparator}${itemName}`;
} else {
// Priority 3: Root is too small, hide it and truncate item name
// Format: .../truncated_item...
const availableForItem = maxLength - ellipsisSeparator.length;
const truncatedItem = `${itemName.slice(0, availableForItem - ellipsisSeparator.length)}...`;
result = `...${SEPARATOR}${truncatedItem}`;
}

return result;
}

export const directoryItemSchema = yup.object().shape({
[DIRECTORY_ITEM_ID]: yup.string().required(),
[DIRECTORY_ITEM_FULL_PATH]: yup.string().required(),
});

export type DirectoryItemSchema = yup.InferType<typeof directoryItemSchema>;
Loading