Skip to content
Merged
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
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,60 @@ and routing.

Browse interactive examples in [Storybook](https://preview.gravity-ui.com/querieskit/).

### QueriesNavigation

`QueriesNavigation` renders cluster and path navigation, an optional detail panel, and
application-provided header actions. Applications remain responsible for loading data, routing,
favorites, and item-specific operations.

```tsx
import {Button, DropdownMenu, Flex} from '@gravity-ui/uikit';
import {QueriesNavigation} from '@gravity-ui/querieskit';

<QueriesNavigation
location={location}
onUpdate={setLocation}
items={items}
search={{value: search, onUpdate: setSearch}}
parentRow={{showDuringSearch: true}}
header={{
actions,
getBreadcrumbHref: ({cluster, path}) =>
cluster ? `/navigation/${cluster}${path ?? ''}` : '/navigation',
renderActions: ({location, actions: headerActions}) => (
<Flex gap={1}>
<DropdownMenu
items={headerActions.map((action) => ({
text: action.title,
disabled: action.disabled,
hidden: action.hidden,
action: () => action.onClick(location),
}))}
/>
<Button href={`/navigation/${location.cluster}${location.path ?? ''}`} target="_blank">
Open
</Button>
</Flex>
),
}}
/>;
```

Without `renderActions`, actions keep the standard button rendering. A custom renderer receives
the unfiltered action array and owns handling of `hidden`, `disabled`, and clicks; returning
`null` intentionally leaves the action area empty. In a detail panel the array contains the
panel actions (or header actions when panel actions are absent), followed by actions supplied by
the resolved detail config.

`getBreadcrumbHref` turns every breadcrumb, including the cluster root and current item, into a
real link. Plain clicks continue through `onUpdate`, while modified and middle clicks keep native
browser behavior. URL construction and router integration belong to the application. Breadcrumb
segments are derived from normalized single-slash paths; the parser does not preserve a Cypress
`//` prefix.

`parentRow.showDuringSearch` defaults to `false`. Set it to `true` to keep the parent navigation
row available while search results are filtered, including when the result list is empty.

## Widgets

| Widget | Description |
Expand Down
34 changes: 27 additions & 7 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import FolderTreeIcon from '@gravity-ui/icons/svgs/folder-tree.svg';
import PencilIcon from '@gravity-ui/icons/svgs/pencil.svg';
import cn from 'bem-cn-lite';
import {parsePathSegments} from './helpers/parsePathSegments';
import {NavigationLocation} from '../../types/navigation';
import type {GetNavigationBreadcrumbHref, NavigationLocation} from '../../types/navigation';
import type {LoadPathSuggestions} from '../../types/pathEditor';
import {PathEditor} from '../PathEditor';
import i18n from './i18n';
Expand All @@ -15,6 +15,7 @@ export type BreadcrumbsProps = {
hideResetButton?: boolean;
className?: string;
onUpdate: (location: NavigationLocation) => void;
getBreadcrumbHref?: GetNavigationBreadcrumbHref;
onLoadSuggestions?: LoadPathSuggestions;
};

Expand All @@ -24,6 +25,7 @@ export const Breadcrumbs: FC<BreadcrumbsProps> = ({
location,
hideResetButton,
onUpdate,
getBreadcrumbHref,
onLoadSuggestions,
className,
}) => {
Expand Down Expand Up @@ -83,16 +85,34 @@ export const Breadcrumbs: FC<BreadcrumbsProps> = ({
<GravityBreadcrumbs showRoot className={block('list')} maxItems={3}>
{items.map((item, index) => {
const isLast = index === items.length - 1;
const itemLocation = {cluster, path: item.path};
const href = getBreadcrumbHref?.(itemLocation);

return (
<GravityBreadcrumbs.Item
key={item.path ?? 'root'}
disabled={isLast}
onClick={
isLast
? undefined
: () => onUpdate({cluster, path: item.path})
}
href={href}
disabled={isLast && !href}
onClick={(event) => {
const isPlainLeftClick =
event.button === 0 &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey;

if (href && !isPlainLeftClick) {
return;
}

if (href) {
event.preventDefault();
}

if (!isLast) {
onUpdate(itemLocation);
}
}}
>
{item.title}
</GravityBreadcrumbs.Item>
Expand Down
8 changes: 8 additions & 0 deletions src/modules/NavigationDetail/NavigationDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {Flex} from '@gravity-ui/uikit';
import {NavigationHeader} from '../NavigationHeader';
import {SearchWithButtons} from '../../components/SearchWithButtons';
import type {
GetNavigationBreadcrumbHref,
NavigationDetailConfig,
NavigationHeaderAction,
NavigationLocation,
RenderNavigationHeaderActions,
} from '../../types/navigation';
import type {LoadPathSuggestions} from '../../types/pathEditor';
import {NavigationDetailTabs} from './internal/NavigationDetailTabs';
Expand All @@ -22,6 +24,8 @@ export type NavigationDetailProps = {
onUpdate: (location: NavigationLocation) => void;
onLoadSuggestions?: LoadPathSuggestions;
actions?: NavigationHeaderAction[];
renderActions?: RenderNavigationHeaderActions;
getBreadcrumbHref?: GetNavigationBreadcrumbHref;
activeTab?: string;
onTabUpdate?: (tab: string) => void;
search?: string;
Expand All @@ -35,6 +39,8 @@ export const NavigationDetail: React.FC<NavigationDetailProps> = ({
onUpdate,
onLoadSuggestions,
actions,
renderActions,
getBreadcrumbHref,
activeTab: activeTabProp,
onTabUpdate,
search: searchProp,
Expand Down Expand Up @@ -93,6 +99,8 @@ export const NavigationDetail: React.FC<NavigationDetailProps> = ({
<NavigationHeader
location={location}
actions={mergedActions}
renderActions={renderActions}
getBreadcrumbHref={getBreadcrumbHref}
onUpdate={onUpdate}
onLoadSuggestions={onLoadSuggestions}
/>
Expand Down
18 changes: 16 additions & 2 deletions src/modules/NavigationHeader/NavigationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import React, {FC} from 'react';
import {Flex} from '@gravity-ui/uikit';
import {Breadcrumbs} from '../../components/Breadcrumbs';
import {NavigationActionButtons} from '../../components/NavigationActionButtons';
import type {NavigationHeaderAction, NavigationLocation} from '../../types/navigation';
import type {
GetNavigationBreadcrumbHref,
NavigationHeaderAction,
NavigationLocation,
RenderNavigationHeaderActions,
} from '../../types/navigation';
import type {LoadPathSuggestions} from '../../types/pathEditor';

export type NavigationHeaderProps = {
location: NavigationLocation;
actions?: NavigationHeaderAction[];
renderActions?: RenderNavigationHeaderActions;
getBreadcrumbHref?: GetNavigationBreadcrumbHref;
onUpdate: (location: NavigationLocation) => void;
onLoadSuggestions?: LoadPathSuggestions;
className?: string;
Expand All @@ -16,6 +23,8 @@ export type NavigationHeaderProps = {
export const NavigationHeader: FC<NavigationHeaderProps> = ({
location,
actions,
renderActions,
getBreadcrumbHref,
onUpdate,
onLoadSuggestions,
className,
Expand All @@ -25,9 +34,14 @@ export const NavigationHeader: FC<NavigationHeaderProps> = ({
<Breadcrumbs
location={location}
onUpdate={onUpdate}
getBreadcrumbHref={getBreadcrumbHref}
onLoadSuggestions={onLoadSuggestions}
/>
<NavigationActionButtons actions={actions} arg={location} />
{renderActions ? (
renderActions({location, actions: actions ?? []})
) : (
<NavigationActionButtons actions={actions} arg={location} />
)}
</Flex>
);
};
5 changes: 4 additions & 1 deletion src/modules/NavigationItemsList/NavigationItemsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import cn from 'bem-cn-lite';
import type {
NavigationItem,
NavigationParentRowConfig,
NavigationSortOrder,
RenderNavigationItem,
} from '../../types/navigation';
Expand All @@ -19,6 +20,7 @@ export type NavigationItemsListProps<T extends NavigationItem = NavigationItem>
items: T[];
path?: string;
search?: string;
parentRow?: NavigationParentRowConfig;
sort?: NavigationSortOrder;
onSortUpdate?: (sort: NavigationSortOrder) => void;
titleLabel: string;
Expand All @@ -36,6 +38,7 @@ export const NavigationItemsList = <T extends NavigationItem = NavigationItem>({
items,
path,
search,
parentRow: parentRowConfig,
sort,
onSortUpdate,
titleLabel,
Expand All @@ -48,7 +51,7 @@ export const NavigationItemsList = <T extends NavigationItem = NavigationItem>({
onItemClick,
className,
}: NavigationItemsListProps<T>) => {
const parentRow = useParentRow(path, search);
const parentRow = useParentRow(path, search, parentRowConfig?.showDuringSearch);

const rows = (parentRow ? [parentRow as T, ...items] : items) as T[];

Expand Down
4 changes: 3 additions & 1 deletion src/modules/NavigationItemsList/internal/useParentRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {getParentPath} from '../../../helpers/getParentPath';
export function useParentRow(
path: string | undefined,
search: string | undefined,
showDuringSearch = false,
): NavigationItem | undefined {
const parentPath = path && !search ? getParentPath(path) : undefined;
const parentPath =
path && path !== '/' && (!search || showDuringSearch) ? getParentPath(path) : undefined;

return useMemo<NavigationItem | undefined>(() => {
if (!parentPath) {
Expand Down
17 changes: 17 additions & 0 deletions src/types/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export type NavigationAction<TArg> = {

export type NavigationHeaderAction = NavigationAction<NavigationLocation>;

export type NavigationHeaderActionsRenderContext = {
location: NavigationLocation;
actions: NavigationHeaderAction[];
};

export type RenderNavigationHeaderActions = (
context: NavigationHeaderActionsRenderContext,
) => ReactNode;

export type GetNavigationBreadcrumbHref = (location: NavigationLocation) => string | undefined;

export type NavigationCluster = {
id: string;
title: string;
Expand Down Expand Up @@ -193,9 +204,15 @@ export type NavigationListStateConfig = {

export type NavigationHeaderConfig = {
actions?: NavigationHeaderAction[];
renderActions?: RenderNavigationHeaderActions;
getBreadcrumbHref?: GetNavigationBreadcrumbHref;
onLoadSuggestions?: LoadPathSuggestions;
};

export type NavigationParentRowConfig = {
showDuringSearch?: boolean;
};

export type NavigationDetailPanelConfig<TItem extends NavigationItem = NavigationItem> = {
openedItem?: TItem;
onItemOpen?: (item: TItem) => void;
Expand Down
Loading
Loading