From f5617aca1c4c95a38cc439da9230f168e604e724 Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins <matiss@mja.lv> Date: Sun, 2 Jul 2023 17:41:02 +0100 Subject: [PATCH] :recycle: moving more components out of common.tsx (#1257) Moving some more components out of `common.tsx` into their own files. There are no functional changes. This is a direct copy&paste into new files. --- .../desktop-client/src/components/common.tsx | 434 +----------------- .../src/components/common/AlignedText.tsx | 56 +++ .../src/components/common/CustomSelect.tsx | 52 +++ .../src/components/common/FormError.tsx | 16 + .../src/components/common/InitialFocus.tsx | 34 ++ .../components/common/InputWithContent.tsx | 72 +++ .../src/components/common/Menu.tsx | 181 ++++++++ .../src/components/common/Search.tsx | 43 ++ upcoming-release-notes/1257.md | 6 + 9 files changed, 471 insertions(+), 423 deletions(-) create mode 100644 packages/desktop-client/src/components/common/AlignedText.tsx create mode 100644 packages/desktop-client/src/components/common/CustomSelect.tsx create mode 100644 packages/desktop-client/src/components/common/FormError.tsx create mode 100644 packages/desktop-client/src/components/common/InitialFocus.tsx create mode 100644 packages/desktop-client/src/components/common/InputWithContent.tsx create mode 100644 packages/desktop-client/src/components/common/Menu.tsx create mode 100644 packages/desktop-client/src/components/common/Search.tsx create mode 100644 upcoming-release-notes/1257.md diff --git a/packages/desktop-client/src/components/common.tsx b/packages/desktop-client/src/components/common.tsx index 554b5c6bb..d63d82331 100644 --- a/packages/desktop-client/src/components/common.tsx +++ b/packages/desktop-client/src/components/common.tsx @@ -1,52 +1,40 @@ import React, { useRef, - useEffect, useLayoutEffect, - useState, useCallback, - type ChangeEvent, type ComponentProps, - type ReactElement, type ReactNode, - type Ref, forwardRef, - createElement, - cloneElement, } from 'react'; import { NavLink, useMatch, useNavigate } from 'react-router-dom'; -import { - ListboxInput, - ListboxButton, - ListboxPopover, - ListboxList, - ListboxOption, -} from '@reach/listbox'; import { type CSSProperties, css } from 'glamor'; -import ExpandArrow from '../icons/v0/ExpandArrow'; import { styles, colors } from '../style'; import type { HTMLPropsWithStyle } from '../types/utils'; -import Block from './common/Block'; import Button from './common/Button'; -import Input, { defaultInputStyle } from './common/Input'; -import Text from './common/Text'; -import View from './common/View'; -export { default as Modal, ModalButtons } from './common/Modal'; +export { default as AlignedText } from './common/AlignedText'; export { default as Block } from './common/Block'; export { default as Button, ButtonWithLoading } from './common/Button'; export { default as Card } from './common/Card'; +export { default as CustomSelect } from './common/CustomSelect'; +export { default as FormError } from './common/FormError'; export { default as HoverTarget } from './common/HoverTarget'; +export { default as InitialFocus } from './common/InitialFocus'; export { default as InlineField } from './common/InlineField'; export { default as Input } from './common/Input'; +export { default as InputWithContent } from './common/InputWithContent'; export { default as Label } from './common/Label'; -export { default as View } from './common/View'; -export { default as Text } from './common/Text'; -export { default as TextOneLine } from './common/TextOneLine'; +export { default as Menu } from './common/Menu'; +export { default as Modal, ModalButtons } from './common/Modal'; +export { default as Search } from './common/Search'; export { default as Select } from './common/Select'; export { default as Stack } from './Stack'; +export { default as Text } from './common/Text'; +export { default as TextOneLine } from './common/TextOneLine'; +export { default as View } from './common/View'; type UseStableCallbackArg = (...args: unknown[]) => unknown; @@ -174,368 +162,6 @@ export function ButtonLink({ ); } -type InputWithContentProps = ComponentProps<typeof Input> & { - leftContent: ReactNode; - rightContent: ReactNode; - inputStyle?: CSSProperties; - style?: CSSProperties; - getStyle?: (focused: boolean) => CSSProperties; -}; -export function InputWithContent({ - leftContent, - rightContent, - inputStyle, - style, - getStyle, - ...props -}: InputWithContentProps) { - let [focused, setFocused] = useState(false); - - return ( - <View - style={[ - defaultInputStyle, - { - padding: 0, - flex: 1, - flexDirection: 'row', - alignItems: 'center', - }, - focused && { - border: '1px solid ' + colors.b5, - boxShadow: '0 1px 1px ' + colors.b7, - }, - style, - getStyle && getStyle(focused), - ]} - > - {leftContent} - <Input - {...props} - style={[ - inputStyle, - { - flex: 1, - '&, &:focus, &:hover': { - border: 0, - backgroundColor: 'transparent', - boxShadow: 'none', - color: 'inherit', - }, - }, - ]} - onFocus={e => { - setFocused(true); - props.onFocus && props.onFocus(e); - }} - onBlur={e => { - setFocused(false); - props.onBlur && props.onBlur(e); - }} - /> - {rightContent} - </View> - ); -} - -type SearchProps = { - inputRef: Ref<HTMLInputElement>; - value: string; - onChange: (value: string) => unknown; - placeholder: string; - isInModal: boolean; - width?: number; -}; -export function Search({ - inputRef, - value, - onChange, - placeholder, - isInModal, - width = 350, -}: SearchProps) { - return ( - <Input - inputRef={inputRef} - placeholder={placeholder} - value={value} - onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} - style={{ - width, - borderColor: isInModal ? null : 'transparent', - backgroundColor: isInModal ? null : colors.n11, - ':focus': isInModal - ? null - : { - backgroundColor: 'white', - '::placeholder': { color: colors.n8 }, - }, - }} - /> - ); -} - -type CustomSelectProps = { - options: Array<[string, string]>; - value: string; - onChange?: (newValue: string) => void; - style?: CSSProperties; - disabledKeys?: string[]; -}; - -export function CustomSelect({ - options, - value, - onChange, - style, - disabledKeys = [], -}: CustomSelectProps) { - return ( - <ListboxInput - value={value} - onChange={onChange} - style={{ lineHeight: '1em' }} - > - <ListboxButton - {...css([{ borderWidth: 0, padding: 5, borderRadius: 4 }, style])} - arrow={<ExpandArrow style={{ width: 7, height: 7, paddingTop: 3 }} />} - /> - <ListboxPopover style={{ zIndex: 10000, outline: 0, borderRadius: 4 }}> - <ListboxList> - {options.map(([value, label]) => ( - <ListboxOption - key={value} - value={value} - disabled={disabledKeys.includes(value)} - > - {label} - </ListboxOption> - ))} - </ListboxList> - </ListboxPopover> - </ListboxInput> - ); -} - -type KeybindingProps = { - keyName: ReactNode; -}; - -function Keybinding({ keyName }: KeybindingProps) { - return <Text style={{ fontSize: 10, color: colors.n6 }}>{keyName}</Text>; -} - -type MenuItem = { - type?: string | symbol; - name: string; - disabled?: boolean; - icon?; - iconSize?: number; - text: string; - key?: string; -}; - -type MenuProps = { - header?: ReactNode; - footer?: ReactNode; - items: Array<MenuItem | typeof Menu.line>; - onMenuSelect; -}; - -export function Menu({ - header, - footer, - items: allItems, - onMenuSelect, -}: MenuProps) { - let elRef = useRef(null); - let items = allItems.filter(x => x); - let [hoveredIndex, setHoveredIndex] = useState(null); - - useEffect(() => { - const el = elRef.current; - el.focus(); - - let onKeyDown = e => { - let filteredItems = items.filter( - item => item && item !== Menu.line && item.type !== Menu.label, - ); - let currentIndex = filteredItems.indexOf(items[hoveredIndex]); - - let transformIndex = idx => items.indexOf(filteredItems[idx]); - - switch (e.key) { - case 'ArrowUp': - e.preventDefault(); - setHoveredIndex( - hoveredIndex === null - ? 0 - : transformIndex(Math.max(currentIndex - 1, 0)), - ); - break; - case 'ArrowDown': - e.preventDefault(); - setHoveredIndex( - hoveredIndex === null - ? 0 - : transformIndex( - Math.min(currentIndex + 1, filteredItems.length - 1), - ), - ); - break; - case 'Enter': - e.preventDefault(); - const item = items[hoveredIndex]; - if (hoveredIndex !== null && item !== Menu.line) { - onMenuSelect && onMenuSelect(item.name); - } - break; - default: - } - }; - - el.addEventListener('keydown', onKeyDown); - - return () => { - el.removeEventListener('keydown', onKeyDown); - }; - }, [hoveredIndex]); - - return ( - <View - style={{ outline: 'none', borderRadius: 4, overflow: 'hidden' }} - tabIndex={1} - innerRef={elRef} - > - {header} - {items.map((item, idx) => { - if (item === Menu.line) { - return ( - <View key={idx} style={{ margin: '3px 0px' }}> - <View style={{ borderTop: '1px solid ' + colors.n10 }} /> - </View> - ); - } else if (item.type === Menu.label) { - return ( - <Text - key={item.name} - style={{ - color: colors.n6, - fontSize: 11, - lineHeight: '1em', - textTransform: 'uppercase', - margin: '3px 9px', - marginTop: 5, - }} - > - {item.name} - </Text> - ); - } - - let lastItem = items[idx - 1]; - - return ( - <View - role="button" - key={item.name} - style={[ - { - cursor: 'default', - padding: '9px 10px', - marginTop: - idx === 0 || - lastItem === Menu.line || - lastItem.type === Menu.label - ? 0 - : -3, - flexDirection: 'row', - alignItems: 'center', - }, - item.disabled && { color: colors.n7 }, - !item.disabled && - hoveredIndex === idx && { backgroundColor: colors.n10 }, - ]} - onMouseEnter={() => setHoveredIndex(idx)} - onMouseLeave={() => setHoveredIndex(null)} - onClick={e => - !item.disabled && onMenuSelect && onMenuSelect(item.name) - } - > - {/* Force it to line up evenly */} - <Text style={{ lineHeight: 0 }}> - {item.icon && - createElement(item.icon, { - width: item.iconSize || 10, - height: item.iconSize || 10, - style: { marginRight: 7, width: 10 }, - })} - </Text> - <Text>{item.text}</Text> - <View style={{ flex: 1 }} /> - {item.key && <Keybinding keyName={item.key} />} - </View> - ); - })} - {footer} - </View> - ); -} - -const MenuLine: unique symbol = Symbol('menu-line'); -Menu.line = MenuLine; -Menu.label = Symbol('menu-label'); - -type AlignedTextProps = ComponentProps<typeof View> & { - left; - right; - style?: CSSProperties; - leftStyle?: CSSProperties; - rightStyle?: CSSProperties; - truncate?: 'left' | 'right'; -}; -export function AlignedText({ - left, - right, - style, - leftStyle, - rightStyle, - truncate = 'left', - ...nativeProps -}: AlignedTextProps) { - const truncateStyle = { - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - }; - - return ( - <View - style={[{ flexDirection: 'row', alignItems: 'center' }, style]} - {...nativeProps} - > - <Block - style={[ - { marginRight: 10 }, - truncate === 'left' && truncateStyle, - leftStyle, - ]} - > - {left} - </Block> - <Block - style={[ - { flex: 1, textAlign: 'right' }, - truncate === 'right' && truncateStyle, - rightStyle, - ]} - > - {right} - </Block> - </View> - ); -} - type PProps = HTMLPropsWithStyle<HTMLDivElement> & { isLast?: boolean; }; @@ -550,43 +176,5 @@ export function P({ style, isLast, children, ...props }: PProps) { ); } -type FormErrorProps = { - style?: CSSProperties; - children?: ReactNode; -}; - -export function FormError({ style, children }: FormErrorProps) { - return ( - <View style={[{ color: 'red', fontSize: 13 }, style]}>{children}</View> - ); -} - -type InitialFocusProps = { - children?: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement); -}; - -export function InitialFocus({ children }: InitialFocusProps) { - let node = useRef(null); - - useEffect(() => { - if (node.current && !global.IS_DESIGN_MODE) { - // This is needed to avoid a strange interaction with - // `ScopeTab`, which doesn't allow it to be focused at first for - // some reason. Need to look into it. - setTimeout(() => { - if (node.current) { - node.current.focus(); - node.current.setSelectionRange(0, 10000); - } - }, 0); - } - }, []); - - if (typeof children === 'function') { - return children(node); - } - return cloneElement(children, { inputRef: node }); -} - export * from './tooltips'; export { useTooltip } from './tooltips'; diff --git a/packages/desktop-client/src/components/common/AlignedText.tsx b/packages/desktop-client/src/components/common/AlignedText.tsx new file mode 100644 index 000000000..1636bf17c --- /dev/null +++ b/packages/desktop-client/src/components/common/AlignedText.tsx @@ -0,0 +1,56 @@ +import { type ComponentProps } from 'react'; + +import { type CSSProperties } from 'glamor'; + +import Block from './Block'; +import View from './View'; + +type AlignedTextProps = ComponentProps<typeof View> & { + left; + right; + style?: CSSProperties; + leftStyle?: CSSProperties; + rightStyle?: CSSProperties; + truncate?: 'left' | 'right'; +}; +export default function AlignedText({ + left, + right, + style, + leftStyle, + rightStyle, + truncate = 'left', + ...nativeProps +}: AlignedTextProps) { + const truncateStyle = { + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + }; + + return ( + <View + style={[{ flexDirection: 'row', alignItems: 'center' }, style]} + {...nativeProps} + > + <Block + style={[ + { marginRight: 10 }, + truncate === 'left' && truncateStyle, + leftStyle, + ]} + > + {left} + </Block> + <Block + style={[ + { flex: 1, textAlign: 'right' }, + truncate === 'right' && truncateStyle, + rightStyle, + ]} + > + {right} + </Block> + </View> + ); +} diff --git a/packages/desktop-client/src/components/common/CustomSelect.tsx b/packages/desktop-client/src/components/common/CustomSelect.tsx new file mode 100644 index 000000000..7114057fc --- /dev/null +++ b/packages/desktop-client/src/components/common/CustomSelect.tsx @@ -0,0 +1,52 @@ +import { + ListboxInput, + ListboxButton, + ListboxPopover, + ListboxList, + ListboxOption, +} from '@reach/listbox'; +import { type CSSProperties, css } from 'glamor'; + +import ExpandArrow from '../../icons/v0/ExpandArrow'; + +type CustomSelectProps = { + options: Array<[string, string]>; + value: string; + onChange?: (newValue: string) => void; + style?: CSSProperties; + disabledKeys?: string[]; +}; + +export default function CustomSelect({ + options, + value, + onChange, + style, + disabledKeys = [], +}: CustomSelectProps) { + return ( + <ListboxInput + value={value} + onChange={onChange} + style={{ lineHeight: '1em' }} + > + <ListboxButton + {...css([{ borderWidth: 0, padding: 5, borderRadius: 4 }, style])} + arrow={<ExpandArrow style={{ width: 7, height: 7, paddingTop: 3 }} />} + /> + <ListboxPopover style={{ zIndex: 10000, outline: 0, borderRadius: 4 }}> + <ListboxList> + {options.map(([value, label]) => ( + <ListboxOption + key={value} + value={value} + disabled={disabledKeys.includes(value)} + > + {label} + </ListboxOption> + ))} + </ListboxList> + </ListboxPopover> + </ListboxInput> + ); +} diff --git a/packages/desktop-client/src/components/common/FormError.tsx b/packages/desktop-client/src/components/common/FormError.tsx new file mode 100644 index 000000000..90854653f --- /dev/null +++ b/packages/desktop-client/src/components/common/FormError.tsx @@ -0,0 +1,16 @@ +import { type ReactNode } from 'react'; + +import { type CSSProperties } from 'glamor'; + +import View from './View'; + +type FormErrorProps = { + style?: CSSProperties; + children?: ReactNode; +}; + +export default function FormError({ style, children }: FormErrorProps) { + return ( + <View style={[{ color: 'red', fontSize: 13 }, style]}>{children}</View> + ); +} diff --git a/packages/desktop-client/src/components/common/InitialFocus.tsx b/packages/desktop-client/src/components/common/InitialFocus.tsx new file mode 100644 index 000000000..7c6ad0a65 --- /dev/null +++ b/packages/desktop-client/src/components/common/InitialFocus.tsx @@ -0,0 +1,34 @@ +import { + type ReactElement, + type Ref, + cloneElement, + useEffect, + useRef, +} from 'react'; + +type InitialFocusProps = { + children?: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement); +}; + +export default function InitialFocus({ children }: InitialFocusProps) { + let node = useRef(null); + + useEffect(() => { + if (node.current && !global.IS_DESIGN_MODE) { + // This is needed to avoid a strange interaction with + // `ScopeTab`, which doesn't allow it to be focused at first for + // some reason. Need to look into it. + setTimeout(() => { + if (node.current) { + node.current.focus(); + node.current.setSelectionRange(0, 10000); + } + }, 0); + } + }, []); + + if (typeof children === 'function') { + return children(node); + } + return cloneElement(children, { inputRef: node }); +} diff --git a/packages/desktop-client/src/components/common/InputWithContent.tsx b/packages/desktop-client/src/components/common/InputWithContent.tsx new file mode 100644 index 000000000..6d907be0f --- /dev/null +++ b/packages/desktop-client/src/components/common/InputWithContent.tsx @@ -0,0 +1,72 @@ +import { type ComponentProps, type ReactNode, useState } from 'react'; + +import { type CSSProperties } from 'glamor'; + +import { colors } from '../../style'; + +import Input, { defaultInputStyle } from './Input'; +import View from './View'; + +type InputWithContentProps = ComponentProps<typeof Input> & { + leftContent: ReactNode; + rightContent: ReactNode; + inputStyle?: CSSProperties; + style?: CSSProperties; + getStyle?: (focused: boolean) => CSSProperties; +}; +export default function InputWithContent({ + leftContent, + rightContent, + inputStyle, + style, + getStyle, + ...props +}: InputWithContentProps) { + let [focused, setFocused] = useState(false); + + return ( + <View + style={[ + defaultInputStyle, + { + padding: 0, + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + focused && { + border: '1px solid ' + colors.b5, + boxShadow: '0 1px 1px ' + colors.b7, + }, + style, + getStyle && getStyle(focused), + ]} + > + {leftContent} + <Input + {...props} + style={[ + inputStyle, + { + flex: 1, + '&, &:focus, &:hover': { + border: 0, + backgroundColor: 'transparent', + boxShadow: 'none', + color: 'inherit', + }, + }, + ]} + onFocus={e => { + setFocused(true); + props.onFocus && props.onFocus(e); + }} + onBlur={e => { + setFocused(false); + props.onBlur && props.onBlur(e); + }} + /> + {rightContent} + </View> + ); +} diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx new file mode 100644 index 000000000..8aa49d3e1 --- /dev/null +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -0,0 +1,181 @@ +import { + type ReactNode, + createElement, + useEffect, + useRef, + useState, +} from 'react'; + +import { colors } from '../../style'; + +import Text from './Text'; +import View from './View'; + +type KeybindingProps = { + keyName: ReactNode; +}; + +function Keybinding({ keyName }: KeybindingProps) { + return <Text style={{ fontSize: 10, color: colors.n6 }}>{keyName}</Text>; +} + +type MenuItem = { + type?: string | symbol; + name: string; + disabled?: boolean; + icon?; + iconSize?: number; + text: string; + key?: string; +}; + +type MenuProps = { + header?: ReactNode; + footer?: ReactNode; + items: Array<MenuItem | typeof Menu.line>; + onMenuSelect; +}; + +export default function Menu({ + header, + footer, + items: allItems, + onMenuSelect, +}: MenuProps) { + let elRef = useRef(null); + let items = allItems.filter(x => x); + let [hoveredIndex, setHoveredIndex] = useState(null); + + useEffect(() => { + const el = elRef.current; + el.focus(); + + let onKeyDown = e => { + let filteredItems = items.filter( + item => item && item !== Menu.line && item.type !== Menu.label, + ); + let currentIndex = filteredItems.indexOf(items[hoveredIndex]); + + let transformIndex = idx => items.indexOf(filteredItems[idx]); + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + setHoveredIndex( + hoveredIndex === null + ? 0 + : transformIndex(Math.max(currentIndex - 1, 0)), + ); + break; + case 'ArrowDown': + e.preventDefault(); + setHoveredIndex( + hoveredIndex === null + ? 0 + : transformIndex( + Math.min(currentIndex + 1, filteredItems.length - 1), + ), + ); + break; + case 'Enter': + e.preventDefault(); + const item = items[hoveredIndex]; + if (hoveredIndex !== null && item !== Menu.line) { + onMenuSelect && onMenuSelect(item.name); + } + break; + default: + } + }; + + el.addEventListener('keydown', onKeyDown); + + return () => { + el.removeEventListener('keydown', onKeyDown); + }; + }, [hoveredIndex]); + + return ( + <View + style={{ outline: 'none', borderRadius: 4, overflow: 'hidden' }} + tabIndex={1} + innerRef={elRef} + > + {header} + {items.map((item, idx) => { + if (item === Menu.line) { + return ( + <View key={idx} style={{ margin: '3px 0px' }}> + <View style={{ borderTop: '1px solid ' + colors.n10 }} /> + </View> + ); + } else if (item.type === Menu.label) { + return ( + <Text + key={item.name} + style={{ + color: colors.n6, + fontSize: 11, + lineHeight: '1em', + textTransform: 'uppercase', + margin: '3px 9px', + marginTop: 5, + }} + > + {item.name} + </Text> + ); + } + + let lastItem = items[idx - 1]; + + return ( + <View + role="button" + key={item.name} + style={[ + { + cursor: 'default', + padding: '9px 10px', + marginTop: + idx === 0 || + lastItem === Menu.line || + lastItem.type === Menu.label + ? 0 + : -3, + flexDirection: 'row', + alignItems: 'center', + }, + item.disabled && { color: colors.n7 }, + !item.disabled && + hoveredIndex === idx && { backgroundColor: colors.n10 }, + ]} + onMouseEnter={() => setHoveredIndex(idx)} + onMouseLeave={() => setHoveredIndex(null)} + onClick={e => + !item.disabled && onMenuSelect && onMenuSelect(item.name) + } + > + {/* Force it to line up evenly */} + <Text style={{ lineHeight: 0 }}> + {item.icon && + createElement(item.icon, { + width: item.iconSize || 10, + height: item.iconSize || 10, + style: { marginRight: 7, width: 10 }, + })} + </Text> + <Text>{item.text}</Text> + <View style={{ flex: 1 }} /> + {item.key && <Keybinding keyName={item.key} />} + </View> + ); + })} + {footer} + </View> + ); +} + +const MenuLine: unique symbol = Symbol('menu-line'); +Menu.line = MenuLine; +Menu.label = Symbol('menu-label'); diff --git a/packages/desktop-client/src/components/common/Search.tsx b/packages/desktop-client/src/components/common/Search.tsx new file mode 100644 index 000000000..c1b6bf832 --- /dev/null +++ b/packages/desktop-client/src/components/common/Search.tsx @@ -0,0 +1,43 @@ +import { type ChangeEvent, type Ref } from 'react'; + +import { colors } from '../../style'; + +import Input from './Input'; + +type SearchProps = { + inputRef: Ref<HTMLInputElement>; + value: string; + onChange: (value: string) => unknown; + placeholder: string; + isInModal: boolean; + width?: number; +}; + +export default function Search({ + inputRef, + value, + onChange, + placeholder, + isInModal, + width = 350, +}: SearchProps) { + return ( + <Input + inputRef={inputRef} + placeholder={placeholder} + value={value} + onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} + style={{ + width, + borderColor: isInModal ? null : 'transparent', + backgroundColor: isInModal ? null : colors.n11, + ':focus': isInModal + ? null + : { + backgroundColor: 'white', + '::placeholder': { color: colors.n8 }, + }, + }} + /> + ); +} diff --git a/upcoming-release-notes/1257.md b/upcoming-release-notes/1257.md new file mode 100644 index 000000000..e0a8d2164 --- /dev/null +++ b/upcoming-release-notes/1257.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Moving more components from `common.tsx` to separate files inside the `common` folder -- GitLab