feat: Add Box, Group and Stack layout components.

This commit is contained in:
a.bouhuolia
2022-10-04 00:29:48 +02:00
parent 41db96d958
commit f9a7021f55
17 changed files with 204 additions and 101 deletions

View File

@@ -0,0 +1,11 @@
import React from 'react';
export interface BoxProps {
className?: string;
}
export function Box({ className, ...rest }: BoxProps) {
const Element = 'div';
return <Element className={className} {...rest} />;
}

View File

@@ -0,0 +1 @@
export * from './Box';

View File

@@ -0,0 +1,56 @@
import React from 'react';
import styled from 'styled-components';
import { Box } from '../Box';
import { filterFalsyChildren } from './_utils';
export type GroupPosition = 'right' | 'center' | 'left' | 'apart';
export const GROUP_POSITIONS = {
left: 'flex-start',
center: 'center',
right: 'flex-end',
apart: 'space-between',
};
export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
/** Defines justify-content property */
position?: GroupPosition;
/** Defined flex-wrap property */
noWrap?: boolean;
/** Defines flex-grow property for each element, true -> 1, false -> 0 */
grow?: boolean;
/** Space between elements */
spacing?: number;
/** Defines align-items css property */
align?: React.CSSProperties['alignItems'];
}
const defaultProps: Partial<GroupProps> = {
position: 'left',
spacing: 20,
};
export function Group({ children, ...props }: GroupProps) {
const groupProps = {
...defaultProps,
...props,
};
const filteredChildren = filterFalsyChildren(children);
return <GroupStyled {...groupProps}>{filteredChildren}</GroupStyled>;
}
const GroupStyled = styled(Box)`
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
justify-content: ${(props: GroupProps) =>
GROUP_POSITIONS[props.position || 'left']};
gap: ${(props: GroupProps) => props.spacing}px;
`;

View File

@@ -0,0 +1,5 @@
import { Children, ReactElement, ReactNode } from 'react';
export function filterFalsyChildren(children: ReactNode) {
return (Children.toArray(children) as ReactElement[]).filter(Boolean);
}

View File

@@ -0,0 +1 @@
export * from './Group';

View File

@@ -0,0 +1,36 @@
import React from 'react';
import styled from 'styled-components';
import { Box } from '../Box';
export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
/** Key of theme.spacing or number to set gap in px */
spacing?: number;
/** align-items CSS property */
align?: React.CSSProperties['alignItems'];
/** justify-content CSS property */
justify?: React.CSSProperties['justifyContent'];
}
const defaultProps: Partial<StackProps> = {
spacing: 20,
align: 'stretch',
justify: 'top',
};
export function Stack(props: StackProps) {
const stackProps = {
...defaultProps,
...props,
};
return <StackStyled {...stackProps} />;
}
const StackStyled = styled(Box)`
display: flex;
flex-direction: column;
align-items: align;
justify-content: justify;
gap: ${(props: StackProps) => props.spacing}px;
`;

View File

@@ -0,0 +1 @@
export * from './Stack';

View File

@@ -0,0 +1,3 @@
export * from './Box';
export * from './Group';
export * from './Stack';

View File

@@ -63,5 +63,6 @@ export * from './Indicator';
export * from './EmptyStatus';
export * from './Postbox';
export * from './AppToaster';
export * from './Layout';
export { MODIFIER, ContextMenu, AvaterCell };

View File

@@ -549,7 +549,7 @@ export const SidebarMenu = [
overlayId: ISidebarMenuOverlayIds.Projects,
children: [
{
text: 'Projects Management',
text: 'Projects',
type: ISidebarMenuItemType.Group,
children: [
{

View File

@@ -8,7 +8,6 @@ import {
FFormGroup,
FInputGroup,
FormattedMessage as T,
FieldRequiredHint,
} from '@/components';
import {
ExpenseSelect,
@@ -100,7 +99,6 @@ const MetaLineLabel = styled.div`
font-weight: 500;
margin-bottom: 8px;
`;
const EstimatedAmountWrap = styled.div`
display: block;
text-align: right;

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import React, { useState } from 'react';
import React from 'react';
import { useProjectBillableEntries } from '../../hooks';
import { DialogContent } from '@/components';

View File

@@ -17,13 +17,9 @@ import {
FormattedMessage as T,
FieldRequiredHint,
CustomerSelectField,
Stack,
} from '@/components';
import {
inputIntent,
momentFormatter,
tansformDateValue,
handleDateChange,
} from '@/utils';
import { inputIntent, momentFormatter } from '@/utils';
import { useProjectFormContext } from './ProjectFormProvider';
/**
@@ -39,76 +35,80 @@ function ProjectFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Contact -----------*/}
<FastField name={'contact_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={intl.get('projects.dialog.contact')}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
>
<CustomerSelectField
contacts={customers}
selectedContactId={value}
defaultSelectText={'Select Contact Account'}
onContactSelected={(customer) => {
form.setFieldValue('contact_id', customer.id);
}}
allowCreate={true}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/*------------ Project Name -----------*/}
<FFormGroup
label={intl.get('projects.dialog.project_name')}
name={'name'}
labelInfo={<FieldRequiredHint />}
>
<FInputGroup name="name" />
</FFormGroup>
{/*------------ DeadLine -----------*/}
<FFormGroup
label={intl.get('projects.dialog.deadline')}
name={'deadline'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="deadline"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FFormGroup>
<Stack spacing={25}>
{/*------------ Contact -----------*/}
<FastField name={'contact_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={intl.get('projects.dialog.contact')}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
>
<CustomerSelectField
contacts={customers}
selectedContactId={value}
defaultSelectText={'Find or create a customer'}
onContactSelected={(customer) => {
form.setFieldValue('contact_id', customer.id);
}}
allowCreate={true}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/*------------ CheckBox -----------*/}
<FFormGroup name={'published'}>
<FCheckbox
name="published"
label={intl.get('projects.dialog.calculator_expenses')}
/>
</FFormGroup>
{/*------------ Cost Estimate -----------*/}
<FFormGroup
name={'cost_estimate'}
label={intl.get('projects.dialog.cost_estimate')}
labelInfo={<FieldRequiredHint />}
>
<ControlGroup>
<InputPrependText text={'USD'} />
<FMoneyInputGroup
disabled={values.published}
name={'cost_estimate'}
allowDecimals={true}
allowNegativeValue={true}
/>
</ControlGroup>
</FFormGroup>
{/*------------ Project Name -----------*/}
<FFormGroup
label={intl.get('projects.dialog.project_name')}
name={'name'}
>
<FInputGroup name="name" />
</FFormGroup>
<Stack spacing={15}>
{/*------------ DeadLine -----------*/}
<FFormGroup
label={intl.get('projects.dialog.deadline')}
name={'deadline'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="deadline"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FFormGroup>
{/*------------ CheckBox -----------*/}
<FFormGroup name={'published'}>
<FCheckbox
name="published"
label={intl.get('projects.dialog.calculator_expenses')}
/>
</FFormGroup>
</Stack>
{/*------------ Cost Estimate -----------*/}
<FFormGroup
name={'cost_estimate'}
label={intl.get('projects.dialog.cost_estimate')}
>
<ControlGroup>
<FMoneyInputGroup
disabled={values.published}
name={'cost_estimate'}
allowDecimals={true}
allowNegativeValue={true}
/>
<InputPrependText text={'USD'} />
</ControlGroup>
</FFormGroup>
</Stack>
</div>
);
}

View File

@@ -18,24 +18,13 @@ function ProjectFormFloatingActions({
// Formik context.
const { isSubmitting } = useFormikContext();
// project form dialog context.
const { dialogName } = useProjectFormContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
style={{ minWidth: '100px' }}
type="submit"
>
<T id={'projects.label.create'} />

View File

@@ -45,8 +45,7 @@ export default compose(withDialogRedux())(ProjectFormDialog);
const ProjectFormDialogRoot = styled(Dialog)`
.bp3-dialog-body {
.bp3-form-group {
margin-bottom: 15px;
margin-top: 15px;
margin-bottom: 0;
label.bp3-label {
margin-bottom: 3px;

View File

@@ -24,6 +24,7 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs';
/**
* Projects actions bar.
@@ -62,7 +63,7 @@ function ProjectsActionsBar({
// Handle new project button click.
const handleNewProjectBtnClick = () => {
openDialog('project-form');
openDialog(DialogsName.ProjectForm);
};
return (

View File

@@ -39,18 +39,23 @@ function ProjectsDataTable({
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
useProjectsListContext();
// Retrieve projects table columns.
const columns = useProjectsListColumns();
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PROJECTS);
// Handle delete project.
const handleDeleteProject = ({ id }) => {
openAlert('project-delete', { projectId: id });
};
// Handle project's status button click.
const handleProjectStatus = ({ id, status_formatted }) => {
openAlert('project-status', { projectId: id, status: status_formatted });
};
// Retrieve projects table columns.
const columns = useProjectsListColumns();
// Handle cell click.
const handleCellClick = ({ row: { original } }) => {
return history.push(`/projects/${original?.id}/details`, {
@@ -72,10 +77,6 @@ function ProjectsDataTable({
projectId: project.id,
});
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PROJECTS);
// Handle view detail project.
const handleViewDetailProject = (project) => {
return history.push(`/projects/${project.id}/details`, {