Skip to content

Commit

Permalink
Merge pull request #7584 from weseek/support/typescriptize-custom-nav
Browse files Browse the repository at this point in the history
support: Typescriptize CustomNav
  • Loading branch information
yuki-takei committed Apr 19, 2023
2 parents ab90cb4 + 8e27e3a commit 663595b
Show file tree
Hide file tree
Showing 15 changed files with 69 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,10 @@ function NotificationSetting(props) {
user_trigger_notification: {
Icon: () => <i className="icon-settings" />,
i18n: 'User trigger notification',
index: 0,
},
global_notification: {
Icon: () => <i className="icon-settings" />,
i18n: 'Global notification',
index: 1,
},
};
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,30 @@ const SecurityManagementContents = () => {
passport_local: {
Icon: () => <i className="fa fa-users" />,
i18n: 'ID/Pass',
index: 0,
},
passport_ldap: {
Icon: () => <i className="fa fa-sitemap" />,
i18n: 'LDAP',
index: 1,
},
passport_saml: {
Icon: () => <i className="fa fa-key" />,
i18n: 'SAML',
index: 2,
},
passport_oidc: {
Icon: () => <i className="fa fa-key" />,
i18n: 'OIDC',
index: 3,
},
passport_google: {
Icon: () => <i className="fa fa-google" />,
i18n: 'Google',
index: 4,
},
passport_github: {
Icon: () => <i className="fa fa-github" />,
i18n: 'GitHub',
index: 5,
},
// passport_facebook: {
// Icon: () => <i className="fa fa-facebook" />,
// i18n: '(TBD) Facebook',
// index: 7,
// },
};
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import React, {
useEffect, useState, useRef, useMemo, useCallback,
} from 'react';

import PropTypes from 'prop-types';
import { Breakpoint } from '@growi/ui/dist/interfaces/breakpoints';
import {
Nav, NavItem, NavLink,
} from 'reactstrap';

import { ICustomNavTabMappings } from '~/interfaces/ui';

import styles from './CustomNav.module.scss';


function getBreakpointOneLevelLarger(breakpoint) {
function getBreakpointOneLevelLarger(breakpoint: Breakpoint): Omit<Breakpoint, 'xs' | 'sm'> {
switch (breakpoint) {
case 'xs':
return 'sm';
case 'sm':
return 'md';
case 'md':
Expand All @@ -25,12 +29,18 @@ function getBreakpointOneLevelLarger(breakpoint) {
}


export const CustomNavDropdown = (props) => {
type CustomNavDropdownProps = {
navTabMapping: ICustomNavTabMappings,
activeTab: string,
onNavSelected?: (selectedTabKey: string) => void,
};

export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element => {
const {
activeTab, navTabMapping, onNavSelected,
} = props;

const activeObj = navTabMapping[activeTab];
const { Icon, i18n } = navTabMapping[activeTab];

const menuItemClickHandler = useCallback((key) => {
if (onNavSelected != null) {
Expand All @@ -48,16 +58,15 @@ export const CustomNavDropdown = (props) => {
aria-expanded="false"
>
<span className="float-left">
{ activeObj != null && (
<><activeObj.Icon /> {activeObj.i18n}</>
) }
{ Icon != null && <Icon /> } {i18n}
</span>
</button>
<div className="dropdown-menu dropdown-menu-right">
{Object.entries(navTabMapping).map(([key, value]) => {

const isActive = activeTab === key;
const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
const _isLinkEnabled = value.isLinkEnabled ?? true;
const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
const { Icon, i18n } = value;

return (
Expand All @@ -68,7 +77,7 @@ export const CustomNavDropdown = (props) => {
disabled={!isLinkEnabled}
onClick={() => menuItemClickHandler(key)}
>
<Icon /> {i18n}
{ Icon != null && <Icon /> } {i18n}
</button>
);
})}
Expand All @@ -77,23 +86,29 @@ export const CustomNavDropdown = (props) => {
);
};

CustomNavDropdown.propTypes = {
navTabMapping: PropTypes.object.isRequired,
activeTab: PropTypes.string,
onNavSelected: PropTypes.func,
};

type CustomNavTabProps = {
activeTab: string,
navTabMapping: ICustomNavTabMappings,
onNavSelected?: (selectedTabKey: string) => void,
hideBorderBottom?: boolean,
breakpointToHideInactiveTabsDown?: Breakpoint,
navRightElement?: JSX.Element,
};

export const CustomNavTab = (props) => {
const navContainer = useRef();
export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
const [sliderWidth, setSliderWidth] = useState(0);
const [sliderMarginLeft, setSliderMarginLeft] = useState(0);

const {
activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown, navRightElement,
activeTab, navTabMapping, onNavSelected,
hideBorderBottom,
breakpointToHideInactiveTabsDown, navRightElement,
} = props;

const navTabRefs = useMemo(() => {
const navContainerRef = useRef<HTMLDivElement>(null);

const navTabRefs: { [key: string]: HTMLAnchorElement } = useMemo(() => {
const obj = {};
Object.keys(navTabMapping).forEach((key) => {
obj[key] = React.createRef();
Expand All @@ -107,9 +122,9 @@ export const CustomNavTab = (props) => {
}
}, [onNavSelected]);

function registerNavLink(key, elm) {
if (elm != null) {
navTabRefs[key] = elm;
function registerNavLink(key: string, anchorElem: HTMLAnchorElement | null) {
if (anchorElem != null) {
navTabRefs[key] = anchorElem;
}
}

Expand All @@ -123,27 +138,28 @@ export const CustomNavTab = (props) => {
return;
}

if (navContainer == null) {
if (navContainerRef.current == null) {
return;
}

let tempML = 0;
const navContainer = navContainerRef.current;

const styles = Object.entries(navTabRefs).map((el) => {
const width = getPercentage(el[1].offsetWidth, navContainer.current.offsetWidth);
const marginLeft = tempML;
tempML += width;
return { width, marginLeft };
});
const { width, marginLeft } = styles[navTabMapping[activeTab].index];
let marginLeft = 0;
for (const [key, anchorElem] of Object.entries(navTabRefs)) {
const width = getPercentage(anchorElem.offsetWidth, navContainer.offsetWidth);

setSliderWidth(width);
setSliderMarginLeft(marginLeft);
if (key === activeTab) {
setSliderWidth(width);
setSliderMarginLeft(marginLeft);
break;
}

marginLeft += width;
}
}, [activeTab, navTabRefs, navTabMapping]);

// determine inactive classes to hide NavItem
const inactiveClassnames = [];
const inactiveClassnames: string[] = [];
if (breakpointToHideInactiveTabsDown != null) {
const breakpointOneLevelLarger = getBreakpointOneLevelLarger(breakpointToHideInactiveTabsDown);
inactiveClassnames.push('d-none');
Expand All @@ -152,12 +168,13 @@ export const CustomNavTab = (props) => {

return (
<div className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
<div ref={navContainer} className="d-flex justify-content-between">
<div ref={navContainerRef} className="d-flex justify-content-between">
<Nav className="nav-title">
{Object.entries(navTabMapping).map(([key, value]) => {

const isActive = activeTab === key;
const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
const _isLinkEnabled = value.isLinkEnabled ?? true;
const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
const { Icon, i18n } = value;

return (
Expand All @@ -166,7 +183,7 @@ export const CustomNavTab = (props) => {
className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
>
<NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
<Icon /> {i18n}
{ Icon != null && <Icon /> } {i18n}
</NavLink>
</NavItem>
);
Expand All @@ -181,27 +198,23 @@ export const CustomNavTab = (props) => {

};

CustomNavTab.propTypes = {
activeTab: PropTypes.string.isRequired,
navTabMapping: PropTypes.object.isRequired,
onNavSelected: PropTypes.func,
hideBorderBottom: PropTypes.bool,
breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
navRightElement: PropTypes.node,
};

CustomNavTab.defaultProps = {
hideBorderBottom: false,
type CustomNavProps = {
activeTab: string,
navTabMapping: ICustomNavTabMappings,
onNavSelected?: (selectedTabKey: string) => void,
hideBorderBottom?: boolean,
breakpointToHideInactiveTabsDown?: Breakpoint,
breakpointToSwitchDropdownDown?: Breakpoint,
};


const CustomNav = (props) => {
const CustomNav = (props: CustomNavProps): JSX.Element => {

const tabClassnames = ['d-none'];
const dropdownClassnames = ['d-block'];

// determine classes to show/hide
const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown);
const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown ?? 'sm');
tabClassnames.push(`d-${breakpointOneLevelLarger}-block`);
dropdownClassnames.push(`d-${breakpointOneLevelLarger}-none`);

Expand All @@ -218,19 +231,4 @@ const CustomNav = (props) => {

};

CustomNav.propTypes = {
activeTab: PropTypes.string.isRequired,
navTabMapping: PropTypes.object.isRequired,
onNavSelected: PropTypes.func,
hideBorderBottom: PropTypes.bool,
breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
breakpointToSwitchDropdownDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
};

CustomNav.defaultProps = {
hideBorderBottom: false,
breakpointToSwitchDropdownDown: 'sm',
};


export default CustomNav;
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ const CustomTabContent = (props: Props): JSX.Element => {
{Object.entries(navTabMapping).map(([key, value]) => {

const { Content } = value;
const content = Content != null ? <Content /> : <></>;

return (
<TabPane key={key} tabId={key}>
<LazyRenderer shouldRender={key === activeTab}>
<Content />
{content}
</LazyRenderer>
</TabPane>
);
Expand Down
2 changes: 0 additions & 2 deletions apps/app/src/components/DescendantsPageListModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const DescendantsPageListModal = (): JSX.Element => {
return <DescendantsPageList path={status.path} />;
},
i18n: t('page_list'),
index: 0,
isLinkEnabled: () => !isSharedUser,
},
timeline: {
Expand All @@ -66,7 +65,6 @@ export const DescendantsPageListModal = (): JSX.Element => {
return <PageTimeline />;
},
i18n: t('Timeline View'),
index: 1,
isLinkEnabled: () => !isSharedUser,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,11 @@ export const InAppNotificationPage: FC = () => {
Icon: () => <></>,
Content: () => InAppNotificationCategoryByStatus(),
i18n: t('in_app_notification.all'),
index: 0,
},
external_accounts: {
Icon: () => <></>,
Content: () => InAppNotificationCategoryByStatus(InAppNotificationStatuses.STATUS_UNOPENED),
i18n: t('in_app_notification.unopend'),
index: 1,
},
};

Expand Down
7 changes: 0 additions & 7 deletions apps/app/src/components/Me/PersonalSettings.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import React, { useMemo } from 'react';

import PropTypes from 'prop-types';
import { useTranslation } from 'next-i18next';

import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
Expand All @@ -23,37 +22,31 @@ const PersonalSettings = () => {
Icon: () => <i className="icon-fw icon-user"></i>,
Content: UserSettings,
i18n: t('User Information'),
index: 0,
},
external_accounts: {
Icon: () => <i className="icon-fw icon-share-alt"></i>,
Content: ExternalAccountLinkedMe,
i18n: t('admin:user_management.external_accounts'),
index: 1,
},
password_settings: {
Icon: () => <i className="icon-fw icon-lock"></i>,
Content: PasswordSettings,
i18n: t('Password Settings'),
index: 2,
},
api_settings: {
Icon: () => <i className="icon-fw icon-paper-plane"></i>,
Content: ApiSettings,
i18n: t('API Settings'),
index: 3,
},
editor_settings: {
Icon: () => <i className="icon-fw icon-pencil"></i>,
Content: EditorSettings,
i18n: t('editor_settings.editor_settings'),
index: 4,
},
in_app_notification_settings: {
Icon: () => <i className="icon-fw icon-bell"></i>,
Content: InAppNotificationSettings,
i18n: t('in_app_notification_settings.in_app_notification_settings'),
index: 5,
},
};
}, [t]);
Expand Down

0 comments on commit 663595b

Please sign in to comment.