Refactor components and views to Storybook compatibility (#17800)
parent
6828007669
commit
6be2861f0c
@ -0,0 +1,15 @@ |
||||
import { useEffect, useState } from 'react'; |
||||
|
||||
export const useAutoToggle = (initialValue = false, ms = 1000) => { |
||||
const [value, setValue] = useState(initialValue); |
||||
|
||||
useEffect(() => { |
||||
const timer = setInterval(() => setValue((value) => !value), ms); |
||||
|
||||
return () => { |
||||
clearInterval(timer); |
||||
}; |
||||
}, []); |
||||
|
||||
return value; |
||||
}; |
@ -1,7 +1,13 @@ |
||||
import { withKnobs } from '@storybook/addon-knobs'; |
||||
import { addDecorator } from '@storybook/react'; |
||||
import { addDecorator, addParameters } from '@storybook/react'; |
||||
|
||||
import { rocketChatDecorator } from './mocks/decorators'; |
||||
import { rocketChatDecorator } from './decorators'; |
||||
|
||||
addDecorator(rocketChatDecorator); |
||||
addDecorator(withKnobs); |
||||
|
||||
addParameters({ |
||||
options: { |
||||
showRoots: true, |
||||
}, |
||||
}); |
||||
|
@ -1,14 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import { GroupSelector } from './GroupSelector'; |
||||
import { SettingsState } from './SettingsState'; |
||||
|
||||
export default { |
||||
title: 'admin/settings/GroupSelector', |
||||
component: GroupSelector, |
||||
decorators: [ |
||||
(storyFn) => <SettingsState>{storyFn()}</SettingsState>, |
||||
], |
||||
}; |
||||
|
||||
export const _default = () => <GroupSelector />; |
||||
|
@ -1,95 +0,0 @@ |
||||
.burger { |
||||
position: relative; |
||||
|
||||
display: none; |
||||
visibility: hidden; |
||||
|
||||
cursor: pointer; |
||||
transition: transform 0.2s ease-out 0.1s; |
||||
will-change: transform; |
||||
|
||||
& .burger__line { |
||||
display: block; |
||||
|
||||
width: 20px; |
||||
height: 2px; |
||||
margin: 5px 0; |
||||
|
||||
transition: transform 0.2s ease-out; |
||||
|
||||
opacity: 0.8; |
||||
|
||||
background-color: var(--rc-color-primary); |
||||
} |
||||
|
||||
/* TODO: unread-burger-alert -> burger__unread-badge */ |
||||
& .unread-burger-alert { |
||||
position: absolute; |
||||
z-index: 3; |
||||
bottom: 13px; |
||||
left: 10px; |
||||
|
||||
min-width: 18px; |
||||
height: 18px; |
||||
padding: 0 4px; |
||||
|
||||
text-align: center; |
||||
|
||||
color: var(--rc-color-content); |
||||
|
||||
border-radius: 20px; |
||||
background-color: var(--rc-color-error-light); |
||||
|
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
line-height: 18px; |
||||
} |
||||
|
||||
/* TODO: .menu-opened -> .burger--open */ |
||||
&.menu-opened .burger__line { |
||||
&:nth-child(1), |
||||
&:nth-child(3) { |
||||
transform-origin: 50%, 50%, 0; |
||||
|
||||
opacity: 1; |
||||
} |
||||
|
||||
&:nth-child(1) { |
||||
transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1); |
||||
} |
||||
|
||||
&:nth-child(3) { |
||||
transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1); |
||||
} |
||||
} |
||||
|
||||
.rtl & { |
||||
right: 0; |
||||
left: auto; |
||||
|
||||
margin-right: 7px; |
||||
margin-left: auto; |
||||
|
||||
& .unread-burger-alert { |
||||
right: auto; |
||||
left: 4px; |
||||
} |
||||
|
||||
&.menu-opened .burger__line { |
||||
&:nth-child(1) { |
||||
transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1); |
||||
} |
||||
|
||||
&:nth-child(3) { |
||||
transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@media (max-width: 780px) { |
||||
.burger { |
||||
display: inline-block; |
||||
visibility: visible; |
||||
} |
||||
} |
@ -1,38 +0,0 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import './BurgerMenuButton.css'; |
||||
import { useSession } from '../../contexts/SessionContext'; |
||||
import { useSidebar } from '../../contexts/SidebarContext'; |
||||
import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; |
||||
|
||||
export const BurgerMenuButton = (props) => { |
||||
const [isSidebarOpen, setSidebarOpen] = useSidebar(); |
||||
const isLayoutEmbedded = useEmbeddedLayout(); |
||||
const unreadMessagesBadge = useSession('unread'); |
||||
|
||||
const handleClick = () => { |
||||
setSidebarOpen(!isSidebarOpen); |
||||
}; |
||||
|
||||
return <Box |
||||
is='button' |
||||
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'} |
||||
className={[ |
||||
'rc-old', |
||||
'burger', |
||||
!!isSidebarOpen && 'menu-opened', |
||||
].filter(Boolean).join(' ')} |
||||
type='button' |
||||
onClick={handleClick} |
||||
{...props} |
||||
> |
||||
<Box is='i' className='burger__line' aria-hidden='true' /> |
||||
<Box is='i' className='burger__line' aria-hidden='true' /> |
||||
<Box is='i' className='burger__line' aria-hidden='true' /> |
||||
{!isLayoutEmbedded && unreadMessagesBadge |
||||
&& <Box className='unread-burger-alert color-error-contrast background-error-color'> |
||||
{unreadMessagesBadge} |
||||
</Box>} |
||||
</Box>; |
||||
}; |
@ -1,46 +0,0 @@ |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { boolean, text } from '@storybook/addon-knobs/react'; |
||||
import React from 'react'; |
||||
|
||||
import { BurgerMenuButton } from './BurgerMenuButton'; |
||||
import { SidebarContext } from '../../contexts/SidebarContext'; |
||||
import { SessionContext } from '../../contexts/SessionContext'; |
||||
|
||||
export default { |
||||
title: 'basic/BurgerMenuButton', |
||||
component: BurgerMenuButton, |
||||
decorators: [(fn) => <div style={{ margin: '1rem' }}>{fn()}</div>], |
||||
parameters: { |
||||
viewport: { defaultViewport: 'mobile1' }, |
||||
}, |
||||
}; |
||||
|
||||
export const _default = () => <BurgerMenuButton |
||||
isSidebarOpen={boolean('isSidebarOpen')} |
||||
isLayoutEmbedded={boolean('isLayoutEmbedded')} |
||||
unreadMessagesBadge={text('unreadMessagesBadge')} |
||||
onClick={action('click')} |
||||
/>; |
||||
_default.story = { |
||||
decorators: [ |
||||
(fn) => <SidebarContext.Provider children={fn()} value={[false, action('setSidebarOpen')]} />, |
||||
], |
||||
}; |
||||
|
||||
export const whenSidebarOpen = () => <BurgerMenuButton />; |
||||
whenSidebarOpen.story = { |
||||
decorators: [ |
||||
(fn) => <SidebarContext.Provider children={fn()} value={[true, action('setSidebarOpen')]} />, |
||||
], |
||||
}; |
||||
|
||||
export const unreadMessagesBadge = () => <BurgerMenuButton />; |
||||
unreadMessagesBadge.story = { |
||||
decorators: [ |
||||
(fn) => <SessionContext.Provider value={{ |
||||
get: (name) => name === 'unread' && '99', |
||||
}}> |
||||
<SidebarContext.Provider children={fn()} value={[false, action('setSidebarOpen')]} /> |
||||
</SessionContext.Provider>, |
||||
], |
||||
}; |
@ -1,9 +1,19 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { useMemo } from 'react'; |
||||
import marked from 'marked'; |
||||
|
||||
import { Markdown } from '../../../app/markdown/client'; |
||||
import RawText from './RawText'; |
||||
marked.InlineLexer.rules.gfm.strong = /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/; |
||||
marked.InlineLexer.rules.gfm.em = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/; |
||||
|
||||
const MarkdownText = ({ children }) => |
||||
<RawText>{Markdown.parse(children)}</RawText>; |
||||
function MarkdownText({ content, ...props }) { |
||||
const options = useMemo(() => ({ |
||||
gfm: true, |
||||
headerIds: false, |
||||
}), []); |
||||
|
||||
const __html = useMemo(() => marked(content, options), [content, options]); |
||||
|
||||
return <Box dangerouslySetInnerHTML={{ __html }} withRichContent {...props} />; |
||||
} |
||||
|
||||
export default MarkdownText; |
||||
|
@ -0,0 +1,39 @@ |
||||
import React from 'react'; |
||||
|
||||
import MarkdownText from './MarkdownText'; |
||||
|
||||
export default { |
||||
title: 'components/basic/MarkdownText', |
||||
component: MarkdownText, |
||||
}; |
||||
|
||||
export const Example = () => |
||||
<MarkdownText |
||||
content={` |
||||
# h1 Heading |
||||
## h2 Heading |
||||
### h3 Heading |
||||
#### h4 Heading |
||||
##### h5 Heading |
||||
###### h6 Heading |
||||
|
||||
___ |
||||
|
||||
*This is bold text* |
||||
|
||||
_This is italic text_ |
||||
|
||||
~Strikethrough~ |
||||
|
||||
+ Lorem ipsum dolor sit amet |
||||
+ Consectetur adipiscing elit |
||||
+ Integer molestie lorem at massa |
||||
|
||||
1. Lorem ipsum dolor sit amet |
||||
2. Consectetur adipiscing elit |
||||
3. Integer molestie lorem at massa |
||||
|
||||
\`rocket.chat();\` |
||||
|
||||
https://rocket.chat`}
|
||||
/>; |
@ -0,0 +1,15 @@ |
||||
import { Badge } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
function BurgerBadge({ children }) { |
||||
return <Badge |
||||
position='absolute' |
||||
insetInlineEnd='neg-x8' |
||||
insetBlockStart='neg-x4' |
||||
zIndex='3' |
||||
variant='danger' |
||||
children={children} |
||||
/>; |
||||
} |
||||
|
||||
export default BurgerBadge; |
@ -0,0 +1,21 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import BurgerBadge from './BurgerBadge'; |
||||
import { centeredDecorator } from '../../../../.storybook/decorators'; |
||||
|
||||
export default { |
||||
title: 'components/basic/burger/BurgerBadge', |
||||
component: BurgerBadge, |
||||
decorators: [ |
||||
(storyFn) => <Box size='x24' borderWidth='x1' borderStyle='dashed' position='relative'> |
||||
{storyFn()} |
||||
</Box>, |
||||
centeredDecorator, |
||||
], |
||||
}; |
||||
|
||||
export const Basic = () => |
||||
<BurgerBadge> |
||||
99 |
||||
</BurgerBadge>; |
@ -0,0 +1,69 @@ |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useIsReducedMotionPreferred } from '../../../hooks/useIsReducedMotionPreferred'; |
||||
|
||||
const Wrapper = ({ children }) => |
||||
<Box |
||||
is='span' |
||||
display='inline-flex' |
||||
flexDirection='column' |
||||
alignItems='center' |
||||
justifyContent='space-between' |
||||
size='x24' |
||||
paddingBlock='x4' |
||||
paddingInline='x2' |
||||
verticalAlign='middle' |
||||
children={children} |
||||
/>; |
||||
|
||||
const Line = ({ animated, moved }) => |
||||
<Box |
||||
is='span' |
||||
width='x20' |
||||
height='x2' |
||||
backgroundColor='currentColor' |
||||
className={[ |
||||
animated && css` |
||||
will-change: transform; |
||||
transition: transform 0.2s ease-out; |
||||
`,
|
||||
moved && css` |
||||
&:nth-child(1), |
||||
&:nth-child(3) { |
||||
transform-origin: 50%, 50%, 0; |
||||
} |
||||
|
||||
&:nth-child(1) { |
||||
transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1); |
||||
} |
||||
|
||||
[dir=rtl] &:nth-child(1) { |
||||
transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1); |
||||
} |
||||
|
||||
&:nth-child(3) { |
||||
transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1); |
||||
} |
||||
|
||||
[dir=rtl] &:nth-child(3) { |
||||
transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1); |
||||
} |
||||
`,
|
||||
]} |
||||
aria-hidden='true' |
||||
/>; |
||||
|
||||
function BurgerIcon({ children, open }) { |
||||
const isReducedMotionPreferred = useIsReducedMotionPreferred(); |
||||
|
||||
return <Wrapper> |
||||
<Line animated={!isReducedMotionPreferred} moved={open} /> |
||||
<Line animated={!isReducedMotionPreferred} moved={open} /> |
||||
<Line animated={!isReducedMotionPreferred} moved={open} /> |
||||
{children} |
||||
</Wrapper>; |
||||
} |
||||
|
||||
export default BurgerIcon; |
@ -0,0 +1,22 @@ |
||||
import React from 'react'; |
||||
|
||||
import { centeredDecorator } from '../../../../.storybook/decorators'; |
||||
import { useAutoToggle } from '../../../../.storybook/hooks'; |
||||
import BurgerIcon from './BurgerIcon'; |
||||
|
||||
export default { |
||||
title: 'components/basic/burger/BurgerIcon', |
||||
component: BurgerIcon, |
||||
decorators: [ |
||||
centeredDecorator, |
||||
], |
||||
}; |
||||
|
||||
export const Normal = () => |
||||
<BurgerIcon />; |
||||
|
||||
export const Open = () => |
||||
<BurgerIcon open />; |
||||
|
||||
export const Transitioning = () => |
||||
<BurgerIcon open={useAutoToggle()} />; |
@ -0,0 +1,27 @@ |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; |
||||
import BurgerIcon from './BurgerIcon'; |
||||
import BurgerBadge from './BurgerBadge'; |
||||
|
||||
function BurgerMenuButton({ open, badge, ...props }) { |
||||
const isLayoutEmbedded = useEmbeddedLayout(); |
||||
const t = useTranslation(); |
||||
|
||||
return <Box |
||||
is='button' |
||||
aria-label={open ? t('Close menu') : t('Open menu')} |
||||
type='button' |
||||
position='relative' |
||||
className={css`cursor: pointer;`} |
||||
{...props} |
||||
> |
||||
<BurgerIcon open={open} /> |
||||
{!isLayoutEmbedded && badge && <BurgerBadge>{badge}</BurgerBadge>} |
||||
</Box>; |
||||
} |
||||
|
||||
export default BurgerMenuButton; |
@ -0,0 +1,22 @@ |
||||
import { action } from '@storybook/addon-actions'; |
||||
import React from 'react'; |
||||
|
||||
import { centeredDecorator } from '../../../../.storybook/decorators'; |
||||
import BurgerMenuButton from './BurgerMenuButton'; |
||||
|
||||
export default { |
||||
title: 'components/basic/burger/BurgerMenuButton', |
||||
component: BurgerMenuButton, |
||||
decorators: [ |
||||
centeredDecorator, |
||||
], |
||||
}; |
||||
|
||||
export const Basic = () => |
||||
<BurgerMenuButton onClick={action('click')} />; |
||||
|
||||
export const Open = () => |
||||
<BurgerMenuButton open onClick={action('click')} />; |
||||
|
||||
export const WithBadge = () => |
||||
<BurgerMenuButton badge='99' onClick={action('click')} />; |
@ -1,10 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import PageNotFound from './PageNotFound'; |
||||
|
||||
export default { |
||||
title: 'pageNotFound/PageNotFound', |
||||
component: PageNotFound, |
||||
}; |
||||
|
||||
export const _default = () => <PageNotFound />; |
@ -0,0 +1,7 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
type ModalContextValue = unknown; |
||||
|
||||
export const ModalContext = createContext<ModalContextValue>({}); |
||||
|
||||
export const useModal = (): ModalContextValue => useContext(ModalContext); |
@ -0,0 +1,249 @@ |
||||
import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { createContext, useContext, RefObject, useState, useEffect, useLayoutEffect } from 'react'; |
||||
|
||||
import { useReactiveValue } from '../hooks/useReactiveValue'; |
||||
import { useBatchSettingsDispatch } from './SettingsContext'; |
||||
import { useToastMessageDispatch } from './ToastMessagesContext'; |
||||
import { useTranslation, useLoadLanguage } from './TranslationContext'; |
||||
import { useUser } from './UserContext'; |
||||
|
||||
type Setting = object & { |
||||
_id: unknown; |
||||
type: string; |
||||
blocked: boolean; |
||||
enableQuery: unknown; |
||||
group: string; |
||||
section: string; |
||||
changed: boolean; |
||||
value: unknown; |
||||
packageValue: unknown; |
||||
packageEditor: unknown; |
||||
editor: unknown; |
||||
disabled?: boolean; |
||||
update?: () => void; |
||||
reset?: () => void; |
||||
}; |
||||
|
||||
type PrivateSettingsState = { |
||||
settings: Setting[]; |
||||
persistedSettings: Setting[]; |
||||
}; |
||||
|
||||
type EqualityFunction<T> = (a: T, b: T) => boolean; |
||||
|
||||
type PrivateSettingsContextValue = { |
||||
subscribers: Set<(state: PrivateSettingsState) => void>; |
||||
stateRef: RefObject<PrivateSettingsState>; |
||||
hydrate: (changes: any[]) => void; |
||||
isDisabled: (setting: Setting) => boolean; |
||||
}; |
||||
|
||||
export const PrivateSettingsContext = createContext<PrivateSettingsContextValue>({ |
||||
subscribers: new Set<(state: PrivateSettingsState) => void>(), |
||||
stateRef: { |
||||
current: { |
||||
settings: [], |
||||
persistedSettings: [], |
||||
}, |
||||
}, |
||||
hydrate: () => undefined, |
||||
isDisabled: () => false, |
||||
}); |
||||
|
||||
const useSelector = <T>( |
||||
selector: (state: PrivateSettingsState) => T, |
||||
equalityFunction: EqualityFunction<T> = Object.is, |
||||
): T | null => { |
||||
const { subscribers, stateRef } = useContext(PrivateSettingsContext); |
||||
const [value, setValue] = useState<T | null>(() => (stateRef.current ? selector(stateRef.current) : null)); |
||||
|
||||
const handleUpdate = useMutableCallback((state: PrivateSettingsState) => { |
||||
const newValue = selector(state); |
||||
|
||||
if (!value || !equalityFunction(newValue, value)) { |
||||
setValue(newValue); |
||||
} |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
subscribers.add(handleUpdate); |
||||
|
||||
return (): void => { |
||||
subscribers.delete(handleUpdate); |
||||
}; |
||||
}, [handleUpdate]); |
||||
|
||||
useLayoutEffect(() => { |
||||
handleUpdate(stateRef.current); |
||||
}); |
||||
|
||||
return value; |
||||
}; |
||||
|
||||
export const usePrivateSettingsGroup = (groupId: string): any => { |
||||
const group = useSelector((state) => state.settings.find(({ _id, type }) => _id === groupId && type === 'group')); |
||||
|
||||
const filterSettings = (settings: any[]): any[] => settings.filter(({ group }) => group === groupId); |
||||
|
||||
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed)); |
||||
const sections = useSelector((state) => Array.from(new Set(filterSettings(state.settings).map(({ section }) => section || ''))), (a, b) => a.length === b.length && a.join() === b.join()); |
||||
|
||||
const batchSetSettings = useBatchSettingsDispatch(); |
||||
const { stateRef, hydrate } = useContext(PrivateSettingsContext); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch() as any; |
||||
const t = useTranslation() as (key: string, ...args: any[]) => string; |
||||
const loadLanguage = useLoadLanguage() as any; |
||||
const user = useUser() as any; |
||||
|
||||
const save = useMutableCallback(async () => { |
||||
const state = stateRef.current; |
||||
const settings = filterSettings(state?.settings ?? []); |
||||
|
||||
const changes = settings.filter(({ changed }) => changed) |
||||
.map(({ _id, value, editor }) => ({ _id, value, editor })); |
||||
|
||||
if (changes.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
await batchSetSettings(changes); |
||||
|
||||
if (changes.some(({ _id }) => _id === 'Language')) { |
||||
const lng = user?.language |
||||
|| changes.filter(({ _id }) => _id === 'Language').shift()?.value |
||||
|| 'en'; |
||||
|
||||
try { |
||||
await loadLanguage(lng); |
||||
dispatchToastMessage({ type: 'success', message: t('Settings_updated', { lng }) }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Settings_updated') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const cancel = useMutableCallback(() => { |
||||
const state = stateRef.current; |
||||
const settings = filterSettings(state?.settings ?? []); |
||||
const persistedSettings = filterSettings(state?.persistedSettings ?? []); |
||||
|
||||
const changes = settings.filter(({ changed }) => changed) |
||||
.map((field) => { |
||||
const { _id, value, editor } = persistedSettings.find(({ _id }) => _id === field._id); |
||||
return { _id, value, editor, changed: false }; |
||||
}); |
||||
|
||||
hydrate(changes); |
||||
}); |
||||
|
||||
return group && { ...group, sections, changed, save, cancel }; |
||||
}; |
||||
|
||||
export const usePrivateSettingsSection = (groupId: string, sectionName?: string): any => { |
||||
sectionName = sectionName || ''; |
||||
|
||||
const filterSettings = (settings: any[]): any[] => |
||||
settings.filter(({ group, section }) => group === groupId && ((!sectionName && !section) || (sectionName === section))); |
||||
|
||||
const canReset = useSelector((state) => filterSettings(state.settings).some(({ value, packageValue }) => JSON.stringify(value) !== JSON.stringify(packageValue))); |
||||
const settingsIds = useSelector((state) => filterSettings(state.settings).map(({ _id }) => _id), (a, b) => a.length === b.length && a.join() === b.join()); |
||||
|
||||
const { stateRef, hydrate, isDisabled } = useContext(PrivateSettingsContext); |
||||
|
||||
const reset = useMutableCallback(() => { |
||||
const state = stateRef.current; |
||||
const settings = filterSettings(state?.settings ?? []) |
||||
.filter((setting) => Tracker.nonreactive(() => !isDisabled(setting))); // Ignore disabled settings
|
||||
const persistedSettings = filterSettings(state?.persistedSettings ?? []); |
||||
|
||||
const changes = settings.map((setting) => { |
||||
const { _id, value, packageValue, packageEditor } = persistedSettings.find(({ _id }) => _id === setting._id); |
||||
return { |
||||
_id, |
||||
value: packageValue, |
||||
editor: packageEditor, |
||||
changed: JSON.stringify(packageValue) !== JSON.stringify(value), |
||||
}; |
||||
}); |
||||
|
||||
hydrate(changes); |
||||
}); |
||||
|
||||
return { |
||||
name: sectionName, |
||||
canReset, |
||||
settings: settingsIds, |
||||
reset, |
||||
}; |
||||
}; |
||||
|
||||
export const usePrivateSettingActions = (persistedSetting: Setting | null | undefined): { |
||||
update: () => void; |
||||
reset: () => void; |
||||
} => { |
||||
const { hydrate } = useContext(PrivateSettingsContext); |
||||
|
||||
const update = useDebouncedCallback(({ value, editor }) => { |
||||
const changes = [{ |
||||
_id: persistedSetting?._id, |
||||
...value !== undefined && { value }, |
||||
...editor !== undefined && { editor }, |
||||
changed: JSON.stringify(persistedSetting?.value) !== JSON.stringify(value) || JSON.stringify(editor) !== JSON.stringify(persistedSetting?.editor), |
||||
}]; |
||||
|
||||
hydrate(changes); |
||||
}, 100, [hydrate, persistedSetting]) as () => void; |
||||
|
||||
const reset = useDebouncedCallback(() => { |
||||
const changes = [{ |
||||
_id: persistedSetting?._id, |
||||
value: persistedSetting?.packageValue, |
||||
editor: persistedSetting?.packageEditor, |
||||
changed: JSON.stringify(persistedSetting?.packageValue) !== JSON.stringify(persistedSetting?.value) || JSON.stringify(persistedSetting?.packageEditor) !== JSON.stringify(persistedSetting?.editor), |
||||
}]; |
||||
|
||||
hydrate(changes); |
||||
}, 100, [hydrate, persistedSetting]) as () => void; |
||||
|
||||
return { update, reset }; |
||||
}; |
||||
|
||||
export const usePrivateSettingDisabledState = (setting: Setting | null | undefined): boolean => { |
||||
const { isDisabled } = useContext(PrivateSettingsContext); |
||||
return useReactiveValue(() => (setting ? isDisabled(setting) : false), [setting?.blocked, setting?.enableQuery]) as unknown as boolean; |
||||
}; |
||||
|
||||
export const usePrivateSettingsSectionChangedState = (groupId: string, sectionName: string): boolean => |
||||
!!useSelector((state) => |
||||
state.settings.some(({ group, section, changed }) => |
||||
group === groupId && ((!sectionName && !section) || (sectionName === section)) && changed)); |
||||
|
||||
export const usePrivateSetting = (_id: string): Setting | null | undefined => { |
||||
const selectSetting = (settings: Setting[]): Setting | undefined => settings.find((setting) => setting._id === _id); |
||||
|
||||
const setting = useSelector((state) => selectSetting(state.settings)); |
||||
const persistedSetting = useSelector((state) => selectSetting(state.persistedSettings)); |
||||
|
||||
const { update, reset } = usePrivateSettingActions(persistedSetting); |
||||
const disabled = usePrivateSettingDisabledState(persistedSetting); |
||||
|
||||
if (!setting) { |
||||
return null; |
||||
} |
||||
|
||||
return { |
||||
...setting, |
||||
disabled, |
||||
update, |
||||
reset, |
||||
}; |
||||
}; |
@ -0,0 +1,4 @@ |
||||
declare module '@rocket.chat/fuselage-hooks' { |
||||
export const useDebouncedCallback: (fn: (...args: any[]) => any, ms: number, deps: any[]) => (...args: any[]) => any; |
||||
export const useMutableCallback: (fn: (...args: any[]) => any) => (...args: any[]) => any; |
||||
} |
@ -0,0 +1,3 @@ |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
export const useIsReducedMotionPreferred = () => useMediaQuery('(prefers-reduced-motion: reduce)'); |
@ -1,3 +0,0 @@ |
||||
import { modal } from '../../app/ui-utils/client/lib/modal'; |
||||
|
||||
export const useModal = () => modal; |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import { ModalContext } from '../contexts/ModalContext'; |
||||
import { modal } from '../../app/ui-utils/client/lib/modal'; |
||||
|
||||
function ModalProvider({ children }) { |
||||
return <ModalContext.Provider children={children} value={modal} />; |
||||
} |
||||
|
||||
export default ModalProvider; |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import NotFoundPage from './NotFoundPage'; |
||||
|
||||
export default { |
||||
title: 'views/notFound/NotFoundPage', |
||||
component: NotFoundPage, |
||||
}; |
||||
|
||||
export const Default = () => <NotFoundPage />; |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue