From 723b4de8fec922db0e8c79d51f099c8093985169 Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Thu, 23 Jul 2026 01:09:56 +0530 Subject: [PATCH 1/4] feat: add permissionKey support to Select, TextField, Checkbox, and NavigationNavbar Signed-off-by: Rishi Raj --- src/base/Checkbox/Checkbox.tsx | 39 ++++++++++- src/base/Checkbox/index.tsx | 1 + src/base/Select/Select.tsx | 39 ++++++++++- src/base/Select/index.tsx | 3 +- src/base/TextField/TextField.tsx | 39 ++++++++++- src/base/TextField/index.tsx | 3 +- src/custom/NavigationNavbar/index.tsx | 1 + .../NavigationNavbar/navigationNavbar.tsx | 68 ++++++++++++++----- src/custom/index.tsx | 1 + 9 files changed, 167 insertions(+), 27 deletions(-) diff --git a/src/base/Checkbox/Checkbox.tsx b/src/base/Checkbox/Checkbox.tsx index 302a16dcb..08bd172f5 100644 --- a/src/base/Checkbox/Checkbox.tsx +++ b/src/base/Checkbox/Checkbox.tsx @@ -1,8 +1,43 @@ -import { Checkbox as MuiCheckbox, type CheckboxProps } from '@mui/material'; +import { Checkbox as MuiCheckbox, type CheckboxProps as MuiCheckboxProps } from '@mui/material'; import React from 'react'; +import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; +import { Key, PermissionShield } from '../../custom/permissions'; + +export interface CheckboxProps extends MuiCheckboxProps { + permissionKey?: Key; + /** + * Determines behavior when the user lacks the required permission. + * + * - `'showShield'` (default) — disables the checkbox and shows a shield icon + * with a permission-metadata tooltip. + * - `'hide'` — renders nothing. + * + * Ignored when `permissionKey` is not provided. + */ + permissionAction?: PermissionAction; +} const Checkbox = React.forwardRef((props, ref) => { - return ; + const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; + const hasPermission = useHasPermission(permissionKey); + + // useHasPermission returns true when no permissionKey is provided (backward compatible) + if (hasPermission) { + return ; + } + + // User LACKS permission → apply the permissionAction + if (permissionAction === 'hide') { + return null; + } + + return ( + + + + ); }); +Checkbox.displayName = 'Checkbox'; + export default Checkbox; diff --git a/src/base/Checkbox/index.tsx b/src/base/Checkbox/index.tsx index c81c2c021..d8cf500b4 100644 --- a/src/base/Checkbox/index.tsx +++ b/src/base/Checkbox/index.tsx @@ -1,3 +1,4 @@ import Checkbox from './Checkbox'; export { Checkbox }; +export type { CheckboxProps } from './Checkbox'; diff --git a/src/base/Select/Select.tsx b/src/base/Select/Select.tsx index a21c5f4eb..7e4ea628b 100644 --- a/src/base/Select/Select.tsx +++ b/src/base/Select/Select.tsx @@ -1,8 +1,43 @@ import { Select as MuiSelect, type SelectProps as MuiSelectProps } from '@mui/material'; import React from 'react'; +import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; +import { Key, PermissionShield } from '../../custom/permissions'; -const Select = React.forwardRef((props, ref) => { - return ; +export type SelectProps = MuiSelectProps & { + permissionKey?: Key; + /** + * Determines behavior when the user lacks the required permission. + * + * - `'showShield'` (default) — disables the select and shows a shield icon + * with a permission-metadata tooltip. + * - `'hide'` — renders nothing. + * + * Ignored when `permissionKey` is not provided. + */ + permissionAction?: PermissionAction; +}; + +const Select = React.forwardRef((props, ref) => { + const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; + const hasPermission = useHasPermission(permissionKey); + + // useHasPermission returns true when no permissionKey is provided (backward compatible) + if (hasPermission) { + return ; + } + + // User LACKS permission → apply the permissionAction + if (permissionAction === 'hide') { + return null; + } + + return ( + + + + ); }); +Select.displayName = 'Select'; + export default Select; diff --git a/src/base/Select/index.tsx b/src/base/Select/index.tsx index dffc2763d..51f6f2607 100644 --- a/src/base/Select/index.tsx +++ b/src/base/Select/index.tsx @@ -1,5 +1,4 @@ -import { SelectProps } from '@mui/material'; import Select from './Select'; export { Select }; -export type { SelectProps }; +export type { SelectProps } from './Select'; diff --git a/src/base/TextField/TextField.tsx b/src/base/TextField/TextField.tsx index e8fd8b555..922305938 100644 --- a/src/base/TextField/TextField.tsx +++ b/src/base/TextField/TextField.tsx @@ -1,8 +1,43 @@ import { TextField as MuiTextField, type TextFieldProps as MuiTextFieldProps } from '@mui/material'; import React from 'react'; +import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; +import { Key, PermissionShield } from '../../custom/permissions'; -const TextField = React.forwardRef((props, ref) => { - return ; +export type TextFieldProps = MuiTextFieldProps & { + permissionKey?: Key; + /** + * Determines behavior when the user lacks the required permission. + * + * - `'showShield'` (default) — disables the text field and shows a shield icon + * with a permission-metadata tooltip. + * - `'hide'` — renders nothing. + * + * Ignored when `permissionKey` is not provided. + */ + permissionAction?: PermissionAction; +}; + +const TextField = React.forwardRef((props, ref) => { + const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; + const hasPermission = useHasPermission(permissionKey); + + // useHasPermission returns true when no permissionKey is provided (backward compatible) + if (hasPermission) { + return ; + } + + // User LACKS permission → apply the permissionAction + if (permissionAction === 'hide') { + return null; + } + + return ( + + + + ); }); +TextField.displayName = 'TextField'; + export default TextField; diff --git a/src/base/TextField/index.tsx b/src/base/TextField/index.tsx index 9b68a14a8..0f6e26710 100644 --- a/src/base/TextField/index.tsx +++ b/src/base/TextField/index.tsx @@ -1,5 +1,4 @@ -import { TextFieldProps } from '@mui/material'; import TextField from './TextField'; export { TextField }; -export type { TextFieldProps }; +export type { TextFieldProps } from './TextField'; diff --git a/src/custom/NavigationNavbar/index.tsx b/src/custom/NavigationNavbar/index.tsx index f9b89250a..6dd0d95ca 100644 --- a/src/custom/NavigationNavbar/index.tsx +++ b/src/custom/NavigationNavbar/index.tsx @@ -1,3 +1,4 @@ import NavigationNavbar from './navigationNavbar'; +export type { NavigationItem } from './navigationNavbar'; export { NavigationNavbar }; diff --git a/src/custom/NavigationNavbar/navigationNavbar.tsx b/src/custom/NavigationNavbar/navigationNavbar.tsx index d657dbb0a..2bb024c4a 100644 --- a/src/custom/NavigationNavbar/navigationNavbar.tsx +++ b/src/custom/NavigationNavbar/navigationNavbar.tsx @@ -3,13 +3,34 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { ListItemTextProps, MenuListProps, useMediaQuery, useTheme } from '@mui/material'; import React, { MouseEvent, useState } from 'react'; import { Collapse, Divider, ListItemText, MenuItem } from '../../base'; +import { type PermissionAction } from '../PermissionProvider'; +import { type Key } from '../permissions'; import { IconWrapper, MenuItemList, MenuItemSubList, MenuListStyle, SubIconWrapper } from './style'; -type NavigationItem = { +export type NavigationItem = { id: string; title: string; - icon: React.ReactNode; + icon?: React.ReactNode; + /** + * Legacy boolean permission flag. + * When `permissionKey` is provided, this field is ignored. + * @deprecated Prefer `permissionKey` for automatic PermissionShield support. + */ permission?: boolean; + /** + * Sistent permission key for automatic PermissionShield integration. + * When provided, the underlying `MenuItem` receives this key and handles + * disabled state + shield tooltip automatically. Takes precedence over `permission`. + */ + permissionKey?: Key; + /** + * Determines behavior when the user lacks the required permission. + * Only used when `permissionKey` is provided. + * + * - `'showShield'` (default) — disables the item and shows a shield icon. + * - `'hide'` — renders nothing. + */ + permissionAction?: PermissionAction; onClick: () => void; subItems?: NavigationItem[]; addDivider?: boolean; @@ -41,7 +62,6 @@ const NavigationNavbar: React.FC = ({ {navigationItems.map((item) => { const isOpen = openSectionId === item.id; - const permission = item.permission ?? true; const addDivider = item.addDivider ?? false; const showOnWeb = item.showOnWeb ?? true; @@ -50,10 +70,17 @@ const NavigationNavbar: React.FC = ({ return null; } + // When permissionKey is provided, let MenuItem handle permission gating. + // Otherwise fall back to the legacy boolean `permission` field. + const usePermissionKey = !!item.permissionKey; + const legacyPermission = item.permission ?? true; + return ( @@ -73,19 +100,26 @@ const NavigationNavbar: React.FC = ({ {item.subItems && ( - {item.subItems.map((subItem) => ( - - - {subItem.icon && {subItem.icon}} - - - - ))} + {item.subItems.map((subItem) => { + const useSubPermissionKey = !!subItem.permissionKey; + const subLegacyPermission = subItem.permission ?? true; + + return ( + + + {subItem.icon && {subItem.icon}} + + + + ); + })} )} {addDivider && } diff --git a/src/custom/index.tsx b/src/custom/index.tsx index dd445847f..65e924249 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -70,6 +70,7 @@ export { export { InputSearchField } from './InputSearchField'; export { LearningContent } from './LearningContent'; export { NavigationNavbar } from './NavigationNavbar'; +export type { NavigationItem } from './NavigationNavbar'; export { Note } from './Note'; export { Panel } from './Panel'; export { From e6a8fc945f7c41fc7e924973068e1af5faf3056e Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Fri, 24 Jul 2026 01:43:28 +0530 Subject: [PATCH 2/4] revert: remove permissionKey support from Select, TextField, and Checkbox Signed-off-by: Rishi Raj --- src/base/Checkbox/Checkbox.tsx | 39 ++------------------------------ src/base/Checkbox/index.tsx | 1 - src/base/Select/Select.tsx | 39 ++------------------------------ src/base/Select/index.tsx | 3 ++- src/base/TextField/TextField.tsx | 39 ++------------------------------ src/base/TextField/index.tsx | 3 ++- 6 files changed, 10 insertions(+), 114 deletions(-) diff --git a/src/base/Checkbox/Checkbox.tsx b/src/base/Checkbox/Checkbox.tsx index 08bd172f5..302a16dcb 100644 --- a/src/base/Checkbox/Checkbox.tsx +++ b/src/base/Checkbox/Checkbox.tsx @@ -1,43 +1,8 @@ -import { Checkbox as MuiCheckbox, type CheckboxProps as MuiCheckboxProps } from '@mui/material'; +import { Checkbox as MuiCheckbox, type CheckboxProps } from '@mui/material'; import React from 'react'; -import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; -import { Key, PermissionShield } from '../../custom/permissions'; - -export interface CheckboxProps extends MuiCheckboxProps { - permissionKey?: Key; - /** - * Determines behavior when the user lacks the required permission. - * - * - `'showShield'` (default) — disables the checkbox and shows a shield icon - * with a permission-metadata tooltip. - * - `'hide'` — renders nothing. - * - * Ignored when `permissionKey` is not provided. - */ - permissionAction?: PermissionAction; -} const Checkbox = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; - const hasPermission = useHasPermission(permissionKey); - - // useHasPermission returns true when no permissionKey is provided (backward compatible) - if (hasPermission) { - return ; - } - - // User LACKS permission → apply the permissionAction - if (permissionAction === 'hide') { - return null; - } - - return ( - - - - ); + return ; }); -Checkbox.displayName = 'Checkbox'; - export default Checkbox; diff --git a/src/base/Checkbox/index.tsx b/src/base/Checkbox/index.tsx index d8cf500b4..c81c2c021 100644 --- a/src/base/Checkbox/index.tsx +++ b/src/base/Checkbox/index.tsx @@ -1,4 +1,3 @@ import Checkbox from './Checkbox'; export { Checkbox }; -export type { CheckboxProps } from './Checkbox'; diff --git a/src/base/Select/Select.tsx b/src/base/Select/Select.tsx index 7e4ea628b..a21c5f4eb 100644 --- a/src/base/Select/Select.tsx +++ b/src/base/Select/Select.tsx @@ -1,43 +1,8 @@ import { Select as MuiSelect, type SelectProps as MuiSelectProps } from '@mui/material'; import React from 'react'; -import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; -import { Key, PermissionShield } from '../../custom/permissions'; -export type SelectProps = MuiSelectProps & { - permissionKey?: Key; - /** - * Determines behavior when the user lacks the required permission. - * - * - `'showShield'` (default) — disables the select and shows a shield icon - * with a permission-metadata tooltip. - * - `'hide'` — renders nothing. - * - * Ignored when `permissionKey` is not provided. - */ - permissionAction?: PermissionAction; -}; - -const Select = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; - const hasPermission = useHasPermission(permissionKey); - - // useHasPermission returns true when no permissionKey is provided (backward compatible) - if (hasPermission) { - return ; - } - - // User LACKS permission → apply the permissionAction - if (permissionAction === 'hide') { - return null; - } - - return ( - - - - ); +const Select = React.forwardRef((props, ref) => { + return ; }); -Select.displayName = 'Select'; - export default Select; diff --git a/src/base/Select/index.tsx b/src/base/Select/index.tsx index 51f6f2607..dffc2763d 100644 --- a/src/base/Select/index.tsx +++ b/src/base/Select/index.tsx @@ -1,4 +1,5 @@ +import { SelectProps } from '@mui/material'; import Select from './Select'; export { Select }; -export type { SelectProps } from './Select'; +export type { SelectProps }; diff --git a/src/base/TextField/TextField.tsx b/src/base/TextField/TextField.tsx index 922305938..e8fd8b555 100644 --- a/src/base/TextField/TextField.tsx +++ b/src/base/TextField/TextField.tsx @@ -1,43 +1,8 @@ import { TextField as MuiTextField, type TextFieldProps as MuiTextFieldProps } from '@mui/material'; import React from 'react'; -import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider'; -import { Key, PermissionShield } from '../../custom/permissions'; -export type TextFieldProps = MuiTextFieldProps & { - permissionKey?: Key; - /** - * Determines behavior when the user lacks the required permission. - * - * - `'showShield'` (default) — disables the text field and shows a shield icon - * with a permission-metadata tooltip. - * - `'hide'` — renders nothing. - * - * Ignored when `permissionKey` is not provided. - */ - permissionAction?: PermissionAction; -}; - -const TextField = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'showShield', ...muiProps } = props; - const hasPermission = useHasPermission(permissionKey); - - // useHasPermission returns true when no permissionKey is provided (backward compatible) - if (hasPermission) { - return ; - } - - // User LACKS permission → apply the permissionAction - if (permissionAction === 'hide') { - return null; - } - - return ( - - - - ); +const TextField = React.forwardRef((props, ref) => { + return ; }); -TextField.displayName = 'TextField'; - export default TextField; diff --git a/src/base/TextField/index.tsx b/src/base/TextField/index.tsx index 0f6e26710..9b68a14a8 100644 --- a/src/base/TextField/index.tsx +++ b/src/base/TextField/index.tsx @@ -1,4 +1,5 @@ +import { TextFieldProps } from '@mui/material'; import TextField from './TextField'; export { TextField }; -export type { TextFieldProps } from './TextField'; +export type { TextFieldProps }; From 85e0733b974f0b6154c891188631336c0df9e6a7 Mon Sep 17 00:00:00 2001 From: marblom007 <158522975+marblom007@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:17:51 -0500 Subject: [PATCH 3/4] fix: force NavigationNavbar and NavigationItem into published d.ts NavigationItem (and NavigationNavbar's own declaration) reached the root barrel only through `export * from './custom'`, which rollup-plugin-dts drops from the bundled d.ts - so `import { type NavigationItem } from '@sistent/sistent'` failed type-checking downstream despite the runtime export existing. Add an explicit root re-export following the established FeedbackButton pattern to force both declarations into the published bundle. Signed-off-by: marblom007 <158522975+marblom007@users.noreply.github.com> --- src/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 02ed7a8c4..a1fb32597 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -48,6 +48,14 @@ export { export { DataTableToolbar, type DataTableToolbarProps } from './custom/DataTableToolbar'; +// Same nested-barrel dts-drop quirk as FeedbackButton above: reaching the entry +// only through `export * from './custom'`, rollup-plugin-dts drops both the +// `NavigationNavbar` component declaration and the `NavigationItem` type from the +// bundled d.ts, so `import { NavigationNavbar, type NavigationItem } from +// "@sistent/sistent"` fails type-checking despite the runtime exports existing. +// The explicit re-export forces both declarations into the published bundle. +export { NavigationNavbar, type NavigationItem } from './custom/NavigationNavbar'; + // Same nested-barrel dts-drop quirk as FeedbackButton above. `createCanShow` is // worse than a missing type: consumers still resolve it at runtime, so the import // silently degrades to `any` and its `eventBus` argument stops being From 15e807131fc54b91252b81303209706ba1874a6e Mon Sep 17 00:00:00 2001 From: marblom007 <158522975+marblom007@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:29:46 -0500 Subject: [PATCH 4/4] docs: note that new public exports need an explicit root re-export Document the rollup-plugin-dts nested-barrel declaration-drop quirk that this PR's NavigationItem/NavigationNavbar fix addresses, so future maintainers adding custom exports know to add an explicit re-export in src/index.tsx and verify against the built dist/index.d.ts. Signed-off-by: marblom007 <158522975+marblom007@users.noreply.github.com> --- AGENTS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index aaca5ffa7..7d51e61f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,17 @@ Keep that peer list in step with the enforced set in the test above. Use the Known live instance: [#1735](https://github.com/layer5io/sistent/issues/1735) (`date-fns` in `src/custom/UniversalFilter.tsx`). +## New public exports need an explicit root re-export + +`rollup-plugin-dts` (used by tsup for the declaration bundle) silently drops symbols that reach +the root barrel only through a nested `export * from './custom'` (or `./base`, etc.) - the runtime +export in `dist/index.*js` survives, but the declaration is missing from `dist/index.d.ts`, so +`import { Foo, type FooProps } from '@sistent/sistent'` fails type-checking downstream. When you add +a new public component or type in a `src//` subtree, also add an explicit +`export { Foo, type FooProps } from './/Foo';` to `src/index.tsx` (see the documented block +of examples there, e.g. `FeedbackButton`, `NavigationItem`). Verify by building and grepping +`dist/index.d.ts` for the symbol - a green `jest`/lint run will not catch this. + ## Repo state that looks broken but is pre-existing `prettier --check` fails on ~82 files and `tsc --noEmit` reports errors across `src/` (including