feat: add New Object Custom form (#2105)
* feat: add New Object Custom form Closes #1808 * fix: fix lint error
This commit is contained in:
@ -1,14 +1,22 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { AutosizeTextInput } from '@/ui/input/components/AutosizeTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
|
||||
type SettingsObjectFormSectionProps = {
|
||||
disabled?: boolean;
|
||||
singularName?: string;
|
||||
pluralName?: string;
|
||||
description?: string;
|
||||
onChange?: (
|
||||
formValues: Partial<{
|
||||
singularName: string;
|
||||
pluralName: string;
|
||||
description: string;
|
||||
}>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
@ -23,9 +31,11 @@ const StyledTextInput = styled(TextInput)`
|
||||
`;
|
||||
|
||||
export const SettingsObjectFormSection = ({
|
||||
singularName,
|
||||
pluralName,
|
||||
description,
|
||||
disabled,
|
||||
singularName = '',
|
||||
pluralName = '',
|
||||
description = '',
|
||||
onChange,
|
||||
}: SettingsObjectFormSectionProps) => (
|
||||
<Section>
|
||||
<H2Title
|
||||
@ -35,19 +45,25 @@ export const SettingsObjectFormSection = ({
|
||||
<StyledInputsContainer>
|
||||
<StyledTextInput
|
||||
label="Singular"
|
||||
placeholder="Invoice"
|
||||
placeholder="Investor"
|
||||
value={singularName}
|
||||
onChange={(value) => onChange?.({ singularName: value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledTextInput
|
||||
label="Plural"
|
||||
placeholder="Invoices"
|
||||
placeholder="Investors"
|
||||
value={pluralName}
|
||||
onChange={(value) => onChange?.({ pluralName: value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
<AutosizeTextInput
|
||||
<TextArea
|
||||
placeholder="Write a description"
|
||||
minRows={4}
|
||||
value={description}
|
||||
onChange={(value) => onChange?.({ description: value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@ -6,6 +5,13 @@ import { IconBox, IconDatabase, IconFileCheck } from '@/ui/display/icon';
|
||||
|
||||
import { SettingsObjectTypeCard } from './SettingsObjectTypeCard';
|
||||
|
||||
export type NewObjectType = 'Standard' | 'Custom' | 'Remote';
|
||||
|
||||
type SettingsNewObjectTypeProps = {
|
||||
selectedType?: NewObjectType;
|
||||
onTypeSelect?: (type: NewObjectType) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -13,12 +19,11 @@ const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsNewObjectType = () => {
|
||||
export const SettingsNewObjectType = ({
|
||||
selectedType = 'Standard',
|
||||
onTypeSelect,
|
||||
}: SettingsNewObjectTypeProps) => {
|
||||
const theme = useTheme();
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
const handleCardClick = (selectedType: string) => {
|
||||
setSelectedType(selectedType);
|
||||
};
|
||||
return (
|
||||
<StyledContainer>
|
||||
<SettingsObjectTypeCard
|
||||
@ -32,8 +37,8 @@ export const SettingsNewObjectType = () => {
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleCardClick('Standard')}
|
||||
></SettingsObjectTypeCard>
|
||||
onClick={() => onTypeSelect?.('Standard')}
|
||||
/>
|
||||
<SettingsObjectTypeCard
|
||||
title="Custom"
|
||||
color="orange"
|
||||
@ -45,8 +50,8 @@ export const SettingsNewObjectType = () => {
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleCardClick('Custom')}
|
||||
></SettingsObjectTypeCard>
|
||||
onClick={() => onTypeSelect?.('Custom')}
|
||||
/>
|
||||
<SettingsObjectTypeCard
|
||||
title="Remote"
|
||||
soon
|
||||
@ -60,7 +65,7 @@ export const SettingsNewObjectType = () => {
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
}
|
||||
></SettingsObjectTypeCard>
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -58,21 +58,19 @@ export const SettingsObjectTypeCard = ({
|
||||
}: SettingsObjectTypeCardProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div onClick={() => onclick}>
|
||||
<StyledObjectTypeCard
|
||||
title={title}
|
||||
soon={soon}
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
>
|
||||
{prefixIcon}
|
||||
<StyledTag color={color} text={title} />
|
||||
{soon && <SoonPill />}
|
||||
{!disabled && selected && <StyledIconCheck size={theme.icon.size.md} />}
|
||||
</StyledObjectTypeCard>
|
||||
</div>
|
||||
<StyledObjectTypeCard
|
||||
title={title}
|
||||
soon={soon}
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
>
|
||||
{prefixIcon}
|
||||
<StyledTag color={color} text={title} />
|
||||
{soon && <SoonPill />}
|
||||
{!disabled && selected && <StyledIconCheck size={theme.icon.size.md} />}
|
||||
</StyledObjectTypeCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconPigMoney } from '@/ui/display/icon';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
@ -23,15 +24,19 @@ const StyledArrowContainer = styled.div`
|
||||
`;
|
||||
|
||||
type SettingsObjectIconSectionProps = {
|
||||
Icon: IconComponent;
|
||||
iconKey: string;
|
||||
setIconPicker?: (icon: { Icon: IconComponent; iconKey: string }) => void;
|
||||
disabled?: boolean;
|
||||
Icon?: IconComponent;
|
||||
iconKey?: string;
|
||||
label?: string;
|
||||
onChange?: (icon: { Icon: IconComponent; iconKey: string }) => void;
|
||||
};
|
||||
|
||||
export const SettingsObjectIconSection = ({
|
||||
Icon,
|
||||
iconKey,
|
||||
setIconPicker,
|
||||
disabled,
|
||||
Icon = IconPigMoney,
|
||||
iconKey = 'IconPigMoney',
|
||||
label,
|
||||
onChange,
|
||||
}: SettingsObjectIconSectionProps) => {
|
||||
return (
|
||||
<Section>
|
||||
@ -41,15 +46,16 @@ export const SettingsObjectIconSection = ({
|
||||
/>
|
||||
<StyledContainer>
|
||||
<IconPicker
|
||||
disabled={disabled}
|
||||
selectedIconKey={iconKey}
|
||||
onChange={(icon) => {
|
||||
setIconPicker?.({ Icon: icon.Icon, iconKey: icon.iconKey });
|
||||
onChange?.({ Icon: icon.Icon, iconKey: icon.iconKey });
|
||||
}}
|
||||
/>
|
||||
<StyledArrowContainer>
|
||||
<img src={ArrowRight} alt="Arrow right" width={32} height={16} />
|
||||
</StyledArrowContainer>
|
||||
<SettingsObjectIconWithLabel Icon={Icon} label="Workspaces" />
|
||||
<SettingsObjectIconWithLabel Icon={Icon} label={label || 'Investors'} />
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@ -69,6 +69,7 @@ export {
|
||||
IconNumbers,
|
||||
IconPencil,
|
||||
IconPhone,
|
||||
IconPigMoney,
|
||||
IconPlane,
|
||||
IconPlug,
|
||||
IconPlus,
|
||||
|
||||
@ -17,6 +17,7 @@ import { DropdownMenuSkeletonItem } from '../relation-picker/components/skeleton
|
||||
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
|
||||
|
||||
type IconPickerProps = {
|
||||
disabled?: boolean;
|
||||
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
|
||||
selectedIconKey?: string;
|
||||
onClickOutside?: () => void;
|
||||
@ -39,6 +40,7 @@ const convertIconKeyToLabel = (iconKey: string) =>
|
||||
iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim();
|
||||
|
||||
export const IconPicker = ({
|
||||
disabled,
|
||||
onChange,
|
||||
selectedIconKey,
|
||||
onClickOutside,
|
||||
@ -81,6 +83,7 @@ export const IconPicker = ({
|
||||
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
Icon={selectedIconKey ? icons[selectedIconKey] : IconApps}
|
||||
variant="secondary"
|
||||
/>
|
||||
@ -100,6 +103,7 @@ export const IconPicker = ({
|
||||
<StyledMenuIconItemsContainer>
|
||||
{iconKeys.map((iconKey) => (
|
||||
<StyledLightIconButton
|
||||
key={iconKey}
|
||||
aria-label={convertIconKeyToLabel(iconKey)}
|
||||
isSelected={selectedIconKey === iconKey}
|
||||
size="medium"
|
||||
|
||||
63
front/src/modules/ui/input/components/TextArea.tsx
Normal file
63
front/src/modules/ui/input/components/TextArea.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const MAX_ROWS = 5;
|
||||
|
||||
export type TextAreaProps = {
|
||||
disabled?: boolean;
|
||||
minRows?: number;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const StyledTextArea = styled(TextareaAutosize)`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-family: inherit;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
line-height: 16px;
|
||||
overflow: auto;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextArea = ({
|
||||
disabled,
|
||||
placeholder,
|
||||
minRows = 1,
|
||||
value = '',
|
||||
onChange,
|
||||
}: TextAreaProps) => {
|
||||
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
||||
|
||||
return (
|
||||
<StyledTextArea
|
||||
placeholder={placeholder}
|
||||
maxRows={MAX_ROWS}
|
||||
minRows={computedMinRows}
|
||||
value={value}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -19,7 +19,7 @@ import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
|
||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||
|
||||
type TextInputComponentProps = Omit<
|
||||
export type TextInputComponentProps = Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange'
|
||||
> & {
|
||||
@ -53,9 +53,10 @@ const StyledInputContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border: none;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-right: none;
|
||||
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
@ -74,6 +75,10 @@ const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledErrorHelper = styled.div`
|
||||
@ -84,8 +89,10 @@ const StyledErrorHelper = styled.div`
|
||||
|
||||
const StyledTrailingIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-left: none;
|
||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { TextArea, TextAreaProps } from '../TextArea';
|
||||
|
||||
type RenderProps = TextAreaProps;
|
||||
|
||||
const Render = (args: RenderProps) => {
|
||||
const [value, setValue] = useState(args.value);
|
||||
const handleChange = (text: string) => {
|
||||
args.onChange?.(text);
|
||||
setValue(text);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <TextArea {...args} value={value} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof TextArea> = {
|
||||
title: 'UI/Input/TextArea',
|
||||
component: TextArea,
|
||||
decorators: [ComponentDecorator],
|
||||
args: { minRows: 4, placeholder: 'Lorem Ipsum' },
|
||||
render: Render,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TextArea>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Filled: Story = {
|
||||
args: { value: 'Lorem Ipsum' },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true, value: 'Lorem Ipsum' },
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { TextInput, TextInputComponentProps } from '../TextInput';
|
||||
|
||||
type RenderProps = TextInputComponentProps;
|
||||
|
||||
const Render = (args: RenderProps) => {
|
||||
const [value, setValue] = useState(args.value);
|
||||
const handleChange = (text: string) => {
|
||||
args.onChange?.(text);
|
||||
setValue(text);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <TextInput {...args} value={value} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof TextInput> = {
|
||||
title: 'UI/Input/TextInput',
|
||||
component: TextInput,
|
||||
decorators: [ComponentDecorator],
|
||||
args: { placeholder: 'Tim' },
|
||||
render: Render,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TextInput>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Filled: Story = {
|
||||
args: { value: 'Tim' },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true, value: 'Tim' },
|
||||
};
|
||||
@ -1,9 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsNewObjectType } from '@/settings/data-model/new-object/components/SettingsNewObjectType';
|
||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||
import {
|
||||
NewObjectType,
|
||||
SettingsNewObjectType,
|
||||
} from '@/settings/data-model/new-object/components/SettingsNewObjectType';
|
||||
import { SettingsObjectIconSection } from '@/settings/data-model/object-edit/SettingsObjectIconSection';
|
||||
import { IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
@ -12,6 +18,22 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
|
||||
export const SettingsNewObject = () => {
|
||||
const navigate = useNavigate();
|
||||
const [selectedObjectType, setSelectedObjectType] =
|
||||
useState<NewObjectType>('Standard');
|
||||
|
||||
const [customFormValues, setCustomFormValues] = useState<
|
||||
Partial<{
|
||||
pluralName: string;
|
||||
singularName: string;
|
||||
description: string;
|
||||
}>
|
||||
>({});
|
||||
|
||||
const canSave =
|
||||
selectedObjectType === 'Custom' &&
|
||||
!!customFormValues.pluralName &&
|
||||
!!customFormValues.singularName;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
@ -23,11 +45,11 @@ export const SettingsNewObject = () => {
|
||||
]}
|
||||
/>
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => {
|
||||
navigate('/settings/objects');
|
||||
}}
|
||||
onSave={() => {}}
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
</SettingsHeaderContainer>
|
||||
<Section>
|
||||
@ -35,8 +57,27 @@ export const SettingsNewObject = () => {
|
||||
title="Object Type"
|
||||
description="The type of object you want to add"
|
||||
/>
|
||||
<SettingsNewObjectType />
|
||||
<SettingsNewObjectType
|
||||
selectedType={selectedObjectType}
|
||||
onTypeSelect={setSelectedObjectType}
|
||||
/>
|
||||
</Section>
|
||||
{selectedObjectType === 'Custom' && (
|
||||
<>
|
||||
<SettingsObjectIconSection label={customFormValues.pluralName} />
|
||||
<SettingsObjectFormSection
|
||||
singularName={customFormValues.singularName}
|
||||
pluralName={customFormValues.pluralName}
|
||||
description={customFormValues.description}
|
||||
onChange={(formValues) => {
|
||||
setCustomFormValues((previousValues) => ({
|
||||
...previousValues,
|
||||
...formValues,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@ -40,10 +40,13 @@ export const SettingsObjectEdit = () => {
|
||||
{activeObject && (
|
||||
<>
|
||||
<SettingsObjectIconSection
|
||||
disabled={activeObject.type === 'standard'}
|
||||
Icon={activeObject.Icon}
|
||||
iconKey={activeObject.Icon.name}
|
||||
label={activeObject.name}
|
||||
/>
|
||||
<SettingsObjectFormSection
|
||||
disabled={activeObject.type === 'standard'}
|
||||
singularName={activeObject.singularName}
|
||||
pluralName={activeObject.name}
|
||||
description={activeObject.description}
|
||||
@ -51,12 +54,12 @@ export const SettingsObjectEdit = () => {
|
||||
</>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description={`Disable object`} />
|
||||
<H2Title title="Danger zone" description="Disable object" />
|
||||
<Button
|
||||
Icon={IconArchive}
|
||||
title="Disable"
|
||||
size="small"
|
||||
onClick={() => {}}
|
||||
onClick={() => undefined}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
|
||||
Reference in New Issue
Block a user