mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +00:00
feat: Add Box, Group and Stack layout components.
This commit is contained in:
11
src/components/Layout/Box/Box.tsx
Normal file
11
src/components/Layout/Box/Box.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
1
src/components/Layout/Box/index.ts
Normal file
1
src/components/Layout/Box/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Box';
|
||||||
56
src/components/Layout/Group/Group.tsx
Normal file
56
src/components/Layout/Group/Group.tsx
Normal 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;
|
||||||
|
`;
|
||||||
5
src/components/Layout/Group/_utils.ts
Normal file
5
src/components/Layout/Group/_utils.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Children, ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
|
export function filterFalsyChildren(children: ReactNode) {
|
||||||
|
return (Children.toArray(children) as ReactElement[]).filter(Boolean);
|
||||||
|
}
|
||||||
1
src/components/Layout/Group/index.ts
Normal file
1
src/components/Layout/Group/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Group';
|
||||||
36
src/components/Layout/Stack/Stack.tsx
Normal file
36
src/components/Layout/Stack/Stack.tsx
Normal 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;
|
||||||
|
`;
|
||||||
1
src/components/Layout/Stack/index.ts
Normal file
1
src/components/Layout/Stack/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Stack';
|
||||||
3
src/components/Layout/index.ts
Normal file
3
src/components/Layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './Box';
|
||||||
|
export * from './Group';
|
||||||
|
export * from './Stack';
|
||||||
@@ -63,5 +63,6 @@ export * from './Indicator';
|
|||||||
export * from './EmptyStatus';
|
export * from './EmptyStatus';
|
||||||
export * from './Postbox';
|
export * from './Postbox';
|
||||||
export * from './AppToaster';
|
export * from './AppToaster';
|
||||||
|
export * from './Layout';
|
||||||
|
|
||||||
export { MODIFIER, ContextMenu, AvaterCell };
|
export { MODIFIER, ContextMenu, AvaterCell };
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ export const SidebarMenu = [
|
|||||||
overlayId: ISidebarMenuOverlayIds.Projects,
|
overlayId: ISidebarMenuOverlayIds.Projects,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'Projects Management',
|
text: 'Projects',
|
||||||
type: ISidebarMenuItemType.Group,
|
type: ISidebarMenuItemType.Group,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
FieldRequiredHint,
|
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import {
|
import {
|
||||||
ExpenseSelect,
|
ExpenseSelect,
|
||||||
@@ -100,7 +99,6 @@ const MetaLineLabel = styled.div`
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EstimatedAmountWrap = styled.div`
|
const EstimatedAmountWrap = styled.div`
|
||||||
display: block;
|
display: block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { useProjectBillableEntries } from '../../hooks';
|
import { useProjectBillableEntries } from '../../hooks';
|
||||||
import { DialogContent } from '@/components';
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,9 @@ import {
|
|||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
CustomerSelectField,
|
CustomerSelectField,
|
||||||
|
Stack,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import {
|
import { inputIntent, momentFormatter } from '@/utils';
|
||||||
inputIntent,
|
|
||||||
momentFormatter,
|
|
||||||
tansformDateValue,
|
|
||||||
handleDateChange,
|
|
||||||
} from '@/utils';
|
|
||||||
import { useProjectFormContext } from './ProjectFormProvider';
|
import { useProjectFormContext } from './ProjectFormProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,19 +35,19 @@ function ProjectFormFields() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
<Stack spacing={25}>
|
||||||
{/*------------ Contact -----------*/}
|
{/*------------ Contact -----------*/}
|
||||||
<FastField name={'contact_id'}>
|
<FastField name={'contact_id'}>
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={intl.get('projects.dialog.contact')}
|
label={intl.get('projects.dialog.contact')}
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
intent={inputIntent({ error, touched })}
|
intent={inputIntent({ error, touched })}
|
||||||
>
|
>
|
||||||
<CustomerSelectField
|
<CustomerSelectField
|
||||||
contacts={customers}
|
contacts={customers}
|
||||||
selectedContactId={value}
|
selectedContactId={value}
|
||||||
defaultSelectText={'Select Contact Account'}
|
defaultSelectText={'Find or create a customer'}
|
||||||
onContactSelected={(customer) => {
|
onContactSelected={(customer) => {
|
||||||
form.setFieldValue('contact_id', customer.id);
|
form.setFieldValue('contact_id', customer.id);
|
||||||
}}
|
}}
|
||||||
@@ -61,14 +57,16 @@ function ProjectFormFields() {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</FastField>
|
</FastField>
|
||||||
|
|
||||||
{/*------------ Project Name -----------*/}
|
{/*------------ Project Name -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('projects.dialog.project_name')}
|
label={intl.get('projects.dialog.project_name')}
|
||||||
name={'name'}
|
name={'name'}
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
>
|
>
|
||||||
<FInputGroup name="name" />
|
<FInputGroup name="name" />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
<Stack spacing={15}>
|
||||||
{/*------------ DeadLine -----------*/}
|
{/*------------ DeadLine -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('projects.dialog.deadline')}
|
label={intl.get('projects.dialog.deadline')}
|
||||||
@@ -93,22 +91,24 @@ function ProjectFormFields() {
|
|||||||
label={intl.get('projects.dialog.calculator_expenses')}
|
label={intl.get('projects.dialog.calculator_expenses')}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{/*------------ Cost Estimate -----------*/}
|
{/*------------ Cost Estimate -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'cost_estimate'}
|
name={'cost_estimate'}
|
||||||
label={intl.get('projects.dialog.cost_estimate')}
|
label={intl.get('projects.dialog.cost_estimate')}
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
>
|
>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<InputPrependText text={'USD'} />
|
|
||||||
<FMoneyInputGroup
|
<FMoneyInputGroup
|
||||||
disabled={values.published}
|
disabled={values.published}
|
||||||
name={'cost_estimate'}
|
name={'cost_estimate'}
|
||||||
allowDecimals={true}
|
allowDecimals={true}
|
||||||
allowNegativeValue={true}
|
allowNegativeValue={true}
|
||||||
/>
|
/>
|
||||||
|
<InputPrependText text={'USD'} />
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,24 +18,13 @@ function ProjectFormFloatingActions({
|
|||||||
// Formik context.
|
// Formik context.
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
// project form dialog context.
|
|
||||||
const { dialogName } = useProjectFormContext();
|
|
||||||
|
|
||||||
// Handle close button click.
|
|
||||||
const handleCancelBtnClick = () => {
|
|
||||||
closeDialog(dialogName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
|
|
||||||
<T id={'cancel'} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
style={{ minWidth: '75px' }}
|
style={{ minWidth: '100px' }}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<T id={'projects.label.create'} />
|
<T id={'projects.label.create'} />
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ export default compose(withDialogRedux())(ProjectFormDialog);
|
|||||||
const ProjectFormDialogRoot = styled(Dialog)`
|
const ProjectFormDialogRoot = styled(Dialog)`
|
||||||
.bp3-dialog-body {
|
.bp3-dialog-body {
|
||||||
.bp3-form-group {
|
.bp3-form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 0;
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
label.bp3-label {
|
label.bp3-label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
|||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Projects actions bar.
|
* Projects actions bar.
|
||||||
@@ -62,7 +63,7 @@ function ProjectsActionsBar({
|
|||||||
|
|
||||||
// Handle new project button click.
|
// Handle new project button click.
|
||||||
const handleNewProjectBtnClick = () => {
|
const handleNewProjectBtnClick = () => {
|
||||||
openDialog('project-form');
|
openDialog(DialogsName.ProjectForm);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -39,18 +39,23 @@ function ProjectsDataTable({
|
|||||||
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
|
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
|
||||||
useProjectsListContext();
|
useProjectsListContext();
|
||||||
|
|
||||||
|
// Retrieve projects table columns.
|
||||||
|
const columns = useProjectsListColumns();
|
||||||
|
|
||||||
|
// Local storage memorizing columns widths.
|
||||||
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
|
useMemorizedColumnsWidths(TABLES.PROJECTS);
|
||||||
|
|
||||||
// Handle delete project.
|
// Handle delete project.
|
||||||
const handleDeleteProject = ({ id }) => {
|
const handleDeleteProject = ({ id }) => {
|
||||||
openAlert('project-delete', { projectId: id });
|
openAlert('project-delete', { projectId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle project's status button click.
|
||||||
const handleProjectStatus = ({ id, status_formatted }) => {
|
const handleProjectStatus = ({ id, status_formatted }) => {
|
||||||
openAlert('project-status', { projectId: id, status: status_formatted });
|
openAlert('project-status', { projectId: id, status: status_formatted });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retrieve projects table columns.
|
|
||||||
const columns = useProjectsListColumns();
|
|
||||||
|
|
||||||
// Handle cell click.
|
// Handle cell click.
|
||||||
const handleCellClick = ({ row: { original } }) => {
|
const handleCellClick = ({ row: { original } }) => {
|
||||||
return history.push(`/projects/${original?.id}/details`, {
|
return history.push(`/projects/${original?.id}/details`, {
|
||||||
@@ -72,10 +77,6 @@ function ProjectsDataTable({
|
|||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// Local storage memorizing columns widths.
|
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
|
||||||
useMemorizedColumnsWidths(TABLES.PROJECTS);
|
|
||||||
|
|
||||||
// Handle view detail project.
|
// Handle view detail project.
|
||||||
const handleViewDetailProject = (project) => {
|
const handleViewDetailProject = (project) => {
|
||||||
return history.push(`/projects/${project.id}/details`, {
|
return history.push(`/projects/${project.id}/details`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user