mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
Merge branch 'develop' into print-resources
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
defaultManualJournal,
|
||||
} from './utils';
|
||||
import { JournalSyncIncrementSettingsToForm } from './components';
|
||||
import { transformAttachmentsToRequest } from '@/containers/Attachments/utils';
|
||||
|
||||
/**
|
||||
* Journal entries form.
|
||||
@@ -61,7 +62,6 @@ function MakeJournalEntriesForm({
|
||||
journalNumberPrefix,
|
||||
journalNextNumber,
|
||||
);
|
||||
|
||||
// Form initial values.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
@@ -112,6 +112,7 @@ function MakeJournalEntriesForm({
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
const form = {
|
||||
...omit(values, ['journal_number_manually']),
|
||||
...(values.journal_number_manually && {
|
||||
@@ -119,6 +120,7 @@ function MakeJournalEntriesForm({
|
||||
}),
|
||||
entries: R.compose(orderingLinesIndexes)(entries),
|
||||
publish: submitPayload.publish,
|
||||
attachments,
|
||||
};
|
||||
// Handle the request error.
|
||||
const handleError = ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { MakeJournalFormFooterLeft } from './MakeJournalFormFooterLeft';
|
||||
import { MakeJournalFormFooterRight } from './MakeJournalFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
export default function MakeJournalFormFooter() {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ export default function MakeJournalFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<MakeJournalFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AppToaster } from '@/components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useMakeJournalFormContext } from './MakeJournalProvider';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { transformAttachmentsToForm } from '@/containers/Attachments/utils';
|
||||
|
||||
const ERROR = {
|
||||
JOURNAL_NUMBER_ALREADY_EXISTS: 'JOURNAL.NUMBER.ALREADY.EXISTS',
|
||||
@@ -57,6 +58,7 @@ export const defaultManualJournal = {
|
||||
branch_id: '',
|
||||
exchange_rate: 1,
|
||||
entries: [...repeatValue(defaultEntry, DEFAULT_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
// Transform to edit form.
|
||||
@@ -76,9 +78,12 @@ export function transformToEditForm(manualJournal) {
|
||||
ensureEntriesHasEmptyLine(MIN_LINES_NUMBER, defaultEntry),
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(manualJournal);
|
||||
|
||||
return {
|
||||
...transformToForm(manualJournal, defaultManualJournal),
|
||||
entries,
|
||||
attachments,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
.popover :global .bp4-popover-content{
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.attachmentButton:not([class*=bp4-intent-]) {
|
||||
&,
|
||||
&:hover{
|
||||
background-color: #fff;
|
||||
}
|
||||
border: 1px solid rgb(206, 212, 218);
|
||||
}
|
||||
|
||||
.attachmentField :global .bp4-label{
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import clsx from 'classnames';
|
||||
import { Field, useFormikContext } from 'formik';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
} from '@blueprintjs/core';
|
||||
import { FFormGroup } from '@/components';
|
||||
import { UploadAttachmentsPopoverContent } from './UploadAttachmentsPopoverContent';
|
||||
import { transformToCamelCase, transfromToSnakeCase } from '@/utils';
|
||||
import styles from './UploadAttachmentButton.module.scss';
|
||||
|
||||
function UploadAttachmentButtonButtonContentField() {
|
||||
return (
|
||||
<Field name={'attachments'}>
|
||||
{({ form: { setFieldValue }, field: { value } }) => (
|
||||
<UploadAttachmentsPopoverContent
|
||||
value={transformToCamelCase(value)}
|
||||
onChange={(changedValue) => {
|
||||
setFieldValue('attachments', transfromToSnakeCase(changedValue));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export function UploadAttachmentButton() {
|
||||
const { values } = useFormikContext();
|
||||
const uploadedFiles = values?.attachments?.length || 0;
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'attachments'}
|
||||
label={'Attachments'}
|
||||
className={styles.attachmentField}
|
||||
fastField={true}
|
||||
>
|
||||
<Popover
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
popoverClassName={clsx(styles.popover, Classes.POPOVER_CONTENT_SIZING)}
|
||||
placement={'top-start'}
|
||||
content={<UploadAttachmentButtonButtonContentField />}
|
||||
>
|
||||
<Button className={styles.attachmentButton}>
|
||||
{uploadedFiles > 0 ? (
|
||||
<>Upload attachments ({uploadedFiles})</>
|
||||
) : (
|
||||
<>Upload attachments</>
|
||||
)}
|
||||
</Button>
|
||||
</Popover>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
.content {
|
||||
}
|
||||
|
||||
.hintText{
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
color: #738091;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.attachments{
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.attachmentItem{
|
||||
border-top: 1px solid #D3D8DE;
|
||||
border-left: 1px solid #D3D8DE;
|
||||
border-right: 1px solid #D3D8DE;
|
||||
padding: 10px 14px;
|
||||
justify-content: space-between;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
&:last-child{
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-bottom: 1px solid #D3D8DE;
|
||||
}
|
||||
}
|
||||
|
||||
.attachmentFilenameText{
|
||||
|
||||
}
|
||||
|
||||
.attachmentSizeText,
|
||||
.attachmentLoadingText{
|
||||
font-size: 13px;
|
||||
color: #738091;
|
||||
}
|
||||
|
||||
.attachmentContent{
|
||||
}
|
||||
|
||||
.attachmentIcon{
|
||||
color: #626b7c;
|
||||
}
|
||||
|
||||
.label{
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dropzoneRoot{
|
||||
min-height: 140px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.attachmentIconWrap{
|
||||
width: 20PX;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// @ts-nocheck
|
||||
import { useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Button, Intent, Text, Spinner } from '@blueprintjs/core';
|
||||
import { Box, Group, Icon, Stack } from '@/components';
|
||||
import {
|
||||
ImportDropzoneField,
|
||||
ImportDropzoneFieldProps,
|
||||
} from '@/containers/Import/ImportDropzoneFile';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import {
|
||||
useGetPresignedUrlAttachment,
|
||||
useUploadAttachments,
|
||||
} from '@/hooks/query/attachments';
|
||||
import styles from './UploadAttachmentPopoverContent.module.scss';
|
||||
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
|
||||
import { formatBytes } from '@/utils/format-bytes';
|
||||
|
||||
interface AttachmentFileCommon {
|
||||
originName: string;
|
||||
key: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
}
|
||||
interface AttachmentFileLoaded extends AttachmentFileCommon {}
|
||||
interface AttachmentFileLoading extends AttachmentFileCommon {
|
||||
loading: boolean;
|
||||
}
|
||||
type AttachmentFile = AttachmentFileLoaded | AttachmentFileLoading;
|
||||
|
||||
interface UploadAttachmentsPopoverContentProps {
|
||||
initialValue?: AttachmentFile[];
|
||||
value?: AttachmentFile[];
|
||||
onChange?: (value: AttachmentFile[]) => void;
|
||||
onUploadedChange?: (value: AttachmentFile[]) => void;
|
||||
dropzoneFieldProps?: ImportDropzoneFieldProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads and list the attachments with ability to delete particular attachment.
|
||||
* @param {UploadAttachmentsPopoverContentProps}
|
||||
*/
|
||||
export function UploadAttachmentsPopoverContent({
|
||||
initialValue,
|
||||
value,
|
||||
onChange,
|
||||
onUploadedChange,
|
||||
dropzoneFieldProps,
|
||||
}: UploadAttachmentsPopoverContentProps) {
|
||||
// Controlled/uncontrolled value state.
|
||||
const [localFiles, handleFilesChange] = useUncontrolled<AttachmentFile[]>({
|
||||
finalValue: [],
|
||||
initialValue,
|
||||
value,
|
||||
onChange: onChange,
|
||||
});
|
||||
// Stops loading of the given attachment key and updates it to new key,
|
||||
// that came from the server-side after uploading is done.
|
||||
const stopLoadingAttachment = (
|
||||
localFiles: AttachmentFile[],
|
||||
internalKey: string,
|
||||
newKey: string,
|
||||
) => {
|
||||
return localFiles.map((localFile) => {
|
||||
if (localFile.key === internalKey) {
|
||||
return {
|
||||
...localFile,
|
||||
key: newKey,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
return localFile;
|
||||
});
|
||||
};
|
||||
// Uploads the attachments.
|
||||
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
||||
onSuccess: (data) => {
|
||||
const newLocalFiles = stopLoadingAttachment(
|
||||
localFiles,
|
||||
data.config.data.get('internalKey'),
|
||||
data.data.data.key,
|
||||
);
|
||||
handleFilesChange(newLocalFiles);
|
||||
onUploadedChange && onUploadedChange(newLocalFiles);
|
||||
},
|
||||
});
|
||||
// Deletes the attachment of the given file key.
|
||||
const handleClick = (key: string) => () => {
|
||||
const updatedFiles = localFiles.filter((file, i) => file.key !== key);
|
||||
handleFilesChange(updatedFiles);
|
||||
onUploadedChange && onUploadedChange(updatedFiles);
|
||||
};
|
||||
|
||||
// Handle change dropzone.
|
||||
const handleChangeDropzone = (file: File) => {
|
||||
const formData = new FormData();
|
||||
const key = Date.now().toString();
|
||||
|
||||
formData.append('file', file);
|
||||
formData.append('internalKey', key);
|
||||
|
||||
handleFilesChange([
|
||||
{
|
||||
originName: file.name,
|
||||
size: file.size,
|
||||
key,
|
||||
loading: true,
|
||||
},
|
||||
...localFiles,
|
||||
]);
|
||||
uploadAttachments(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<Text className={styles.label}>Attach documents</Text>
|
||||
<Stack spacing={0}>
|
||||
<ImportDropzoneField
|
||||
uploadIcon={null}
|
||||
value={null}
|
||||
title={''}
|
||||
classNames={{ root: styles.dropzoneRoot }}
|
||||
onChange={handleChangeDropzone}
|
||||
dropzoneProps={{
|
||||
accept: [
|
||||
MIME_TYPES.doc,
|
||||
MIME_TYPES.docx,
|
||||
MIME_TYPES.pdf,
|
||||
MIME_TYPES.png,
|
||||
MIME_TYPES.jpeg,
|
||||
],
|
||||
}}
|
||||
{...dropzoneFieldProps}
|
||||
/>
|
||||
<Group className={styles.hintText}>
|
||||
<Box>Maximum: 25MB</Box>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{!isEmpty(localFiles) && (
|
||||
<Stack spacing={0} className={styles.attachments}>
|
||||
{localFiles.map((localFile: AttachmentFile, index: number) => (
|
||||
<Group
|
||||
position={'space-between'}
|
||||
className={styles.attachmentItem}
|
||||
key={index}
|
||||
>
|
||||
<Group spacing={14} className={styles.attachmentContent}>
|
||||
<div className={styles.attachmentIconWrap}>
|
||||
{localFile.loading ? (
|
||||
<Spinner size={20} />
|
||||
) : (
|
||||
<Icon
|
||||
icon={'media'}
|
||||
iconSize={16}
|
||||
className={styles.attachmentIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Stack spacing={2}>
|
||||
<Text className={styles.attachmentFilenameText}>
|
||||
{localFile.originName}
|
||||
</Text>
|
||||
{localFile.loading ? (
|
||||
<Text className={styles.attachmentLoadingText}>
|
||||
Loading...
|
||||
</Text>
|
||||
) : (
|
||||
<Text className={styles.attachmentSizeText}>
|
||||
{formatBytes(localFile.size)}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{!localFile.loading && (
|
||||
<Group spacing={2}>
|
||||
<ViewButton fileKey={localFile.key} />
|
||||
<Button
|
||||
small
|
||||
minimal
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleClick(localFile.key)}
|
||||
>
|
||||
<Icon icon={'trash-16'} iconSize={16} />
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ViewButton = ({ fileKey }: { fileKey: string }) => {
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const { mutateAsync: getAttachmentPresignedUrl } =
|
||||
useGetPresignedUrlAttachment();
|
||||
|
||||
const handleViewBtnClick = (key: string) => () => {
|
||||
setLoading(true);
|
||||
|
||||
getAttachmentPresignedUrl(key).then((data) => {
|
||||
window.open(data.presigned_url);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
small
|
||||
minimal
|
||||
onClick={handleViewBtnClick(fileKey)}
|
||||
disabled={isLoading}
|
||||
intent={Intent.PRIMARY}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
19
packages/webapp/src/containers/Attachments/utils.ts
Normal file
19
packages/webapp/src/containers/Attachments/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// @ts-nocheck
|
||||
import { transformToForm } from '@/utils';
|
||||
|
||||
const attachmentReqSchema = {
|
||||
key: '',
|
||||
size: '',
|
||||
origin_name: '',
|
||||
mime_type: '',
|
||||
};
|
||||
|
||||
export const transformAttachmentsToForm = (values) => {
|
||||
return values.attachments?.map((attachment) =>
|
||||
transformToForm(attachment, attachmentReqSchema),
|
||||
);
|
||||
};
|
||||
|
||||
export const transformAttachmentsToRequest = (values) => {
|
||||
return values.attachments?.map((attachment) => ({ key: attachment.key }));
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { ExpenseFormFooterLeft } from './ExpenseFormFooterLeft';
|
||||
import { ExpenseFormFooterRight } from './ExpenseFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
export default function ExpenseFormFooter() {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ export default function ExpenseFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<ExpenseFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
formattedAmount,
|
||||
} from '@/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
const ERROR = {
|
||||
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
|
||||
@@ -46,6 +50,7 @@ export const defaultExpense = {
|
||||
branch_id: '',
|
||||
exchange_rate: 1,
|
||||
categories: [...repeatValue(defaultExpenseEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -93,9 +98,12 @@ export const transformToEditForm = (
|
||||
ensureEntriesHasEmptyLine(MIN_LINES_NUMBER, expenseEntry),
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(expense);
|
||||
|
||||
return {
|
||||
...transformToForm(expense, defaultExpense),
|
||||
categories,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,10 +141,12 @@ export const filterNonZeroEntries = (categories) => {
|
||||
*/
|
||||
export const transformFormValuesToRequest = (values) => {
|
||||
const categories = filterNonZeroEntries(values.categories);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...values,
|
||||
categories: R.compose(orderingLinesIndexes)(categories),
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import { useRef } from 'react';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { Box, Icon, Stack } from '@/components';
|
||||
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
||||
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
|
||||
import styles from './ImportDropzone.module.css';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import styles from './ImportDropzone.module.css';
|
||||
|
||||
interface ImportDropzoneFieldProps {
|
||||
export interface ImportDropzoneFieldProps {
|
||||
initialValue?: File;
|
||||
value?: File;
|
||||
onChange?: (file: File) => void;
|
||||
dropzoneProps?: DropzoneProps;
|
||||
uploadIcon?: JSX.Element;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
classNames?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function ImportDropzoneField({
|
||||
@@ -19,6 +24,10 @@ export function ImportDropzoneField({
|
||||
value,
|
||||
onChange,
|
||||
dropzoneProps,
|
||||
uploadIcon = <Icon icon="download" iconSize={26} />,
|
||||
title = 'Drag images here or click to select files',
|
||||
subtitle = 'Drag and Drop file here or Choose file',
|
||||
classNames,
|
||||
}: ImportDropzoneFieldProps) {
|
||||
const [localValue, handleChange] = useUncontrolled({
|
||||
value,
|
||||
@@ -38,15 +47,18 @@ export function ImportDropzoneField({
|
||||
onReject={(files) => console.log('rejected files', files)}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={[MIME_TYPES.csv, MIME_TYPES.xls, MIME_TYPES.xlsx]}
|
||||
classNames={{ content: styles.dropzoneContent }}
|
||||
classNames={{ root: classNames?.root, content: styles.dropzoneContent }}
|
||||
activateOnClick={false}
|
||||
openRef={openRef}
|
||||
{...dropzoneProps}
|
||||
>
|
||||
<Stack spacing={12} align="center" className={styles.content}>
|
||||
<Box className={styles.iconWrap}>
|
||||
<Icon icon="download" iconSize={26} />
|
||||
</Box>
|
||||
<Stack
|
||||
spacing={12}
|
||||
align="center"
|
||||
className={clsx(styles.content, classNames?.content)}
|
||||
>
|
||||
{uploadIcon && <Box className={styles.iconWrap}>{uploadIcon}</Box>}
|
||||
|
||||
{localValue ? (
|
||||
<Stack spacing={6} justify="center" align="center">
|
||||
<h4 className={styles.title}>{localValue.name}</h4>
|
||||
@@ -56,15 +68,10 @@ export function ImportDropzoneField({
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={4} align="center">
|
||||
<h4 className={styles.title}>
|
||||
Drag images here or click to select files
|
||||
</h4>
|
||||
<span className={styles.subtitle}>
|
||||
Drag and Drop file here or Choose file
|
||||
</span>
|
||||
{title && <h4 className={styles.title}>{title}</h4>}
|
||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Button
|
||||
intent="none"
|
||||
onClick={() => openRef.current?.()}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Paper, Row, Col } from '@/components';
|
||||
import { BillFormFooterLeft } from './BillFormFooterLeft';
|
||||
import { BillFormFooterRight } from './BillFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
// Bill form floating actions.
|
||||
export default function BillFormFooter() {
|
||||
@@ -16,6 +17,7 @@ export default function BillFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<BillFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -60,6 +64,7 @@ export const defaultBill = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultBillEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
export const ERRORS = {
|
||||
@@ -88,12 +93,15 @@ export const transformToEditForm = (bill) => {
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(bill);
|
||||
|
||||
return {
|
||||
...transformToForm(bill, defaultBill),
|
||||
inclusive_exclusive_tax: bill.is_inclusive_tax
|
||||
? TaxType.Inclusive
|
||||
: TaxType.Exclusive,
|
||||
entries,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -120,11 +128,13 @@ export const filterNonZeroEntries = (entries) => {
|
||||
*/
|
||||
export const transformFormValuesToRequest = (values) => {
|
||||
const entries = filterNonZeroEntries(values.entries);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...values,
|
||||
entries: transformEntriesToSubmit(entries),
|
||||
open: false,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { VendorCreditNoteFormFooterLeft } from './VendorCreditNoteFormFooterLeft';
|
||||
import { VendorCreditNoteFormFooterRight } from './VendorCreditNoteFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
/**
|
||||
* Vendor Credit note form footer.
|
||||
@@ -18,6 +19,7 @@ export default function VendorCreditNoteFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<VendorCreditNoteFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -20,6 +20,10 @@ import { useFormikContext } from 'formik';
|
||||
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -48,6 +52,7 @@ export const defaultVendorsCreditNote = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,9 +73,12 @@ export const transformToEditForm = (creditNote) => {
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(creditNote);
|
||||
|
||||
return {
|
||||
...transformToForm(creditNote, defaultVendorsCreditNote),
|
||||
entries,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -100,11 +108,13 @@ export const filterNonZeroEntries = (entries) => {
|
||||
*/
|
||||
export const transformFormValuesToRequest = (values) => {
|
||||
const entries = filterNonZeroEntries(values.entries);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...values,
|
||||
entries: transformEntriesToSubmit(entries),
|
||||
open: false,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { PaymentMadeFormFooterLeft } from './PaymentMadeFormFooterLeft';
|
||||
import { PaymentMadeFormFooterRight } from './PaymentMadeFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
/**
|
||||
* Payment made form footer.
|
||||
@@ -18,6 +19,7 @@ export default function PaymentMadeFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<PaymentMadeFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
} from '@/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { PAYMENT_MADE_ERRORS } from '../constants';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const ERRORS = {
|
||||
PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE',
|
||||
@@ -44,9 +48,12 @@ export const defaultPaymentMade = {
|
||||
branch_id: '',
|
||||
exchange_rate: 1,
|
||||
entries: [],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
|
||||
const attachments = transformAttachmentsToForm(paymentMade);
|
||||
|
||||
return {
|
||||
...transformToForm(paymentMade, defaultPaymentMade),
|
||||
full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'),
|
||||
@@ -56,6 +63,7 @@ export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
|
||||
payment_amount: paymentMadeEntry.payment_amount || '',
|
||||
})),
|
||||
],
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -101,7 +109,9 @@ export const transformFormToRequest = (form) => {
|
||||
...pick(entry, ['payment_amount', 'bill_id']),
|
||||
}));
|
||||
|
||||
return { ...form, entries: orderingLinesIndexes(entries) };
|
||||
const attachments = transformAttachmentsToRequest(form);
|
||||
|
||||
return { ...form, entries: orderingLinesIndexes(entries), attachments };
|
||||
};
|
||||
|
||||
export const useSetPrimaryBranchToForm = () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { CreditNoteFormFooterLeft } from './CreditNoteFormFooterLeft';
|
||||
import { CreditNoteFormFooterRight } from './CreditNoteFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
/**
|
||||
* Credit note form footer.
|
||||
@@ -18,6 +19,7 @@ export default function CreditNoteFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<CreditNoteFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
defaultFastFieldShouldUpdate,
|
||||
transformToForm,
|
||||
repeatValue,
|
||||
transactionNumber,
|
||||
formattedAmount,
|
||||
orderingLinesIndexes,
|
||||
} from '@/utils';
|
||||
@@ -21,6 +20,10 @@ import {
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -51,6 +54,7 @@ export const defaultCreditNote = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
|
||||
attachments: []
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,9 +75,12 @@ export function transformToEditForm(creditNote) {
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
const attachment = transformAttachmentsToForm(creditNote);
|
||||
|
||||
return {
|
||||
...transformToForm(creditNote, defaultCreditNote),
|
||||
entries,
|
||||
attachment,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,11 +110,13 @@ export const filterNonZeroEntries = (entries) => {
|
||||
*/
|
||||
export const transformFormValuesToRequest = (values) => {
|
||||
const entries = filterNonZeroEntries(values.entries);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...values,
|
||||
entries: transformEntriesToSubmit(entries),
|
||||
open: false,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { EstimateFormFooterLeft } from './EstimateFormFooterLeft';
|
||||
import { EstimateFormFooterRight } from './EstimateFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
/**
|
||||
* Estimate form footer.
|
||||
@@ -18,6 +19,7 @@ export default function EstiamteFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<EstimateFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -56,6 +60,7 @@ export const defaultEstimate = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)],
|
||||
attachments: []
|
||||
};
|
||||
|
||||
const ERRORS = {
|
||||
@@ -78,9 +83,12 @@ export const transformToEditForm = (estimate) => {
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(estimate);
|
||||
|
||||
return {
|
||||
...transformToForm(estimate, defaultEstimate),
|
||||
entries,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -150,6 +158,8 @@ export const transfromsFormValuesToRequest = (values) => {
|
||||
const entries = values.entries.filter(
|
||||
(item) => item.item_id && item.quantity,
|
||||
);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...omit(values, ['estimate_number_manually', 'estimate_number']),
|
||||
// The `estimate_number_manually` will be presented just if the auto-increment
|
||||
@@ -160,6 +170,7 @@ export const transfromsFormValuesToRequest = (values) => {
|
||||
entries: entries.map((entry) => ({
|
||||
...transformToForm(entry, defaultEstimateEntryReq),
|
||||
})),
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Paper, Row, Col } from '@/components';
|
||||
import { InvoiceFormFooterLeft } from './InvoiceFormFooterLeft';
|
||||
import { InvoiceFormFooterRight } from './InvoiceFormFooterRight';
|
||||
import { UploadAttachmentButton } from '../../../Attachments/UploadAttachmentButton';
|
||||
|
||||
export default function InvoiceFormFooter() {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ export default function InvoiceFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<InvoiceFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import intl from 'react-intl-universal';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { omit, first, sumBy, round } from 'lodash';
|
||||
import { omit, first, sumBy } from 'lodash';
|
||||
import {
|
||||
compose,
|
||||
transformToForm,
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
ensureEntriesHaveEmptyLine,
|
||||
} from '@/containers/Entries/utils';
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -63,6 +67,7 @@ export const defaultInvoice = {
|
||||
warehouse_id: '',
|
||||
project_id: '',
|
||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,6 +94,7 @@ export function transformToEditForm(invoice) {
|
||||
? TaxType.Inclusive
|
||||
: TaxType.Exclusive,
|
||||
entries,
|
||||
attachments: transformAttachmentsToForm(invoice),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,6 +198,7 @@ export function transformValueToRequest(values) {
|
||||
...omit(entry, ['amount', 'tax_amount', 'tax_rate']),
|
||||
})),
|
||||
delivered: false,
|
||||
attachments: transformAttachmentsToRequest(values),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { PaymentReceiveFormFootetLeft } from './PaymentReceiveFormFootetLeft';
|
||||
import { PaymentReceiveFormFootetRight } from './PaymentReceiveFormFootetRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
/**
|
||||
* Payment receive form footer.
|
||||
@@ -18,6 +19,7 @@ export default function PaymentReceiveFormFooter() {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<PaymentReceiveFormFootetLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -9,13 +9,16 @@ import { AppToaster } from '@/components';
|
||||
import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider';
|
||||
import {
|
||||
defaultFastFieldShouldUpdate,
|
||||
transactionNumber,
|
||||
transformToForm,
|
||||
safeSumBy,
|
||||
orderingLinesIndexes,
|
||||
formattedAmount,
|
||||
} from '@/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
// Default payment receive entry.
|
||||
export const defaultPaymentReceiveEntry = {
|
||||
@@ -39,11 +42,12 @@ export const defaultPaymentReceive = {
|
||||
// Holds the payment number that entered manually only.
|
||||
payment_receive_no_manually: '',
|
||||
statement: '',
|
||||
full_amount: '',
|
||||
full_amount: '',
|
||||
currency_code: '',
|
||||
branch_id: '',
|
||||
exchange_rate: 1,
|
||||
entries: [],
|
||||
attachments: []
|
||||
};
|
||||
|
||||
export const defaultRequestPaymentEntry = {
|
||||
@@ -74,6 +78,7 @@ export const transformToEditForm = (paymentReceive, paymentReceiveEntries) => ({
|
||||
payment_amount: paymentReceiveEntry.payment_amount || '',
|
||||
})),
|
||||
],
|
||||
attachments: transformAttachmentsToForm(paymentReceive),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -155,6 +160,8 @@ export const transformFormToRequest = (form) => {
|
||||
...pick(entry, Object.keys(defaultRequestPaymentEntry)),
|
||||
}));
|
||||
|
||||
const attachments = transformAttachmentsToRequest(form);
|
||||
|
||||
return {
|
||||
...omit(form, ['payment_receive_no_manually', 'payment_receive_no']),
|
||||
// The `payment_receive_no_manually` will be presented just if the auto-increment
|
||||
@@ -163,6 +170,7 @@ export const transformFormToRequest = (form) => {
|
||||
payment_receive_no: form.payment_receive_no,
|
||||
}),
|
||||
entries: orderingLinesIndexes(entries),
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CLASSES } from '@/constants/classes';
|
||||
import { Paper, Row, Col } from '@/components';
|
||||
import { ReceiptFormFooterLeft } from './ReceiptFormFooterLeft';
|
||||
import { ReceiptFormFooterRight } from './ReceiptFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
|
||||
export default function ReceiptFormFooter({}) {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ export default function ReceiptFormFooter({}) {
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<ReceiptFormFooterLeft />
|
||||
<UploadAttachmentButton />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -56,6 +60,7 @@ export const defaultReceipt = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultReceiptEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
const ERRORS = {
|
||||
@@ -81,9 +86,12 @@ export const transformToEditForm = (receipt) => {
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
const attachments = transformAttachmentsToForm(receipt);
|
||||
|
||||
return {
|
||||
...transformToForm(receipt, defaultReceipt),
|
||||
entries,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -142,6 +150,7 @@ export const transformFormValuesToRequest = (values) => {
|
||||
const entries = values.entries.filter(
|
||||
(item) => item.item_id && item.quantity,
|
||||
);
|
||||
const attachments = transformAttachmentsToRequest(values);
|
||||
|
||||
return {
|
||||
...omit(values, ['receipt_number_manually', 'receipt_number']),
|
||||
@@ -152,6 +161,7 @@ export const transformFormValuesToRequest = (values) => {
|
||||
...transformToForm(entry, defaultReceiptEntryReq),
|
||||
})),
|
||||
closed: false,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
42
packages/webapp/src/hooks/query/attachments.ts
Normal file
42
packages/webapp/src/hooks/query/attachments.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// @ts-nocheck
|
||||
import { useMutation } from 'react-query';
|
||||
import useApiRequest from '../useRequest';
|
||||
|
||||
/**
|
||||
* Uploads the given attachments.
|
||||
*/
|
||||
export function useUploadAttachments(props) {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
(values) => apiRequest.post('/attachments', values),
|
||||
props,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given attachment key.
|
||||
*/
|
||||
export function useDeleteAttachment(props) {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
(key: string) => apiRequest.delete(`/attachments/${key}`),
|
||||
props,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given attachments.
|
||||
*/
|
||||
export function useGetPresignedUrlAttachment(props) {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
(key: string) =>
|
||||
apiRequest
|
||||
.get(`/attachments/${key}/presigned-url`)
|
||||
.then((res) => res.data),
|
||||
props,
|
||||
);
|
||||
}
|
||||
@@ -578,26 +578,30 @@ export default {
|
||||
viewBox: '0 0 20 20',
|
||||
},
|
||||
done: {
|
||||
path: [
|
||||
'M395-285 226-455l50-50 119 118 289-288 50 51-339 339Z',
|
||||
],
|
||||
viewBox: '0 -960 960 960'
|
||||
path: ['M395-285 226-455l50-50 119 118 289-288 50 51-339 339Z'],
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
download: {
|
||||
path: [
|
||||
'M480-336 288-528l51-51 105 105v-342h72v342l105-105 51 51-192 192ZM263.717-192Q234-192 213-213.15T192-264v-72h72v72h432v-72h72v72q0 29.7-21.162 50.85Q725.676-192 695.96-192H263.717Z'
|
||||
'M480-336 288-528l51-51 105 105v-342h72v342l105-105 51 51-192 192ZM263.717-192Q234-192 213-213.15T192-264v-72h72v72h432v-72h72v72q0 29.7-21.162 50.85Q725.676-192 695.96-192H263.717Z',
|
||||
],
|
||||
viewBox: '0 -960 960 960'
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
'chevron-up': {
|
||||
path: [
|
||||
'M12.71,9.29l-4-4C8.53,5.11,8.28,5,8,5S7.47,5.11,7.29,5.29l-4,4C3.11,9.47,3,9.72,3,10c0,0.55,0.45,1,1,1c0.28,0,0.53-0.11,0.71-0.29L8,7.41l3.29,3.29C11.47,10.89,11.72,11,12,11c0.55,0,1-0.45,1-1C13,9.72,12.89,9.47,12.71,9.29'
|
||||
'M12.71,9.29l-4-4C8.53,5.11,8.28,5,8,5S7.47,5.11,7.29,5.29l-4,4C3.11,9.47,3,9.72,3,10c0,0.55,0.45,1,1,1c0.28,0,0.53-0.11,0.71-0.29L8,7.41l3.29,3.29C11.47,10.89,11.72,11,12,11c0.55,0,1-0.45,1-1C13,9.72,12.89,9.47,12.71,9.29',
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
'chevron-down': {
|
||||
path: [
|
||||
'M12,5c-0.28,0-0.53,0.11-0.71,0.29L8,8.59L4.71,5.29C4.53,5.11,4.28,5,4,5C3.45,5,3,5.45,3,6c0,0.28,0.11,0.53,0.29,0.71l4,4C7.47,10.89,7.72,11,8,11s0.53-0.11,0.71-0.29l4-4C12.89,6.53,13,6.28,13,6C13,5.45,12.55,5,12,5z'
|
||||
'M12,5c-0.28,0-0.53,0.11-0.71,0.29L8,8.59L4.71,5.29C4.53,5.11,4.28,5,4,5C3.45,5,3,5.45,3,6c0,0.28,0.11,0.53,0.29,0.71l4,4C7.47,10.89,7.72,11,8,11s0.53-0.11,0.71-0.29l4-4C12.89,6.53,13,6.28,13,6C13,5.45,12.55,5,12,5z',
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
media: {
|
||||
path: [
|
||||
'M11.99,6.99c0.55,0,1-0.45,1-1s-0.45-1-1-1s-1,0.45-1,1S11.44,6.99,11.99,6.99zM14.99,1.99h-14c-0.55,0-1,0.45-1,1v10c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1v-10C15.99,2.44,15.54,1.99,14.99,1.99zM13.99,10.99l-5-3l-1,2l-3-4l-3,5v-7h12V10.99z',
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
|
||||
28
packages/webapp/src/utils/format-bytes.ts
Normal file
28
packages/webapp/src/utils/format-bytes.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Converts a number of bytes into a human-readable string with units.
|
||||
* The function takes the number of bytes and an optional number of decimal places,
|
||||
* then calculates the appropriate unit (Bytes, KB, MB, etc.) and formats the number.
|
||||
* @param {number} bytes - The number of bytes to format.
|
||||
* @param {number} decimals - The number of decimal places to include in the formatted string.
|
||||
* @returns {string} The formatted string with the appropriate unit.
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals: number = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k: number = 1024;
|
||||
const dm: number = decimals < 0 ? 0 : decimals;
|
||||
const sizes: string[] = [
|
||||
'Bytes',
|
||||
'KB',
|
||||
'MB',
|
||||
'GB',
|
||||
'TB',
|
||||
'PB',
|
||||
'EB',
|
||||
'ZB',
|
||||
'YB',
|
||||
];
|
||||
const i: number = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
Reference in New Issue
Block a user