From a6c9a3d3d62f62edac149ea89c30edf8dd4c4c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 27 Aug 2024 11:51:06 +0200 Subject: [PATCH 01/14] update eslint --- .storybook/components/Theme.tsx | 4 +-- .storybook/main.ts | 2 +- .storybook/preview.tsx | 2 +- package-lock.json | 8 ++--- package.json | 2 +- src/components/action/Actions.stories.tsx | 2 +- src/components/alert/Alert.stories.tsx | 10 +++--- src/components/alert/ConvenientAlerts.tsx | 2 +- .../breadcrumbs/BreadcrumbLoading.tsx | 2 +- .../breadcrumbs/Breadcrumbs.stories.tsx | 4 +-- src/components/button/Button.stories.tsx | 10 +++--- src/components/button/ButtonIcon.stories.tsx | 10 +++--- src/components/button/ButtonIcon.tsx | 2 +- .../button/ButtonIconLink.stories.tsx | 8 ++--- src/components/button/ButtonIconLink.tsx | 2 +- src/components/button/ButtonLink.stories.tsx | 4 +-- src/components/button/ButtonLink.tsx | 4 +-- .../button/SubmitButton.stories.tsx | 4 +-- .../ContentMessage/ContentMessage.stories.tsx | 2 +- .../DescriptionItem/DescriptionItem.tsx | 2 +- .../dialog/Dialog/Dialog.stories.tsx | 4 +-- .../DialogItem/DialogListItemButton.tsx | 2 +- src/components/form/InputField.stories.tsx | 6 ++-- src/components/form/SearchField.stories.tsx | 2 +- src/components/form/SelectField.stories.tsx | 2 +- .../form/SelectMonthField.stories.tsx | 2 +- src/components/form/SelectMonthField.tsx | 2 +- src/components/form/SelectYearField.tsx | 2 +- .../primitive/Checkbox/Checkbox.stories.tsx | 2 +- .../form/primitive/Checkbox/Checkbox.tsx | 2 +- .../form/primitive/Input.stories.tsx | 4 +-- .../primitive/InputIcon/InputIcon.stories.tsx | 4 +-- src/components/form/primitive/Radio/Radio.tsx | 4 +-- .../form/primitive/Select.stories.tsx | 14 ++++---- .../primitive/ToggleSwitch/ToggleSwitch.tsx | 2 +- src/components/link/TextLink.tsx | 2 +- src/components/menu/Menu.stories.tsx | 4 +-- src/components/menu/Menu.tsx | 8 ++--- src/components/menu/MenuItem.stories.tsx | 4 +-- src/components/menu/MenuItem.tsx | 4 +-- .../pagination/PaginationInMemory.tsx | 2 +- .../PaginationPreviousNextContent.tsx | 6 ++-- .../pagination/PaginationRouter.tsx | 12 +++---- src/components/react-hook-form/AutoSubmit.tsx | 2 +- .../CheckboxFormField.stories.tsx | 12 +++---- .../react-hook-form/DateFormField.stories.tsx | 6 ++-- .../FieldSetFormField.stories.tsx | 2 +- .../InputFormField.stories.tsx | 14 ++++---- .../NumberFormField.stories.tsx | 16 +++++----- .../SearchFormField.stories.tsx | 6 ++-- .../react-hook-form/SelectFormField.tsx | 6 ++-- .../SelectItemFormField.stories.tsx | 4 +-- .../SelectItemFormFieldDialog.tsx | 6 ++-- .../SelectItemFormFieldInput.tsx | 2 +- .../TextAreaFormField.stories.tsx | 12 +++---- .../ToggleSwitchFormField.stories.tsx | 12 +++---- .../__test__/useHandleSubmit.test.ts | 3 +- .../react-hook-form/useHandleSubmit.ts | 6 ++-- .../ConvenientSectionContentMessage.tsx | 2 +- .../Section/SectionAppendix.stories.tsx | 6 ++-- .../SectionFooterArea.stories.tsx | 4 +-- ...onFooterWithPaginationInMemory.stories.tsx | 8 ++--- ...tionFooterWithPaginationRouter.stories.tsx | 6 ++-- .../SectionHeader/SectionHeaderTitle.tsx | 2 +- .../section/SectionItem/SectionListItem.tsx | 2 +- src/components/tabs/TabsContext.ts | 2 +- .../util/__test__/useHandleRequest.test.ts | 2 +- src/components/util/useForwardedRef.ts | 3 +- src/components/util/useHandleRequest.ts | 2 +- src/components/util/useId.ts | 1 - src/examples/Form.stories.tsx | 32 +++++++++---------- src/examples/List.stories.tsx | 16 +++++----- src/examples/Menu.stories.tsx | 8 ++--- src/framework/ReactUIProvider.tsx | 6 ++-- src/framework/router/LinkComponentContext.tsx | 2 +- tsconfig.json | 4 +-- vite.config.ts | 2 +- 77 files changed, 201 insertions(+), 200 deletions(-) diff --git a/.storybook/components/Theme.tsx b/.storybook/components/Theme.tsx index 4fee0bec..d589c265 100644 --- a/.storybook/components/Theme.tsx +++ b/.storybook/components/Theme.tsx @@ -1,8 +1,8 @@ import { Markdown, Subheading } from '@storybook/addon-docs' import { ReactElement } from 'react' import { - defaultTheme, Theme as ThemeType, + defaultTheme, } from '../../src/framework/theme/theme' import { SourceJson } from './SourceJson' @@ -11,7 +11,7 @@ export function Theme({ items, }: { component: T - items?: Array + items?: R[] }): ReactElement { const theme = { [component]: items diff --git a/.storybook/main.ts b/.storybook/main.ts index 7576dc92..a8212d59 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,5 @@ -import type { StorybookConfig } from '@storybook/react-vite' import remarkGfm from 'remark-gfm' +import type { StorybookConfig } from '@storybook/react-vite' const config: StorybookConfig = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 3bab3f51..62d4d5ec 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,7 +1,7 @@ import { Preview } from '@storybook/react' import { ReactUIProvider, defaultTheme, makeLinkComponent } from '../src' -import { Background } from './types' import '../styles/index.css' +import { Background } from './types' const LinkComponent = makeLinkComponent( ({ children, _internal, href, ...props }, ref) => ( diff --git a/package-lock.json b/package-lock.json index c45e461b..e9909faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "react-hook-form": "^7.51.2" }, "devDependencies": { - "@aboutbits/eslint-config": "^2.2.4", + "@aboutbits/eslint-config": "^3.0.0", "@aboutbits/prettier-config": "^1.6.1", "@aboutbits/react-pagination": "^3.0.12", "@aboutbits/ts-config": "^1.1.3", @@ -94,9 +94,9 @@ } }, "node_modules/@aboutbits/eslint-config": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@aboutbits/eslint-config/-/eslint-config-2.2.4.tgz", - "integrity": "sha512-JyF+oggMQyMlhZqZL95+mG5ct2IGfS13utb6oEA+i1iUiIJTTXKoGcUfjwpvD6w881b+JZ1Nc7vv1tqZKy7i/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aboutbits/eslint-config/-/eslint-config-3.0.0.tgz", + "integrity": "sha512-GLgjA752rVnnK4io2qBs9nZcIC/RkvjihV9UKc2JZeZ94XfXBM6TGtqCxqNojNXmpVpdkZrkZ8mPVjD+08rHBg==", "dev": true, "dependencies": { "@typescript-eslint/parser": "^6.1.0 || ^7.3.1", diff --git a/package.json b/package.json index bf8da3f7..d6541748 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "react-hook-form": "^7.51.2" }, "devDependencies": { - "@aboutbits/eslint-config": "^2.2.4", + "@aboutbits/eslint-config": "^3.0.0", "@aboutbits/prettier-config": "^1.6.1", "@aboutbits/react-pagination": "^3.0.12", "@aboutbits/ts-config": "^1.1.3", diff --git a/src/components/action/Actions.stories.tsx b/src/components/action/Actions.stories.tsx index 6e4cd9b4..a0bb9507 100644 --- a/src/components/action/Actions.stories.tsx +++ b/src/components/action/Actions.stories.tsx @@ -7,9 +7,9 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' +import { Theme } from '../../../.storybook/components' import { Button, ButtonVariant } from '../button' import { Tone } from '../types' -import { Theme } from '../../../.storybook/components' import { Actions } from './Actions' const children = { diff --git a/src/components/alert/Alert.stories.tsx b/src/components/alert/Alert.stories.tsx index eba7c4cc..dd65dfb5 100644 --- a/src/components/alert/Alert.stories.tsx +++ b/src/components/alert/Alert.stories.tsx @@ -1,3 +1,6 @@ +import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' import { Controls, Markdown, @@ -7,20 +10,17 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' -import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import { Theme } from '../../../.storybook/components' import { Button, ButtonVariant } from '../button' import { Size, Tone } from '../types' -import { Theme } from '../../../.storybook/components' import { Alert } from './Alert' -import { AlertActionsPosition } from './types' import { AlertCritical, AlertInformative, AlertSuccess, AlertWarning, } from './ConvenientAlerts' +import { AlertActionsPosition } from './types' const icons = { options: ['None', 'Warning', 'Check', 'Info'], diff --git a/src/components/alert/ConvenientAlerts.tsx b/src/components/alert/ConvenientAlerts.tsx index 8ac6b308..00646b9d 100644 --- a/src/components/alert/ConvenientAlerts.tsx +++ b/src/components/alert/ConvenientAlerts.tsx @@ -1,6 +1,6 @@ import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' import { ReactElement } from 'react' import { Tone } from '../types' import { Alert } from './Alert' diff --git a/src/components/breadcrumbs/BreadcrumbLoading.tsx b/src/components/breadcrumbs/BreadcrumbLoading.tsx index 5f6baeb6..984ce67c 100644 --- a/src/components/breadcrumbs/BreadcrumbLoading.tsx +++ b/src/components/breadcrumbs/BreadcrumbLoading.tsx @@ -13,7 +13,7 @@ export function BreadcrumbLoading({ const { breadcrumbs } = useTheme() const { className: loadingBarClassName, ...restLoadingBarProps } = - loadingBarProps || {} + loadingBarProps ?? {} return (
diff --git a/src/components/breadcrumbs/Breadcrumbs.stories.tsx b/src/components/breadcrumbs/Breadcrumbs.stories.tsx index 4175ab9d..d97b96aa 100644 --- a/src/components/breadcrumbs/Breadcrumbs.stories.tsx +++ b/src/components/breadcrumbs/Breadcrumbs.stories.tsx @@ -8,11 +8,11 @@ import { } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../.storybook/components' -import { Breadcrumbs } from './Breadcrumbs' import { BreadcrumbLink } from './BreadcrumbLink' +import { BreadcrumbLoading } from './BreadcrumbLoading' +import { Breadcrumbs } from './Breadcrumbs' import { BreadcrumbsLoading } from './BreadcrumbsLoading' import { BreadcrumbText } from './BreadcrumbText' -import { BreadcrumbLoading } from './BreadcrumbLoading' const meta = { component: Breadcrumbs, diff --git a/src/components/button/Button.stories.tsx b/src/components/button/Button.stories.tsx index 71f0409d..def02892 100644 --- a/src/components/button/Button.stories.tsx +++ b/src/components/button/Button.stories.tsx @@ -1,4 +1,5 @@ -import { Meta, StoryObj } from '@storybook/react' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Controls, Description, @@ -9,14 +10,13 @@ import { Subheading, Title, } from '@storybook/blocks' -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../.storybook/components' -import { Mode, Size } from '../types' import { CustomTheme } from '../../../.storybook/components/CustomTheme' import { Background } from '../../../.storybook/types' -import { ButtonVariant } from './types' +import { Mode, Size } from '../types' import { Button } from './Button' +import { ButtonVariant } from './types' const icons = { options: ['None', 'Add', 'Info'], diff --git a/src/components/button/ButtonIcon.stories.tsx b/src/components/button/ButtonIcon.stories.tsx index 2b49db7c..88e08138 100644 --- a/src/components/button/ButtonIcon.stories.tsx +++ b/src/components/button/ButtonIcon.stories.tsx @@ -1,3 +1,5 @@ +import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Controls, Description, @@ -7,14 +9,12 @@ import { Subheading, Title, } from '@storybook/addon-docs' -import { Meta, StoryObj } from '@storybook/react' -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Source } from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../.storybook/components' import { Mode, Size, Tone } from '../types' -import { ButtonVariant } from './types' import { ButtonIcon } from './ButtonIcon' +import { ButtonVariant } from './types' const icons = { options: ['Info', 'Add'], @@ -75,7 +75,7 @@ const meta = { ' },\n' + '}' } - > + /> ), }, diff --git a/src/components/button/ButtonIcon.tsx b/src/components/button/ButtonIcon.tsx index 5aa4afff..7557ac38 100644 --- a/src/components/button/ButtonIcon.tsx +++ b/src/components/button/ButtonIcon.tsx @@ -1,5 +1,5 @@ -import React, { forwardRef } from 'react' import classNames from 'classnames' +import React, { forwardRef } from 'react' import { useTheme } from '../../framework' import { Mode, Size, Tone } from '../types' import { ButtonIconCommonProps, ButtonStyleProps, ButtonVariant } from './types' diff --git a/src/components/button/ButtonIconLink.stories.tsx b/src/components/button/ButtonIconLink.stories.tsx index ff64f958..1503005a 100644 --- a/src/components/button/ButtonIconLink.stories.tsx +++ b/src/components/button/ButtonIconLink.stories.tsx @@ -1,3 +1,5 @@ +import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Controls, Description, @@ -6,12 +8,10 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' -import { Mode, Size, Tone } from '../types' import { Theme } from '../../../.storybook/components' -import { ButtonVariant } from './types' +import { Mode, Size, Tone } from '../types' import { ButtonIconLink } from './ButtonIconLink' +import { ButtonVariant } from './types' const meta = { component: ButtonIconLink, diff --git a/src/components/button/ButtonIconLink.tsx b/src/components/button/ButtonIconLink.tsx index f93311bb..254c6edf 100644 --- a/src/components/button/ButtonIconLink.tsx +++ b/src/components/button/ButtonIconLink.tsx @@ -5,8 +5,8 @@ import { Mode, Size, Tone } from '../types' import { ButtonIconCommonProps, ButtonStyleProps, - LinkCommonProps, ButtonVariant, + LinkCommonProps, } from './types' export type ButtonIconLinkProps = LinkComponentProps & diff --git a/src/components/button/ButtonLink.stories.tsx b/src/components/button/ButtonLink.stories.tsx index b427ae10..c190c93f 100644 --- a/src/components/button/ButtonLink.stories.tsx +++ b/src/components/button/ButtonLink.stories.tsx @@ -9,10 +9,10 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import { Mode, Size, Tone } from '../types' import { Theme } from '../../../.storybook/components' -import { ButtonVariant } from './types' +import { Mode, Size, Tone } from '../types' import { ButtonLink } from './ButtonLink' +import { ButtonVariant } from './types' const meta = { component: ButtonLink, diff --git a/src/components/button/ButtonLink.tsx b/src/components/button/ButtonLink.tsx index 9d56179a..6e73a6fa 100644 --- a/src/components/button/ButtonLink.tsx +++ b/src/components/button/ButtonLink.tsx @@ -1,12 +1,12 @@ import classNames from 'classnames' import { forwardRef } from 'react' -import { useLinkComponent, LinkComponentProps, useTheme } from '../../framework' +import { LinkComponentProps, useLinkComponent, useTheme } from '../../framework' import { Mode, Size, Tone } from '../types' import { ButtonCommonProps, ButtonStyleProps, - LinkCommonProps, ButtonVariant, + LinkCommonProps, } from './types' export type ButtonLinkProps = LinkComponentProps & diff --git a/src/components/button/SubmitButton.stories.tsx b/src/components/button/SubmitButton.stories.tsx index b4198994..fa2ecbb2 100644 --- a/src/components/button/SubmitButton.stories.tsx +++ b/src/components/button/SubmitButton.stories.tsx @@ -1,3 +1,5 @@ +import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Controls, Description, @@ -8,8 +10,6 @@ import { } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' import { useForm } from 'react-hook-form' -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import { Form } from '../react-hook-form' import { SubmitButton } from './SubmitButton' diff --git a/src/components/content/ContentMessage/ContentMessage.stories.tsx b/src/components/content/ContentMessage/ContentMessage.stories.tsx index 56e602b7..a9840315 100644 --- a/src/components/content/ContentMessage/ContentMessage.stories.tsx +++ b/src/components/content/ContentMessage/ContentMessage.stories.tsx @@ -1,6 +1,6 @@ +import IconError from '@aboutbits/react-material-icons/dist/IconError' import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' -import IconError from '@aboutbits/react-material-icons/dist/IconError' import { Controls, Description, diff --git a/src/components/content/DescriptionItem/DescriptionItem.tsx b/src/components/content/DescriptionItem/DescriptionItem.tsx index 063db19a..29ecdb9c 100644 --- a/src/components/content/DescriptionItem/DescriptionItem.tsx +++ b/src/components/content/DescriptionItem/DescriptionItem.tsx @@ -51,7 +51,7 @@ export function DescriptionItem({ }: DescriptionItemProps) { return ( <> - {((props.hideIfEmpty && props.content) || !props.hideIfEmpty) && ( + {((props.hideIfEmpty && props.content) ?? !props.hideIfEmpty) && ( {title} diff --git a/src/components/dialog/Dialog/Dialog.stories.tsx b/src/components/dialog/Dialog/Dialog.stories.tsx index 426f4b52..21a5b881 100644 --- a/src/components/dialog/Dialog/Dialog.stories.tsx +++ b/src/components/dialog/Dialog/Dialog.stories.tsx @@ -1,6 +1,5 @@ import { ComponentMeta, ComponentStory, DecoratorFn } from '@storybook/react' import { ReactElement, ReactNode, useState } from 'react' -import { Button } from '../../button' import { Dialog, DialogContent, @@ -11,15 +10,16 @@ import { DialogHeader, DialogHeaderArea, DialogHeaderCloseAction, + DialogHeaderGroup, DialogHeaderLeftActionIcon, DialogHeaderRow, - DialogHeaderGroup, DialogHeaderTitle, DialogHeaderWithClose, DialogPosition, DialogProps, DialogSize, } from '../' +import { Button } from '../../button' import DialogDocs from './Dialog.docs.mdx' type TemplateArgs = DialogProps & { content: ReactNode } diff --git a/src/components/dialog/DialogItem/DialogListItemButton.tsx b/src/components/dialog/DialogItem/DialogListItemButton.tsx index 9349bfce..a2686d71 100644 --- a/src/components/dialog/DialogItem/DialogListItemButton.tsx +++ b/src/components/dialog/DialogItem/DialogListItemButton.tsx @@ -1,6 +1,6 @@ import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRight' import classNames from 'classnames' -import { forwardRef, MouseEventHandler } from 'react' +import { MouseEventHandler, forwardRef } from 'react' import { useTheme } from '../../../framework' import { ClassNameProps } from '../../types' diff --git a/src/components/form/InputField.stories.tsx b/src/components/form/InputField.stories.tsx index f897da21..4cd51736 100644 --- a/src/components/form/InputField.stories.tsx +++ b/src/components/form/InputField.stories.tsx @@ -1,5 +1,5 @@ -import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' +import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' import { Controls, Description, @@ -115,8 +115,8 @@ export const Disabled: Story = { export const WithIcons: Story = { render: (args) => (
- - + +
), } diff --git a/src/components/form/SearchField.stories.tsx b/src/components/form/SearchField.stories.tsx index 74efc37d..2b12bc5f 100644 --- a/src/components/form/SearchField.stories.tsx +++ b/src/components/form/SearchField.stories.tsx @@ -1,4 +1,3 @@ -import { Meta, StoryObj } from '@storybook/react' import { Controls, Description, @@ -7,6 +6,7 @@ import { Subheading, Title, } from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' import { InternationalizationMessages, Theme, diff --git a/src/components/form/SelectField.stories.tsx b/src/components/form/SelectField.stories.tsx index 012a145d..c00f2c7a 100644 --- a/src/components/form/SelectField.stories.tsx +++ b/src/components/form/SelectField.stories.tsx @@ -7,8 +7,8 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import { SelectField } from './SelectField' import { Option } from './primitive/Option' +import { SelectField } from './SelectField' import { Status } from './types' const children = ( diff --git a/src/components/form/SelectMonthField.stories.tsx b/src/components/form/SelectMonthField.stories.tsx index 6277735d..9458c5fa 100644 --- a/src/components/form/SelectMonthField.stories.tsx +++ b/src/components/form/SelectMonthField.stories.tsx @@ -45,7 +45,7 @@ const meta = { 'month.november', 'month.december', ]} - > + /> ), }, diff --git a/src/components/form/SelectMonthField.tsx b/src/components/form/SelectMonthField.tsx index 89c5a66f..df792aaf 100644 --- a/src/components/form/SelectMonthField.tsx +++ b/src/components/form/SelectMonthField.tsx @@ -1,8 +1,8 @@ import { forwardRef } from 'react' import { useInternationalization } from '../../framework' import { Mode } from '../types' -import { SelectField, SelectFieldProps } from './SelectField' import { Option } from './primitive' +import { SelectField, SelectFieldProps } from './SelectField' export const MONTHS = [ 'JANUARY', diff --git a/src/components/form/SelectYearField.tsx b/src/components/form/SelectYearField.tsx index 9d4e7d1f..6f71e61b 100644 --- a/src/components/form/SelectYearField.tsx +++ b/src/components/form/SelectYearField.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react' import { Mode } from '../types' -import { SelectField, SelectFieldProps } from './SelectField' import { Option } from './primitive' +import { SelectField, SelectFieldProps } from './SelectField' export type SelectYearFieldProps = Omit & SelectYearFieldOptionsProps diff --git a/src/components/form/primitive/Checkbox/Checkbox.stories.tsx b/src/components/form/primitive/Checkbox/Checkbox.stories.tsx index f331a7e3..e7e45ca2 100644 --- a/src/components/form/primitive/Checkbox/Checkbox.stories.tsx +++ b/src/components/form/primitive/Checkbox/Checkbox.stories.tsx @@ -1,4 +1,3 @@ -import { Meta, StoryObj } from '@storybook/react' import { Controls, Description, @@ -7,6 +6,7 @@ import { Subheading, Title, } from '@storybook/addon-docs' +import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../../../.storybook/components' import { Checkbox } from './Checkbox' diff --git a/src/components/form/primitive/Checkbox/Checkbox.tsx b/src/components/form/primitive/Checkbox/Checkbox.tsx index e75cfdf8..26b8a710 100644 --- a/src/components/form/primitive/Checkbox/Checkbox.tsx +++ b/src/components/form/primitive/Checkbox/Checkbox.tsx @@ -1,7 +1,7 @@ import IconCheckBoxOutlineBlankRounded from '@aboutbits/react-material-icons/dist/IconCheckBoxOutlineBlankRounded' import IconCheckBoxRounded from '@aboutbits/react-material-icons/dist/IconCheckBoxRounded' import classNames from 'classnames' -import { forwardRef, ReactNode } from 'react' +import { ReactNode, forwardRef } from 'react' import { useTheme } from '../../../../framework' import { HideRequiredProps, diff --git a/src/components/form/primitive/Input.stories.tsx b/src/components/form/primitive/Input.stories.tsx index d8ad959e..facdf92a 100644 --- a/src/components/form/primitive/Input.stories.tsx +++ b/src/components/form/primitive/Input.stories.tsx @@ -1,3 +1,5 @@ +import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' +import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' import { Controls, Description, @@ -7,8 +9,6 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' -import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' import { Theme } from '../../../../.storybook/components' import { FormTone, FormVariant } from '../types' import { Input } from './Input' diff --git a/src/components/form/primitive/InputIcon/InputIcon.stories.tsx b/src/components/form/primitive/InputIcon/InputIcon.stories.tsx index de87c1f1..8b1ca549 100644 --- a/src/components/form/primitive/InputIcon/InputIcon.stories.tsx +++ b/src/components/form/primitive/InputIcon/InputIcon.stories.tsx @@ -1,3 +1,5 @@ +import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' +import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' import { Controls, Description, @@ -7,8 +9,6 @@ import { Title, } from '@storybook/addon-docs' import { Meta, StoryObj } from '@storybook/react' -import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' -import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' import { Theme } from '../../../../../.storybook/components' import { InputIcon } from './InputIcon' import { IconPosition } from './types' diff --git a/src/components/form/primitive/Radio/Radio.tsx b/src/components/form/primitive/Radio/Radio.tsx index 9a259c5e..d76f2a30 100644 --- a/src/components/form/primitive/Radio/Radio.tsx +++ b/src/components/form/primitive/Radio/Radio.tsx @@ -1,7 +1,7 @@ -import classNames from 'classnames' -import { forwardRef, ReactNode } from 'react' import IconRadioButtonCheckedRounded from '@aboutbits/react-material-icons/dist/IconRadioButtonCheckedRounded' import IconRadioButtonUncheckedOutlined from '@aboutbits/react-material-icons/dist/IconRadioButtonUncheckedOutlined' +import classNames from 'classnames' +import { ReactNode, forwardRef } from 'react' import { IconProps, Mode, ModeProps, Size } from '../../../types' import { useRadioCss, diff --git a/src/components/form/primitive/Select.stories.tsx b/src/components/form/primitive/Select.stories.tsx index 8ccc8697..65682e5e 100644 --- a/src/components/form/primitive/Select.stories.tsx +++ b/src/components/form/primitive/Select.stories.tsx @@ -9,8 +9,8 @@ import { import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../../.storybook/components' import { FormTone, FormVariant } from '../types' -import { Select } from './Select' import { Option } from './Option' +import { Select } from './Select' const meta = { component: Select, @@ -53,10 +53,10 @@ export const Default: Story = {} export const Variants: Story = { render: (args) => (
- - - - + +
), } @@ -64,8 +64,8 @@ export const Variants: Story = { export const Tone: Story = { render: (args) => (
- - +
), } diff --git a/src/components/form/primitive/ToggleSwitch/ToggleSwitch.tsx b/src/components/form/primitive/ToggleSwitch/ToggleSwitch.tsx index 6daf1c31..cbec729e 100644 --- a/src/components/form/primitive/ToggleSwitch/ToggleSwitch.tsx +++ b/src/components/form/primitive/ToggleSwitch/ToggleSwitch.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { forwardRef, ReactNode } from 'react' +import { ReactNode, forwardRef } from 'react' import { HideRequiredProps, Mode, diff --git a/src/components/link/TextLink.tsx b/src/components/link/TextLink.tsx index 5f01203b..a0dcbd3f 100644 --- a/src/components/link/TextLink.tsx +++ b/src/components/link/TextLink.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames' import { forwardRef } from 'react' -import { useTheme, useLinkComponent, LinkComponentProps } from '../../framework' +import { LinkComponentProps, useLinkComponent, useTheme } from '../../framework' export const TextLink = forwardRef( function TextLink({ children, className, internal = true, ...props }, ref) { diff --git a/src/components/menu/Menu.stories.tsx b/src/components/menu/Menu.stories.tsx index 565ced0f..9cb55095 100644 --- a/src/components/menu/Menu.stories.tsx +++ b/src/components/menu/Menu.stories.tsx @@ -1,15 +1,15 @@ import IconArrowDropUp from '@aboutbits/react-material-icons/dist/IconArrowDropUp' import IconMoreVert from '@aboutbits/react-material-icons/dist/IconMoreVert' import { action } from '@storybook/addon-actions' -import { Meta, StoryObj } from '@storybook/react' import { Controls, + Description, Primary, Stories, Subheading, Title, - Description, } from '@storybook/addon-docs' +import { Meta, StoryObj } from '@storybook/react' import classNames from 'classnames' import { ReactNode, forwardRef, useEffect, useRef } from 'react' import { Theme } from '../../../.storybook/components' diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 67695fc3..dbe4ba7a 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -1,13 +1,13 @@ -import { Menu as HeadlessMenu } from '@headlessui/react' -import { Fragment, ReactElement, ReactNode, ReactPortal } from 'react' import { + FloatingPortal, autoUpdate, - useFloating, flip, offset, - FloatingPortal, + useFloating, } from '@floating-ui/react' +import { Menu as HeadlessMenu } from '@headlessui/react' import classNames from 'classnames' +import { Fragment, ReactElement, ReactNode, ReactPortal } from 'react' import { useTheme } from '../../framework' import { remToPx } from '../util/remToPx' diff --git a/src/components/menu/MenuItem.stories.tsx b/src/components/menu/MenuItem.stories.tsx index 46ea8cd4..29549e93 100644 --- a/src/components/menu/MenuItem.stories.tsx +++ b/src/components/menu/MenuItem.stories.tsx @@ -1,13 +1,13 @@ import { action } from '@storybook/addon-actions' -import { Meta, StoryObj } from '@storybook/react' import { Controls, + Description, Primary, Stories, Subheading, Title, - Description, } from '@storybook/addon-docs' +import { Meta, StoryObj } from '@storybook/react' import { useEffect, useRef } from 'react' import { Theme } from '../../../.storybook/components' import { Button } from '../button' diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx index 7ec553d7..c9f188ba 100644 --- a/src/components/menu/MenuItem.tsx +++ b/src/components/menu/MenuItem.tsx @@ -1,8 +1,8 @@ import { Menu as HeadlessMenu } from '@headlessui/react' -import { Fragment, ReactNode } from 'react' import classNames from 'classnames' -import { ClassNameProps, Tone } from '../types' +import { Fragment, ReactNode } from 'react' import { useLinkComponent, useTheme } from '../../framework' +import { ClassNameProps, Tone } from '../types' export type MenuItemProps = ClassNameProps & { children: ReactNode diff --git a/src/components/pagination/PaginationInMemory.tsx b/src/components/pagination/PaginationInMemory.tsx index 7efa0f85..206972e7 100644 --- a/src/components/pagination/PaginationInMemory.tsx +++ b/src/components/pagination/PaginationInMemory.tsx @@ -1,4 +1,4 @@ -import { calculatePagination, IndexType } from '@aboutbits/pagination' +import { IndexType, calculatePagination } from '@aboutbits/pagination' import classNames from 'classnames' import { ReactNode } from 'react' import { useInternationalization, useTheme } from '../../framework' diff --git a/src/components/pagination/PaginationPreviousNextContent.tsx b/src/components/pagination/PaginationPreviousNextContent.tsx index 667dbefc..c6c36fc4 100644 --- a/src/components/pagination/PaginationPreviousNextContent.tsx +++ b/src/components/pagination/PaginationPreviousNextContent.tsx @@ -1,7 +1,7 @@ -import { ReactElement } from 'react' -import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRight' import IconKeyboardArrowLeft from '@aboutbits/react-material-icons/dist/IconKeyboardArrowLeft' -import { useTheme, useInternationalization } from '../../framework' +import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRight' +import { ReactElement } from 'react' +import { useInternationalization, useTheme } from '../../framework' export function PaginationPreviousContent(): ReactElement { const { pagination } = useTheme() diff --git a/src/components/pagination/PaginationRouter.tsx b/src/components/pagination/PaginationRouter.tsx index 06e6e23e..0ada2639 100644 --- a/src/components/pagination/PaginationRouter.tsx +++ b/src/components/pagination/PaginationRouter.tsx @@ -1,21 +1,21 @@ +import { IndexType, calculatePagination } from '@aboutbits/pagination' import classNames from 'classnames' -import { calculatePagination, IndexType } from '@aboutbits/pagination' import { LinkComponentProps, + useInternationalization, useLinkComponent, useTheme, - useInternationalization, } from '../../framework' import { ClassNameProps } from '../types' import { PaginationContainer } from './PaginationContainer' -import { - PaginationNextContent, - PaginationPreviousContent, -} from './PaginationPreviousNextContent' import { PaginationPagesList, PaginationPagesListItem, } from './PaginationPagesList' +import { + PaginationNextContent, + PaginationPreviousContent, +} from './PaginationPreviousNextContent' export type PaginationRouterProps = ClassNameProps & { /** diff --git a/src/components/react-hook-form/AutoSubmit.tsx b/src/components/react-hook-form/AutoSubmit.tsx index 032986d9..10a05c16 100644 --- a/src/components/react-hook-form/AutoSubmit.tsx +++ b/src/components/react-hook-form/AutoSubmit.tsx @@ -1,6 +1,6 @@ +import { useDebounce } from '@aboutbits/react-toolbox' import { ReactElement, useEffect, useRef } from 'react' import { useWatch } from 'react-hook-form' -import { useDebounce } from '@aboutbits/react-toolbox' export function AutoSubmit({ interval = 200, diff --git a/src/components/react-hook-form/CheckboxFormField.stories.tsx b/src/components/react-hook-form/CheckboxFormField.stories.tsx index ae077e0c..bcac7fd5 100644 --- a/src/components/react-hook-form/CheckboxFormField.stories.tsx +++ b/src/components/react-hook-form/CheckboxFormField.stories.tsx @@ -1,16 +1,16 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { - Title, + Controls, Primary, Stories, Subheading, - Controls, + Title, } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' -import { z } from 'zod' -import { DefaultValues, useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react' +import { DefaultValues, useForm } from 'react-hook-form' +import { z } from 'zod' import { CheckboxFormField } from './CheckboxFormField' import { Form } from './Form' diff --git a/src/components/react-hook-form/DateFormField.stories.tsx b/src/components/react-hook-form/DateFormField.stories.tsx index a1f2dc10..1ecbd93c 100644 --- a/src/components/react-hook-form/DateFormField.stories.tsx +++ b/src/components/react-hook-form/DateFormField.stories.tsx @@ -1,3 +1,5 @@ +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' import { zodResolver } from '@hookform/resolvers/zod' import { action } from '@storybook/addon-actions' import { @@ -11,10 +13,8 @@ import { Description } from '@storybook/blocks' import { Meta, StoryObj } from '@storybook/react' import { DefaultValues, useForm } from 'react-hook-form' import { z } from 'zod' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' -import { Form } from './Form' import { DateFormField } from './DateFormField' +import { Form } from './Form' const icons = { options: ['None', 'Warning', 'Info'], diff --git a/src/components/react-hook-form/FieldSetFormField.stories.tsx b/src/components/react-hook-form/FieldSetFormField.stories.tsx index b435ba40..ee46b1f8 100644 --- a/src/components/react-hook-form/FieldSetFormField.stories.tsx +++ b/src/components/react-hook-form/FieldSetFormField.stories.tsx @@ -11,8 +11,8 @@ import { Description } from '@storybook/blocks' import { Meta, StoryObj } from '@storybook/react' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { Form } from './Form' import { FieldSetFormField } from './FieldSetFormField' +import { Form } from './Form' import { RadioFormField } from './RadioFormField' const YES_NO = ['YES', 'NO'] as const diff --git a/src/components/react-hook-form/InputFormField.stories.tsx b/src/components/react-hook-form/InputFormField.stories.tsx index b52213e5..675ce44c 100644 --- a/src/components/react-hook-form/InputFormField.stories.tsx +++ b/src/components/react-hook-form/InputFormField.stories.tsx @@ -1,20 +1,20 @@ import IconBadge from '@aboutbits/react-material-icons/dist/IconBadge' import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' +import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { - Title, + Controls, Primary, Stories, Subheading, - Controls, + Title, } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' -import { z } from 'zod' -import { DefaultValues, useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react' -import { InputFormField } from './InputFormField' +import { DefaultValues, useForm } from 'react-hook-form' +import { z } from 'zod' import { Form } from './Form' +import { InputFormField } from './InputFormField' const icons = { options: ['None', 'Badge', 'Search'], diff --git a/src/components/react-hook-form/NumberFormField.stories.tsx b/src/components/react-hook-form/NumberFormField.stories.tsx index 1977d4bf..1e571c45 100644 --- a/src/components/react-hook-form/NumberFormField.stories.tsx +++ b/src/components/react-hook-form/NumberFormField.stories.tsx @@ -1,18 +1,18 @@ +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { - Title, + Controls, Primary, Stories, Subheading, - Controls, + Title, } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' -import { z } from 'zod' -import { DefaultValues, useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import { DefaultValues, useForm } from 'react-hook-form' +import { z } from 'zod' import { Form } from './Form' import { NumberFormField } from './NumberFormField' diff --git a/src/components/react-hook-form/SearchFormField.stories.tsx b/src/components/react-hook-form/SearchFormField.stories.tsx index e069767f..bcc6a1de 100644 --- a/src/components/react-hook-form/SearchFormField.stories.tsx +++ b/src/components/react-hook-form/SearchFormField.stories.tsx @@ -1,4 +1,4 @@ -import { Meta, StoryObj } from '@storybook/react' +import { action } from '@storybook/addon-actions' import { Controls, Description, @@ -7,15 +7,15 @@ import { Subheading, Title, } from '@storybook/blocks' -import { action } from '@storybook/addon-actions' +import { Meta, StoryObj } from '@storybook/react' import { useForm } from 'react-hook-form' import { InternationalizationMessages, Theme, } from '../../../.storybook/components' import { Section, SectionHeaderArea } from '../section' -import { SearchFormField } from './SearchFormField' import { Form } from './Form' +import { SearchFormField } from './SearchFormField' const meta = { component: SearchFormField, diff --git a/src/components/react-hook-form/SelectFormField.tsx b/src/components/react-hook-form/SelectFormField.tsx index dac996e9..f193c7b4 100644 --- a/src/components/react-hook-form/SelectFormField.tsx +++ b/src/components/react-hook-form/SelectFormField.tsx @@ -2,10 +2,10 @@ import { Children, ComponentProps, ForwardedRef, - forwardRef, - isValidElement, ReactElement, ReactNode, + forwardRef, + isValidElement, useEffect, useMemo, } from 'react' @@ -57,7 +57,7 @@ export const SelectFormField = forwardRef(function SelectFormField< const setValueAs = useMemo(() => { return ( - registerOptions?.setValueAs || + registerOptions?.setValueAs ?? ((input: unknown) => { return input === '' && transformEmptyToNull ? null : input }) diff --git a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx index 1bf574f2..09bb7e0f 100644 --- a/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx +++ b/src/components/react-hook-form/SelectItemFormField/SelectItemFormField.stories.tsx @@ -10,7 +10,7 @@ import { } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' import { Meta, StoryObj } from '@storybook/react' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { DefaultValues, useForm } from 'react-hook-form' import { z } from 'zod' import { @@ -20,8 +20,8 @@ import { import { ErrorBody } from '../../util' import { Form } from '../Form' import { - SearchQueryParameters, PaginatedResponse, + SearchQueryParameters, SelectItemFormField, } from '.' diff --git a/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldDialog.tsx b/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldDialog.tsx index 2344f3a0..8330199b 100644 --- a/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldDialog.tsx +++ b/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldDialog.tsx @@ -1,9 +1,9 @@ +import { IndexType } from '@aboutbits/pagination' import IconSearch from '@aboutbits/react-material-icons/dist/IconSearch' +import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/routers/inMemory' import { AsyncView } from '@aboutbits/react-toolbox' import { ReactElement, ReactNode } from 'react' import { FormProvider, useForm } from 'react-hook-form' -import { IndexType } from '@aboutbits/pagination' -import { useQueryAndPagination } from '@aboutbits/react-pagination/dist/routers/inMemory' import { useInternationalization, useTheme } from '../../../framework' import { Dialog, @@ -23,8 +23,8 @@ import { } from '../../dialog' import { FormVariant } from '../../form' import { PaginationInMemoryProps } from '../../pagination' -import { InputFormField } from '../InputFormField' import { AutoSubmit } from '../AutoSubmit' +import { InputFormField } from '../InputFormField' type FilterParameters = { search: string diff --git a/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldInput.tsx b/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldInput.tsx index a0f2e242..0def1c95 100644 --- a/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldInput.tsx +++ b/src/components/react-hook-form/SelectItemFormField/SelectItemFormFieldInput.tsx @@ -5,7 +5,7 @@ import { ReactNode, useMemo, useRef } from 'react' import { useInternationalization, useTheme } from '../../../framework' import { FormTone, FormVariant, InputLabel, InputMessage } from '../../form' import { useInputCss } from '../../form/primitive/useThemedCss' -import { Mode, RequiredProps, HideRequiredProps } from '../../types' +import { HideRequiredProps, Mode, RequiredProps } from '../../types' import { useId } from '../../util' import { useFieldError } from '../util/useFieldError' import { replacePlaceholderColorWithTextColor } from './replacePlaceholderColorWithTextColor' diff --git a/src/components/react-hook-form/TextAreaFormField.stories.tsx b/src/components/react-hook-form/TextAreaFormField.stories.tsx index ca93f2f0..6e546eab 100644 --- a/src/components/react-hook-form/TextAreaFormField.stories.tsx +++ b/src/components/react-hook-form/TextAreaFormField.stories.tsx @@ -1,16 +1,16 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { - Title, + Controls, Primary, Stories, Subheading, - Controls, + Title, } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' -import { z } from 'zod' -import { DefaultValues, useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react' +import { DefaultValues, useForm } from 'react-hook-form' +import { z } from 'zod' import { Form } from './Form' import { TextAreaFormField } from './TextAreaFormField' diff --git a/src/components/react-hook-form/ToggleSwitchFormField.stories.tsx b/src/components/react-hook-form/ToggleSwitchFormField.stories.tsx index e82c6c31..172d8f78 100644 --- a/src/components/react-hook-form/ToggleSwitchFormField.stories.tsx +++ b/src/components/react-hook-form/ToggleSwitchFormField.stories.tsx @@ -1,16 +1,16 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { - Title, + Controls, Primary, Stories, Subheading, - Controls, + Title, } from '@storybook/addon-docs' import { Description } from '@storybook/blocks' -import { z } from 'zod' -import { DefaultValues, useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react' +import { DefaultValues, useForm } from 'react-hook-form' +import { z } from 'zod' import { Form } from './Form' import { ToggleSwitchFormField } from './ToggleSwitchFormField' diff --git a/src/components/react-hook-form/__test__/useHandleSubmit.test.ts b/src/components/react-hook-form/__test__/useHandleSubmit.test.ts index edf8c8c3..eaf7fe59 100644 --- a/src/components/react-hook-form/__test__/useHandleSubmit.test.ts +++ b/src/components/react-hook-form/__test__/useHandleSubmit.test.ts @@ -1,8 +1,8 @@ import { act, renderHook, waitFor } from '@testing-library/react' +import { AxiosError, AxiosHeaders } from 'axios' import { FieldValues, useForm } from 'react-hook-form' import { vi } from 'vitest' import { undefined } from 'zod' -import { AxiosError, AxiosHeaders } from 'axios' import { defaultMessages } from '../../../framework/internationalization/defaultMessages.en' import { useHandleSubmit } from '../useHandleSubmit' @@ -27,6 +27,7 @@ describe('useHandleSubmit', () => { }: { response: unknown values: FieldValues + // eslint-disable-next-line @typescript-eslint/no-empty-function }) => {} const headers = new AxiosHeaders() diff --git a/src/components/react-hook-form/useHandleSubmit.ts b/src/components/react-hook-form/useHandleSubmit.ts index d3232765..b8588364 100644 --- a/src/components/react-hook-form/useHandleSubmit.ts +++ b/src/components/react-hook-form/useHandleSubmit.ts @@ -1,11 +1,11 @@ -import { FieldValues, Path, UseFormReturn } from 'react-hook-form' import { useCallback } from 'react' +import { FieldValues, Path, UseFormReturn } from 'react-hook-form' import { - joinFieldErrorMessages, - useHandleRequest, UseHandleRequestOptions, UseHandleRequestReturn, UseHandleRequestTrigger, + joinFieldErrorMessages, + useHandleRequest, } from '../util' const DEFAULT_ERROR_FIELD_PATH = 'apiError' diff --git a/src/components/section/Section/ConvenientSectionContentMessage.tsx b/src/components/section/Section/ConvenientSectionContentMessage.tsx index d2f49021..7d73523a 100644 --- a/src/components/section/Section/ConvenientSectionContentMessage.tsx +++ b/src/components/section/Section/ConvenientSectionContentMessage.tsx @@ -1,6 +1,6 @@ -import { ReactElement } from 'react' import IconList from '@aboutbits/react-material-icons/dist/IconList' import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import { ReactElement } from 'react' import { Tone } from '../../types' import { SectionContentMessage, diff --git a/src/components/section/Section/SectionAppendix.stories.tsx b/src/components/section/Section/SectionAppendix.stories.tsx index ee0895cb..a1880ad8 100644 --- a/src/components/section/Section/SectionAppendix.stories.tsx +++ b/src/components/section/Section/SectionAppendix.stories.tsx @@ -1,4 +1,3 @@ -import { Meta, StoryObj } from '@storybook/react' import { Controls, Description, @@ -7,11 +6,12 @@ import { Subheading, Title, } from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' +import { Theme } from '../../../../.storybook/components/Theme' import { Button, ButtonVariant } from '../../button' import { Tone } from '../../types' -import { Theme } from '../../../../.storybook/components/Theme' -import { SectionAppendix } from './SectionAppendix' import { Section } from './Section' +import { SectionAppendix } from './SectionAppendix' import { SectionContainer } from './SectionContainer' import { SectionContent } from './SectionContent' diff --git a/src/components/section/SectionFooter/SectionFooterArea.stories.tsx b/src/components/section/SectionFooter/SectionFooterArea.stories.tsx index 238a5c47..c137f342 100644 --- a/src/components/section/SectionFooter/SectionFooterArea.stories.tsx +++ b/src/components/section/SectionFooter/SectionFooterArea.stories.tsx @@ -1,12 +1,12 @@ -import { Meta, StoryObj } from '@storybook/react' import { Controls, + Description, Primary, Stories, Subheading, Title, - Description, } from '@storybook/addon-docs' +import { Meta, StoryObj } from '@storybook/react' import { Theme } from '../../../../.storybook/components' import { SectionFooterArea } from '../index' import { SectionFooterVariant } from './types' diff --git a/src/components/section/SectionFooter/SectionFooterWithPaginationInMemory.stories.tsx b/src/components/section/SectionFooter/SectionFooterWithPaginationInMemory.stories.tsx index b4eb39a5..43095ba2 100644 --- a/src/components/section/SectionFooter/SectionFooterWithPaginationInMemory.stories.tsx +++ b/src/components/section/SectionFooter/SectionFooterWithPaginationInMemory.stories.tsx @@ -1,19 +1,19 @@ -import { Meta, StoryObj } from '@storybook/react' +import { IndexType } from '@aboutbits/pagination' import { Controls, + Description, Primary, Stories, Subheading, Title, - Description, } from '@storybook/addon-docs' -import { IndexType } from '@aboutbits/pagination' +import { Meta, StoryObj } from '@storybook/react' import { useState } from 'react' -import { SectionFooterVariant } from './types' import { SectionFooterWithPaginationInMemory, SectionFooterWithPaginationInMemoryProps, } from './SectionFooterWithPaginationInMemory' +import { SectionFooterVariant } from './types' const Template = (args: SectionFooterWithPaginationInMemoryProps) => { const [page, setPage] = useState(args.page) diff --git a/src/components/section/SectionFooter/SectionFooterWithPaginationRouter.stories.tsx b/src/components/section/SectionFooter/SectionFooterWithPaginationRouter.stories.tsx index 613d01bb..60a0bd37 100644 --- a/src/components/section/SectionFooter/SectionFooterWithPaginationRouter.stories.tsx +++ b/src/components/section/SectionFooter/SectionFooterWithPaginationRouter.stories.tsx @@ -1,13 +1,13 @@ -import { Meta, StoryObj } from '@storybook/react' +import { IndexType } from '@aboutbits/pagination' import { Controls, + Description, Primary, Stories, Subheading, Title, - Description, } from '@storybook/addon-docs' -import { IndexType } from '@aboutbits/pagination' +import { Meta, StoryObj } from '@storybook/react' import { SectionFooterWithPaginationRouter } from './SectionFooterWithPaginationRouter' import { SectionFooterVariant } from './types' diff --git a/src/components/section/SectionHeader/SectionHeaderTitle.tsx b/src/components/section/SectionHeader/SectionHeaderTitle.tsx index abfdcbb6..47693707 100644 --- a/src/components/section/SectionHeader/SectionHeaderTitle.tsx +++ b/src/components/section/SectionHeader/SectionHeaderTitle.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from 'react' import classNames from 'classnames' +import { ReactNode } from 'react' import { useTheme } from '../../../framework' import { ClassNameProps } from '../../types' diff --git a/src/components/section/SectionItem/SectionListItem.tsx b/src/components/section/SectionItem/SectionListItem.tsx index 96d7dcde..cbc46311 100644 --- a/src/components/section/SectionItem/SectionListItem.tsx +++ b/src/components/section/SectionItem/SectionListItem.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRight' +import classNames from 'classnames' import { ReactNode, forwardRef } from 'react' import { LinkComponentProps, diff --git a/src/components/tabs/TabsContext.ts b/src/components/tabs/TabsContext.ts index 9dfde698..a3f508d1 100644 --- a/src/components/tabs/TabsContext.ts +++ b/src/components/tabs/TabsContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react' -export interface TabsContextInterface { +export type TabsContextInterface = { /** * Define the active tab by its name */ diff --git a/src/components/util/__test__/useHandleRequest.test.ts b/src/components/util/__test__/useHandleRequest.test.ts index faed68d7..32dd9676 100644 --- a/src/components/util/__test__/useHandleRequest.test.ts +++ b/src/components/util/__test__/useHandleRequest.test.ts @@ -1,6 +1,6 @@ import { act, renderHook, waitFor } from '@testing-library/react' -import { vi } from 'vitest' import { AxiosError, AxiosHeaders } from 'axios' +import { vi } from 'vitest' import { defaultMessages } from '../../../framework/internationalization/defaultMessages.en' import { useHandleRequest } from '../useHandleRequest' diff --git a/src/components/util/useForwardedRef.ts b/src/components/util/useForwardedRef.ts index 54fb88ba..a2bf0101 100644 --- a/src/components/util/useForwardedRef.ts +++ b/src/components/util/useForwardedRef.ts @@ -5,6 +5,7 @@ import { ForwardedRef, useImperativeHandle, useRef } from 'react' */ export function useForwardedRef(ref: ForwardedRef) { const forwardedRef = useRef(null) - useImperativeHandle(ref, () => forwardedRef.current as T) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + useImperativeHandle(ref, () => forwardedRef.current!) return forwardedRef } diff --git a/src/components/util/useHandleRequest.ts b/src/components/util/useHandleRequest.ts index 873170a5..328bd1c6 100644 --- a/src/components/util/useHandleRequest.ts +++ b/src/components/util/useHandleRequest.ts @@ -1,5 +1,5 @@ -import { useCallback, useState } from 'react' import { useIsMounted } from '@aboutbits/react-toolbox' +import { useCallback, useState } from 'react' import { useInternationalization } from '../../framework' import { isAxiosErrorWithErrorBody } from './helpers' import { ErrorBody } from './types' diff --git a/src/components/util/useId.ts b/src/components/util/useId.ts index 897cae95..4b57d971 100644 --- a/src/components/util/useId.ts +++ b/src/components/util/useId.ts @@ -22,7 +22,6 @@ const useIsomorphicLayoutEffect = canUseDOM() ? useLayoutEffect : useEffect function canUseDOM() { return Boolean( typeof window !== 'undefined' && - typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined', ) } diff --git a/src/examples/Form.stories.tsx b/src/examples/Form.stories.tsx index ecddad07..b4a24166 100644 --- a/src/examples/Form.stories.tsx +++ b/src/examples/Form.stories.tsx @@ -1,42 +1,42 @@ import { IndexType } from '@aboutbits/pagination' -import { action } from '@storybook/addon-actions' +import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' import { zodResolver } from '@hookform/resolvers/zod' +import { action } from '@storybook/addon-actions' import { Markdown, Primary, Title } from '@storybook/addon-docs' import { Meta, StoryFn } from '@storybook/react' +import { AxiosError, AxiosHeaders } from 'axios' import { useEffect, useState } from 'react' import { DefaultValues, useForm } from 'react-hook-form' import { z } from 'zod' -import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' -import { AxiosError, AxiosHeaders } from 'axios' import { + Alert, + CheckboxFormField, ContentArea, DescriptionItem, DescriptionItemContentAlignVertical, - Section, - SectionContainer, - SectionContent, - SectionContentLayout, - SectionFooterWithSubmit, - SectionHeader, - Alert, FieldSetField, - FieldSetIndent, - FormError, - Option, - ToggleSwitchLayout, - CheckboxFormField, FieldSetFormField, + FieldSetIndent, Form, + FormError, InputFormField, + Option, PaginatedResponse, RadioFormField, SearchQueryParameters, + Section, + SectionContainer, + SectionContent, + SectionContentLayout, + SectionFooterWithSubmit, + SectionHeader, SelectFormField, SelectItemFormField, TextAreaFormField, ToggleSwitchFormField, - useHandleSubmit, + ToggleSwitchLayout, Tone, + useHandleSubmit, } from '../components' const meta = { diff --git a/src/examples/List.stories.tsx b/src/examples/List.stories.tsx index a5f2d2b8..75e96522 100644 --- a/src/examples/List.stories.tsx +++ b/src/examples/List.stories.tsx @@ -1,15 +1,17 @@ -import { Title, Stories } from '@storybook/blocks' -import { action } from '@storybook/addon-actions' -import { Meta, StoryFn } from '@storybook/react' -import { useMemo, useState } from 'react' import { IndexType } from '@aboutbits/pagination' import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' import { useMatchMediaQuery } from '@aboutbits/react-toolbox' +import { action } from '@storybook/addon-actions' import { Markdown } from '@storybook/addon-docs' +import { Stories, Title } from '@storybook/blocks' +import { Meta, StoryFn } from '@storybook/react' +import { useMemo, useState } from 'react' import { ButtonIcon, ButtonVariant, FormVariant, + Option, + SearchField, Section, SectionContainer, SectionContentEmpty, @@ -24,13 +26,11 @@ import { SectionHeaderSpacer, SectionHeaderTitle, SectionListItemButton, + SectionListItemButtonProps, + SectionListItemLink, SelectField, Tone, - Option, useFilter, - SearchField, - SectionListItemButtonProps, - SectionListItemLink, } from '../components' const meta = { diff --git a/src/examples/Menu.stories.tsx b/src/examples/Menu.stories.tsx index 083e1a05..42f60407 100644 --- a/src/examples/Menu.stories.tsx +++ b/src/examples/Menu.stories.tsx @@ -1,14 +1,14 @@ import { action } from '@storybook/addon-actions' +import { Markdown, Primary, Title } from '@storybook/addon-docs' import { Meta, StoryFn } from '@storybook/react' -import { Primary, Title, Markdown } from '@storybook/addon-docs' import { useEffect, useRef, useState } from 'react' import { - Tone, + Button, + ButtonVariant, Menu, MenuItem, MenuPlacement, - Button, - ButtonVariant, + Tone, } from '../components' const meta = { diff --git a/src/framework/ReactUIProvider.tsx b/src/framework/ReactUIProvider.tsx index a025c0ec..cf2d507a 100644 --- a/src/framework/ReactUIProvider.tsx +++ b/src/framework/ReactUIProvider.tsx @@ -9,8 +9,8 @@ import { LinkComponentContext, } from './router/LinkComponentContext' import { Router, RouterContext } from './router/RouterContext' -import { ThemeContext } from './theme/ThemeContext' import { Theme } from './theme/theme' +import { ThemeContext } from './theme/ThemeContext' export type ReactUIProviderProps = { theme?: Theme @@ -51,9 +51,9 @@ export function ReactUIProvider({ return ( - + {children} diff --git a/src/framework/router/LinkComponentContext.tsx b/src/framework/router/LinkComponentContext.tsx index 3111b34f..020242bf 100644 --- a/src/framework/router/LinkComponentContext.tsx +++ b/src/framework/router/LinkComponentContext.tsx @@ -1,9 +1,9 @@ import { AnchorHTMLAttributes, ComponentType, + ForwardRefRenderFunction, createContext, forwardRef, - ForwardRefRenderFunction, useContext, } from 'react' diff --git a/tsconfig.json b/tsconfig.json index 9b408e36..eb723618 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "compilerOptions": { "moduleResolution": "node", "module": "esnext", - "skipLibCheck": true, + "skipLibCheck": true }, "exclude": ["node_modules", "dist"], - "include": ["**/*.ts", "**/*.tsx", "**/*.js", ".storybook/**/*"] + "include": ["**/*.ts", "**/*.tsx", "**/*.js", ".storybook/**/*", "**/**.json"] } diff --git a/vite.config.ts b/vite.config.ts index 137431f5..68412316 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ /// -import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' export default defineConfig({ plugins: [react()], From 4424cc9ad4cd9bbe0a9b0b4280d7c456403b912b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 27 Aug 2024 11:54:49 +0200 Subject: [PATCH 02/14] disable eslint ?? operator rule for with placeholder --- src/components/content/WithPlaceholder/WithPlaceholder.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/content/WithPlaceholder/WithPlaceholder.tsx b/src/components/content/WithPlaceholder/WithPlaceholder.tsx index 8dd068f3..9a913dff 100644 --- a/src/components/content/WithPlaceholder/WithPlaceholder.tsx +++ b/src/components/content/WithPlaceholder/WithPlaceholder.tsx @@ -14,6 +14,7 @@ export function WithPlaceholder({ placeholder = '-', children, }: WithPlaceholderProps) { + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ return ( <> {typeof children === 'number' From 99cee5f7c458ed6f800b0a3af28560dd96633424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 27 Aug 2024 16:15:27 +0200 Subject: [PATCH 03/14] fix lint errors --- .../content/DescriptionItem/DescriptionItem.tsx | 11 ++++++++--- .../content/WithPlaceholder/WithPlaceholder.tsx | 5 +++-- src/components/util/useId.ts | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/content/DescriptionItem/DescriptionItem.tsx b/src/components/content/DescriptionItem/DescriptionItem.tsx index 29ecdb9c..19d0e16d 100644 --- a/src/components/content/DescriptionItem/DescriptionItem.tsx +++ b/src/components/content/DescriptionItem/DescriptionItem.tsx @@ -47,15 +47,20 @@ export function DescriptionItem({ title, className, contentProps, - ...props + hideIfEmpty = false, + content, }: DescriptionItemProps) { return ( <> - {((props.hideIfEmpty && props.content) ?? !props.hideIfEmpty) && ( + {((hideIfEmpty && + content !== null && + content !== undefined && + content !== '') || + !hideIfEmpty) && ( {title} - {props.content} + {content} )} diff --git a/src/components/content/WithPlaceholder/WithPlaceholder.tsx b/src/components/content/WithPlaceholder/WithPlaceholder.tsx index 9a913dff..4cf4ee8d 100644 --- a/src/components/content/WithPlaceholder/WithPlaceholder.tsx +++ b/src/components/content/WithPlaceholder/WithPlaceholder.tsx @@ -14,14 +14,15 @@ export function WithPlaceholder({ placeholder = '-', children, }: WithPlaceholderProps) { - /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ return ( <> {typeof children === 'number' ? isNaN(children) ? placeholder : children - : children || placeholder} + : children === null || children === undefined || children === '' + ? placeholder + : children} ) } diff --git a/src/components/util/useId.ts b/src/components/util/useId.ts index 4b57d971..8338457c 100644 --- a/src/components/util/useId.ts +++ b/src/components/util/useId.ts @@ -22,6 +22,8 @@ const useIsomorphicLayoutEffect = canUseDOM() ? useLayoutEffect : useEffect function canUseDOM() { return Boolean( typeof window !== 'undefined' && + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined', ) } From 8f7dfa1b4038f7d34c388f6462e617a99697af9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Thu, 29 Aug 2024 17:04:17 +0200 Subject: [PATCH 04/14] add upload component --- package-lock.json | 14 + package.json | 3 +- .../button/ResponsiveButtonIcon.tsx | 52 +++ src/components/button/theme.ts | 4 + src/components/files/DeleteFileAction.tsx | 44 ++ src/components/files/DownloadFileAction.tsx | 23 + src/components/files/FileDropZone.stories.tsx | 44 ++ src/components/files/FileDropZone.tsx | 115 +++++ src/components/files/FileList.tsx | 9 + src/components/files/FileListItem.tsx | 166 +++++++ src/components/files/FileUpload.stories.tsx | 375 ++++++++++++++++ src/components/files/FileUploadState.tsx | 410 ++++++++++++++++++ ...leUploadWithDeleteDialogAction.stories.tsx | 172 ++++++++ src/components/files/index.tsx | 6 + src/components/files/theme.ts | 43 ++ src/components/files/useFileDropZone.ts | 83 ++++ src/components/files/useFileUpload.tsx | 275 ++++++++++++ src/components/files/useMockedUploadApi.ts | 123 ++++++ src/components/files/utils.ts | 64 +++ src/components/loading/IconSpinner.tsx | 37 ++ src/components/loading/theme.ts | 5 + src/components/types.ts | 5 +- .../defaultMessages.en.ts | 10 + src/framework/theme/theme.ts | 2 + tailwind-preset.ts | 23 + 25 files changed, 2105 insertions(+), 2 deletions(-) create mode 100644 src/components/button/ResponsiveButtonIcon.tsx create mode 100644 src/components/files/DeleteFileAction.tsx create mode 100644 src/components/files/DownloadFileAction.tsx create mode 100644 src/components/files/FileDropZone.stories.tsx create mode 100644 src/components/files/FileDropZone.tsx create mode 100644 src/components/files/FileList.tsx create mode 100644 src/components/files/FileListItem.tsx create mode 100644 src/components/files/FileUpload.stories.tsx create mode 100644 src/components/files/FileUploadState.tsx create mode 100644 src/components/files/FileUploadWithDeleteDialogAction.stories.tsx create mode 100644 src/components/files/index.tsx create mode 100644 src/components/files/theme.ts create mode 100644 src/components/files/useFileDropZone.ts create mode 100644 src/components/files/useFileUpload.tsx create mode 100644 src/components/files/useMockedUploadApi.ts create mode 100644 src/components/files/utils.ts create mode 100644 src/components/loading/IconSpinner.tsx diff --git a/package-lock.json b/package-lock.json index e9909faf..76d0197c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@typescript-eslint/eslint-plugin": "^7.4.0", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", + "axios-mock-adapter": "^1.22.0", "cssnano": "^6.1.1", "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", @@ -7935,6 +7936,19 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/axios/node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", diff --git a/package.json b/package.json index d6541748..dba686ff 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,8 @@ "typescript": "^5.4.3", "vite": "^4.4.3", "vitest": "^0.33.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "axios-mock-adapter": "^1.22.0" }, "peerDependencies": { "@aboutbits/react-pagination": "^3.0.3", diff --git a/src/components/button/ResponsiveButtonIcon.tsx b/src/components/button/ResponsiveButtonIcon.tsx new file mode 100644 index 00000000..a77c8816 --- /dev/null +++ b/src/components/button/ResponsiveButtonIcon.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames' +import { ComponentType, ReactNode } from 'react' +import { useTheme } from '../../framework' +import { IconProps } from '../types' +import { Button, ButtonProps } from './Button' +import { ButtonIcon, ButtonIconProps } from './ButtonIcon' + +export type ResponsiveButtonIconProps = Omit< + ButtonProps & ButtonIconProps, + 'ref' | 'children' | 'icon' | 'iconStart' | 'iconEnd' | 'label' +> & { + label: ReactNode +} & ( + | { + icon: ComponentType + iconEnd?: never + } + | { + icon?: never + iconEnd: ComponentType + } + ) + +export function ResponsiveButtonIcon({ + label, + className, + icon, + iconEnd, + ...props +}: ResponsiveButtonIconProps) { + const { button } = useTheme() + return ( + <> + + + + ) +} diff --git a/src/components/button/theme.ts b/src/components/button/theme.ts index bf70370e..157b0b4e 100644 --- a/src/components/button/theme.ts +++ b/src/components/button/theme.ts @@ -77,6 +77,10 @@ export default { }, }, }, + buttonIconResponsive: { + button: 'hidden md:flex', + buttonIcon: 'md:hidden', + }, modeVariantTone: { [Mode.Light]: { [ButtonVariant.Solid]: { diff --git a/src/components/files/DeleteFileAction.tsx b/src/components/files/DeleteFileAction.tsx new file mode 100644 index 00000000..40c1f350 --- /dev/null +++ b/src/components/files/DeleteFileAction.tsx @@ -0,0 +1,44 @@ +import IconDelete from '@aboutbits/react-material-icons/dist/IconDelete' +import { useState } from 'react' +import { useInternationalization } from '../../framework' +import { ButtonVariant } from '../button' +import { ResponsiveButtonIcon } from '../button/ResponsiveButtonIcon' +import { IconSpinner } from '../loading/IconSpinner' +import { Tone } from '../types' +import { FileUploadObject } from './FileUploadState' + +type DeleteFileActionProps = { + fileUploadObject: FileUploadObject + onDelete: ( + fileUploadObject: FileUploadObject, + ) => void | Promise +} + +export function DeleteFileAction({ + fileUploadObject, + onDelete, +}: DeleteFileActionProps) { + const [isDeleting, setIsDeleting] = useState(false) + const { messages } = useInternationalization() + return ( + <> + { + setIsDeleting(true) + Promise.resolve(onDelete(fileUploadObject)) + .then(() => { + setIsDeleting(false) + }) + .catch(() => { + setIsDeleting(false) + }) + }} + icon={isDeleting ? IconSpinner : IconDelete} + label={messages['files.action.delete']} + /> + + ) +} diff --git a/src/components/files/DownloadFileAction.tsx b/src/components/files/DownloadFileAction.tsx new file mode 100644 index 00000000..71509167 --- /dev/null +++ b/src/components/files/DownloadFileAction.tsx @@ -0,0 +1,23 @@ +import IconDownload from '@aboutbits/react-material-icons/dist/IconDownload' +import { useInternationalization } from '../../framework' +import { ButtonVariant } from '../button' +import { + ResponsiveButtonIcon, + ResponsiveButtonIconProps, +} from '../button/ResponsiveButtonIcon' +import { Tone } from '../types' + +export function DownloadFileAction({ + onClick, +}: Pick) { + const { messages } = useInternationalization() + return ( + + ) +} diff --git a/src/components/files/FileDropZone.stories.tsx b/src/components/files/FileDropZone.stories.tsx new file mode 100644 index 00000000..a5ab21c8 --- /dev/null +++ b/src/components/files/FileDropZone.stories.tsx @@ -0,0 +1,44 @@ +import { + Controls, + Description, + Primary, + Subheading, + Title, +} from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' +import { FileDropZone } from './FileDropZone' + +const meta = { + component: FileDropZone, + argTypes: { + fileTypes: { + options: ['None', 'pdf', 'jpg, png, gif'], + control: { + type: 'select', + }, + mapping: { + None: undefined, + pdf: ['pdf'], + 'jpg, png, gif': ['jpg', 'png', 'gif'], + }, + }, + }, + parameters: { + docs: { + page: () => ( + <> + + <Description /> + <Primary /> + <Subheading>Props</Subheading> + <Controls /> + </> + ), + }, + }, +} satisfies Meta<typeof FileDropZone> + +export default meta +type Story = StoryObj<typeof FileDropZone> + +export const Default: Story = {} diff --git a/src/components/files/FileDropZone.tsx b/src/components/files/FileDropZone.tsx new file mode 100644 index 00000000..467f3994 --- /dev/null +++ b/src/components/files/FileDropZone.tsx @@ -0,0 +1,115 @@ +import IconUploadFile from '@aboutbits/react-material-icons/dist/IconUploadFile' +import classNames from 'classnames' +import { ChangeEventHandler, useRef } from 'react' +import { useInternationalization, useTheme } from '../../framework' +import { ClassNameProps } from '../types' +import { useFileDropZone } from './useFileDropZone' +import { useHumanReadableFileSize } from './utils' + +export type FileDropZoneProps = { + fileTypes?: string[] + disabled?: boolean + maxFileSize?: number + className?: string +} & ( + | { multipleFiles?: false; onSelect?: (file: File) => void } + | { multipleFiles: true; onSelect?: (files: File[]) => void } +) & + ClassNameProps + +export function FileDropZone({ + fileTypes, + multipleFiles, + disabled = false, + onSelect, + maxFileSize, + className, +}: FileDropZoneProps) { + const inputRef = useRef<HTMLInputElement>(null) + + const handleFilesChange: ChangeEventHandler<HTMLInputElement> = ({ + target, + }) => { + if (!inputRef.current) { + throw new Error('FileUpload: No input element specified!') + } + + const files = Array.from(target.files ?? []) + + if (multipleFiles) { + onSelect?.(files) + } else if (files[0]) { + onSelect?.(files[0]) + } + + target.value = '' + } + + const { files } = useTheme() + + const { messages } = useInternationalization() + + const formatFileSize = useHumanReadableFileSize() + + const { dropZoneRef, isFileHovering } = useFileDropZone<HTMLButtonElement>({ + disabled, + inputRef, + }) + + return ( + <div className={files.fileDropzone.container.base}> + <input + type="file" + ref={inputRef} + multiple={multipleFiles} + accept={fileTypes?.map((fileType) => `.${fileType}`).join(',')} + onChange={handleFilesChange} + disabled={disabled} + hidden + /> + <button + ref={dropZoneRef} + type="button" + onClick={() => inputRef.current?.click()} + className={classNames( + files.fileDropzone.uploadButton.base, + isFileHovering && files.fileDropzone.uploadButton.fileHovering, + className, + )} + > + <div + className={classNames( + files.icon.container.base, + files.icon.container.default, + )} + > + <IconUploadFile + className={classNames(files.icon.size, files.icon.default)} + /> + </div> + <div className={files.text.base}> + <div> + <span className={files.text.underline}> + {messages['files.dropzone.description.part1.text']} + </span> + {messages['files.dropzone.description.part2.text']} + </div> + <div className={files.text.info}> + {fileTypes && fileTypes.length > 0 && ( + <> + {messages['files.dropzone.allowedFileTypes.text']}{' '} + {fileTypes.join(', ')}{' '} + </> + )} + {maxFileSize && ( + <> + ({messages['files.dropzone.maxFileSize.text']}{' '} + {formatFileSize(maxFileSize)}) + </> + )} + </div> + </div> + </button> + </div> + ) +} diff --git a/src/components/files/FileList.tsx b/src/components/files/FileList.tsx new file mode 100644 index 00000000..7c2d16b0 --- /dev/null +++ b/src/components/files/FileList.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react' +import { useTheme } from '../../framework' + +export type FileListProps = PropsWithChildren + +export function FileList({ children }: FileListProps) { + const { files } = useTheme() + return <div className={files.fileList.container}>{children}</div> +} diff --git a/src/components/files/FileListItem.tsx b/src/components/files/FileListItem.tsx new file mode 100644 index 00000000..5d93582c --- /dev/null +++ b/src/components/files/FileListItem.tsx @@ -0,0 +1,166 @@ +import IconCheckCircle from '@aboutbits/react-material-icons/dist/IconCheckCircle' +import IconInsertDriveFile from '@aboutbits/react-material-icons/dist/IconInsertDriveFile' +import classNames from 'classnames' +import { ReactNode, useEffect, useState } from 'react' +import { useInternationalization, useTheme } from '../../framework' +import { IconSpinner } from '../loading/IconSpinner' +import { FileSpace, FileState, FileUploadObject } from './FileUploadState' +import { useHumanReadableFileSize } from './utils' + +export type FileListItemProps<TRemoteFile> = { + fileUploadObject: FileUploadObject<TRemoteFile> + renderRemoteFileName: (remoteFile: TRemoteFile) => string + /** + * @param remoteFile + * @returns The size of the remote file in bytes + */ + renderRemoteFileSize?: (remoteFile: TRemoteFile) => number + disabled?: boolean + fileActions?: ReactNode +} + +export function FileListItem<TRemoteFile>({ + fileUploadObject, + renderRemoteFileName, + disabled, + fileActions, + renderRemoteFileSize, +}: FileListItemProps<TRemoteFile>) { + const [recentlyUploaded, setRecentlyUploaded] = useState<boolean>() + const formatFileSize = useHumanReadableFileSize() + + const fileSize = + fileUploadObject.space === FileSpace.Remote + ? renderRemoteFileSize?.(fileUploadObject.file) + : fileUploadObject.file.size + + const formattedFileSize = fileSize ? formatFileSize(fileSize) : undefined + + const name = + fileUploadObject.space === FileSpace.Remote + ? renderRemoteFileName(fileUploadObject.file) + : fileUploadObject.file.name + + useEffect(() => { + if ( + fileUploadObject.prevState === FileState.Uploading && + fileUploadObject.state === FileState.Uploaded + ) { + setRecentlyUploaded(true) + setTimeout(() => { + setRecentlyUploaded(false) + }, 1800) + } + }, [fileUploadObject.prevState, fileUploadObject.state]) + + const { files } = useTheme() + + return ( + <div className={classNames(files.fileList.item.container)}> + <FileListItemContent + fileName={name} + fileSize={formattedFileSize} + fileUploadObject={fileUploadObject} + recentlyUploaded={recentlyUploaded} + disabled={disabled} + fileActions={fileActions} + /> + </div> + ) +} + +function FileListItemContent<TRemoteFile>({ + fileName, + fileSize, + fileUploadObject, + recentlyUploaded, + disabled, + fileActions, +}: { + fileName: string + fileSize?: string + fileUploadObject: FileUploadObject<TRemoteFile> + recentlyUploaded?: boolean + disabled?: boolean + fileActions?: ReactNode +}) { + const { files } = useTheme() + const { messages } = useInternationalization() + + const fileState = fileUploadObject.state + return ( + <> + <div className={files.fileList.item.content}> + {fileUploadObject.state === FileState.Uploading ? ( + <div + className={classNames( + files.icon.container.base, + files.icon.container.default, + )} + > + <IconSpinner className={files.icon.size} /> + </div> + ) : ( + <div + className={classNames( + files.icon.container.base, + fileUploadObject.state === FileState.Failed + ? files.icon.container.error + : disabled + ? files.icon.container.disabled + : recentlyUploaded + ? files.icon.container.success + : files.icon.container.default, + )} + > + {recentlyUploaded ? ( + <IconCheckCircle + className={classNames(files.icon.size, files.icon.success)} + /> + ) : ( + <IconInsertDriveFile + className={classNames( + files.icon.size, + disabled + ? files.icon.disabled + : fileUploadObject.state === FileState.Failed + ? files.icon.error + : files.icon.default, + )} + /> + )} + </div> + )} + <div className={files.fileList.item.textContainer}> + <div className={classNames(files.text.bold)}>{fileName}</div> + <div + className={ + fileUploadObject.state === FileState.Failed + ? files.text.error + : files.text.info + } + > + {fileUploadObject.state === FileState.Failed + ? fileUploadObject.message + : fileState === FileState.Uploading + ? messages['files.item.uploading'] + : fileState === FileState.Uploaded && fileSize + ? fileSize + : ''} + </div> + </div> + {fileActions && ( + <div className={files.fileList.item.actions}>{fileActions}</div> + )} + </div> + {fileUploadObject.state === FileState.Uploading && + fileUploadObject.progress !== undefined && + fileUploadObject.progress > 0 && ( + <div + className={files.fileList.item.progress} + style={{ width: `${fileUploadObject.progress * 100}%` }} + /> + )} + </> + ) +} diff --git a/src/components/files/FileUpload.stories.tsx b/src/components/files/FileUpload.stories.tsx new file mode 100644 index 00000000..004403af --- /dev/null +++ b/src/components/files/FileUpload.stories.tsx @@ -0,0 +1,375 @@ +/* eslint-disable react-hooks/rules-of-hooks */ + +import { + Canvas, + Controls, + Description, + Story, + Subheading, + Title, +} from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' +import { FC, useCallback } from 'react' +import { Button } from '../button' +import { Size } from '../types' +import { DeleteFileAction } from './DeleteFileAction' +import { DownloadFileAction } from './DownloadFileAction' +import { FileDropZone } from './FileDropZone' +import { FileList } from './FileList' +import { FileListItem } from './FileListItem' +import { FileSpace, FileState, FileUploadObject } from './FileUploadState' +import { + FileUploadOnUploadMulitple, + FileUploadOnUploadSingle, + useFileUpload, +} from './useFileUpload' +import { useMockedUploadApi } from './useMockedUploadApi' + +export type CustomRemoteFile = { + id: number + name: string + size: number +} + +type DemoComponentType = FC<{ + autoUpload: boolean +}> +const DemoComponent = () => null + +const meta = { + component: DemoComponent, + args: { + autoUpload: true, + }, + argTypes: { + autoUpload: { + control: { + type: 'boolean', + }, + }, + }, + parameters: { + docs: { + page: () => ( + <> + <Title /> + <Description /> + <Subheading>Single file</Subheading> + <Canvas> + <Story of={Single} /> + </Canvas> + <Controls of={Single} /> + <Subheading>Multiple files</Subheading> + <Canvas> + <Story of={Multiple} /> + </Canvas> + <Controls of={Multiple} /> + <Subheading>With delete dialog</Subheading> + <Canvas> + <Story of={WithDeleteDialog} /> + </Canvas> + <Controls of={WithDeleteDialog} /> + </> + ), + }, + }, +} satisfies Meta<DemoComponentType> + +export default meta +type Story = StoryObj<DemoComponentType> + +export const Single: Story = { + render: ({ autoUpload }) => { + const { remoteFile, mutateRemoteFiles, axiosInstance } = useMockedUploadApi( + { + multipleFiles: false, + initialFile: { id: 1, name: 'file1.pdf', size: 1024 }, + }, + ) + + const onUpload = useCallback<FileUploadOnUploadSingle>( + async (file, { onProgress, onError, onSuccess }) => { + const formData = new FormData() + formData.append('file', file) + + try { + await axiosInstance.post('/upload', formData, { + onUploadProgress: (event) => { + if (event.total) { + onProgress(event.loaded / event.total) + } + }, + }) + onSuccess() + } catch { + onError('Datei konnte nicht hochgeladen werden') + } + + await mutateRemoteFiles() + }, + [axiosInstance, mutateRemoteFiles], + ) + + const { + fileUploadObjects, + isUploading, + triggerUpload, + removeFile, + addFilesToUpload, + } = useFileUpload<CustomRemoteFile>({ + remoteFile, + fileUploadObjectIsFile: (fileUploadObject, file) => + fileUploadObject.file.name === file.name, + onUpload, + autoUpload, + }) + + const handleDelete = async ( + fileUploadObject: FileUploadObject<CustomRemoteFile>, + ) => { + if (fileUploadObject.space === FileSpace.Local) { + removeFile(fileUploadObject.file) + } else { + await axiosInstance.post('/delete') + removeFile(fileUploadObject.file) + } + } + + return ( + <div className="flex flex-col gap-4"> + {fileUploadObjects.length === 0 && ( + <FileDropZone + onSelect={addFilesToUpload} + fileTypes={['pdf']} + maxFileSize={1000000} + /> + )} + {fileUploadObjects.length > 0 && ( + <FileList> + {fileUploadObjects.map((fileUploadObject) => ( + <FileListItem + key={fileUploadObject.id} + fileUploadObject={fileUploadObject} + renderRemoteFileName={(remoteFile) => remoteFile.name} + renderRemoteFileSize={(remoteFile) => remoteFile.size} + disabled={ + isUploading && fileUploadObject.state !== FileState.Uploading + } + fileActions={ + <DeleteFileAction + fileUploadObject={fileUploadObject} + onDelete={handleDelete} + /> + } + /> + ))} + </FileList> + )} + {!autoUpload && ( + <Button size={Size.Md} onClick={void triggerUpload} className="mt-8"> + Upload + </Button> + )} + </div> + ) + }, +} + +export const Multiple: Story = { + render: ({ autoUpload }) => { + const { remoteFiles, mutateRemoteFiles, axiosInstance } = + useMockedUploadApi({ + multipleFiles: true, + initialFiles: [ + { id: 1, name: 'file1.pdf', size: 1000 }, + { id: 2, name: 'file2.pdf', size: 2000 }, + { id: 3, name: 'blank.pdf', size: 3000 }, + ], + }) + + const onUpload = useCallback<FileUploadOnUploadMulitple>( + async (files, { onProgress, onError, onSuccess }) => { + await Promise.allSettled( + files.map(async (file) => { + const formData = new FormData() + formData.append('file', file) + + try { + await axiosInstance.post('/upload', formData, { + onUploadProgress: (event) => { + if (event.total) { + onProgress(file, event.loaded / event.total) + } + }, + }) + onSuccess(file) + } catch { + onError(file, 'Datei konnte nicht hochgeladen werden') + } + }), + ) + await mutateRemoteFiles() + }, + [axiosInstance, mutateRemoteFiles], + ) + + const { fileUploadObjects, triggerUpload, removeFile, addFilesToUpload } = + useFileUpload({ + remoteFiles, + fileUploadObjectIsFile: (fileUploadObject, file) => + fileUploadObject.file.name === file.name, + onUpload, + multipleFiles: true, + autoUpload, + }) + + const handleDelete = async ( + fileUploadObject: FileUploadObject<CustomRemoteFile>, + ) => { + if (fileUploadObject.space === FileSpace.Local) { + removeFile(fileUploadObject.file) // Move this logic into FileListItem + } else { + await axiosInstance.post('/delete') + removeFile(fileUploadObject.file) + } + } + + return ( + <div className="flex flex-col gap-4"> + <FileDropZone + multipleFiles + onSelect={addFilesToUpload} + fileTypes={['pdf']} + /> + {fileUploadObjects.length > 0 && ( + <FileList> + {fileUploadObjects.map((fileUploadObject) => ( + <FileListItem + key={fileUploadObject.id} + fileUploadObject={fileUploadObject} + renderRemoteFileName={(remoteFile) => remoteFile.name} + renderRemoteFileSize={(remoteFile) => remoteFile.size} + fileActions={ + <> + <DownloadFileAction onClick={() => void triggerUpload()} /> + <DeleteFileAction + fileUploadObject={fileUploadObject} + onDelete={handleDelete} + /> + </> + } + /> + ))} + </FileList> + )} + {!autoUpload && ( + <Button + size={Size.Md} + onClick={() => void triggerUpload()} + className="mt-8" + > + Upload + </Button> + )} + </div> + ) + }, +} + +export const WithDeleteDialog: Story = { + render: ({ autoUpload }) => { + const { remoteFile, mutateRemoteFiles, axiosInstance } = useMockedUploadApi( + { + multipleFiles: false, + initialFile: { id: 1, name: 'file1.pdf', size: 1024 }, + }, + ) + + const onUpload = useCallback<FileUploadOnUploadSingle>( + async (file, { onProgress, onError, onSuccess }) => { + const formData = new FormData() + formData.append('file', file) + + try { + await axiosInstance.post('/upload', formData, { + onUploadProgress: (event) => { + if (event.total) { + onProgress(event.loaded / event.total) + } + }, + }) + onSuccess() + } catch { + onError('Datei konnte nicht hochgeladen werden') + } + + await mutateRemoteFiles() + }, + [axiosInstance, mutateRemoteFiles], + ) + + const { + fileUploadObjects, + isUploading, + triggerUpload, + removeFile, + addFilesToUpload, + } = useFileUpload<CustomRemoteFile>({ + remoteFile, + fileUploadObjectIsFile: (fileUploadObject, file) => + fileUploadObject.file.name === file.name, + onUpload, + autoUpload, + }) + + const handleDelete = async ( + fileUploadObject: FileUploadObject<CustomRemoteFile>, + ) => { + if (fileUploadObject.space === FileSpace.Local) { + removeFile(fileUploadObject.file) + } else { + await axiosInstance.post('/delete') + removeFile(fileUploadObject.file) + } + } + + return ( + <div className="flex flex-col gap-4"> + {fileUploadObjects.length === 0 && ( + <FileDropZone + onSelect={addFilesToUpload} + fileTypes={['pdf']} + maxFileSize={1000000} + /> + )} + {fileUploadObjects.length > 0 && ( + <FileList> + {fileUploadObjects.map((fileUploadObject) => ( + <FileListItem + key={fileUploadObject.id} + fileUploadObject={fileUploadObject} + renderRemoteFileName={(remoteFile) => remoteFile.name} + renderRemoteFileSize={(remoteFile) => remoteFile.size} + disabled={ + isUploading && fileUploadObject.state !== FileState.Uploading + } + fileActions={ + <DeleteFileAction + fileUploadObject={fileUploadObject} + onDelete={handleDelete} + /> + } + /> + ))} + </FileList> + )} + + {!autoUpload && ( + <Button size={Size.Md} onClick={void triggerUpload} className="mt-8"> + Upload + </Button> + )} + </div> + ) + }, +} diff --git a/src/components/files/FileUploadState.tsx b/src/components/files/FileUploadState.tsx new file mode 100644 index 00000000..2c280309 --- /dev/null +++ b/src/components/files/FileUploadState.tsx @@ -0,0 +1,410 @@ +export enum FileState { + ToUpload = 'TO_UPLOAD', + Uploading = 'UPLOADING', + Uploaded = 'UPLOADED', + Failed = 'FAILED', +} + +export enum FileSpace { + Local = 'LOCAL', + Remote = 'REMOTE', +} + +type FileUploadProgress = number | undefined + +export type FileUploadObject<TRemoteFile = unknown> = { + id: string +} & ( + | { + state: FileState.ToUpload + space: FileSpace.Local + prevState: undefined + file: File + } + | { + state: FileState.Uploading + space: FileSpace.Local + prevState: undefined | FileState + file: File + progress: FileUploadProgress + } + | { + state: FileState.Uploaded + space: FileSpace.Remote + prevState: undefined | FileState + file: TRemoteFile + } + | { + state: FileState.Uploaded + space: FileSpace.Local + prevState: undefined | FileState + file: File + } + | { + state: FileState.Failed + space: FileSpace.Local + prevState: undefined | FileState + file: File + message: string | undefined + } +) + +export type FileUploadOptions = { + /** Whether multiple files can be selected. */ + multipleFiles?: boolean +} + +export type FileUploadState<TRemoteFile> = { + fileUploadObjects: FileUploadObject<TRemoteFile>[] + options?: FileUploadOptions +} + +export enum FileUploadActionType { + AddFileToUpload = 'ADD_FILE_TO_UPLOAD', + AddFailedFile = 'ADD_FAILED_FILE', + OverwriteUploadedFiles = 'OVERWRITE_UPLOADED_FILES', + SetFileToUploading = 'SET_FILE_TO_UPLOADING', + SetFileToUploaded = 'SET_FILE_TO_UPLOADED', + SetFileToFailed = 'SET_FILE_TO_FAILED', + UpdateFileUploadProgress = 'UPDATE_FILE_UPLOAD_PROGRESS', + RemoveFileRemote = 'REMOVE_FILE_FROM_REMOTE', + RemoveFileLocal = 'REMOVE_FILE_LOCAL', +} + +export type FileUploadAction<TRemoteFile> = + | { + type: FileUploadActionType.AddFileToUpload + file: File + } + | { + type: FileUploadActionType.AddFailedFile + file: File + message: string | undefined + } + | { + type: FileUploadActionType.OverwriteUploadedFiles + files: TRemoteFile[] + } + | { + type: FileUploadActionType.SetFileToUploading + file: File + progress: FileUploadProgress + } + | { + type: FileUploadActionType.SetFileToUploaded + file: File + uploadedFile?: TRemoteFile + } + | { + type: FileUploadActionType.SetFileToFailed + file: File + message: string | undefined + } + | { + type: FileUploadActionType.UpdateFileUploadProgress + file: File + progress: FileUploadProgress + } + | { + type: FileUploadActionType.RemoveFileLocal + file: File + } + | { + type: FileUploadActionType.RemoveFileRemote + file: TRemoteFile + } + +export function getFileUploadReducer<TRemoteFile>({ + fileUploadObjectIsFile, +}: { + fileUploadObjectIsFile: ( + fileUploadObject: FileUploadObject<TRemoteFile>, + file: File | TRemoteFile, + ) => boolean +}) { + return function fileUploadReducer( + state: FileUploadState<TRemoteFile>, + action: FileUploadAction<TRemoteFile>, + ): FileUploadState<TRemoteFile> { + const fileUploadObjectIsFileInSpace = ( + a: FileUploadObject<TRemoteFile>, + b: File | TRemoteFile, + spaceB: FileSpace, + ) => fileUploadObjectIsFile(a, b) && a.space === spaceB + + switch (action.type) { + case FileUploadActionType.AddFileToUpload: { + const sameFile = state.fileUploadObjects.find((fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ), + ) + if (sameFile) { + if (sameFile.state === FileState.Failed) { + state.fileUploadObjects = state.fileUploadObjects.filter( + (fileUploadObject) => + !fileUploadObjectIsFileInSpace( + fileUploadObject, + sameFile.file, + sameFile.space, + ), + ) + } else { + return state + } + } + return { + ...state, + fileUploadObjects: [ + ...(state.options?.multipleFiles + ? state.fileUploadObjects.filter( + (fileUploadObject) => + !fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ), + ) + : state.fileUploadObjects.filter( + (fileUploadObject) => + fileUploadObject.space !== FileSpace.Local, + )), + { + id: uniqueID(), + state: FileState.ToUpload, + space: FileSpace.Local, + prevState: undefined, + file: action.file, + }, + ], + } + } + case FileUploadActionType.AddFailedFile: { + const fileUploadObject = state.fileUploadObjects.find( + (fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ), + ) + + return { + ...state, + fileUploadObjects: [ + ...(state.options?.multipleFiles + ? state.fileUploadObjects.filter( + (fileUploadObject) => + !fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ), + ) + : state.fileUploadObjects.filter( + (fileUploadObject) => + fileUploadObject.space !== FileSpace.Local, + )), + { + id: fileUploadObject?.id ?? uniqueID(), + state: FileState.Failed, + space: FileSpace.Local, + prevState: fileUploadObject?.state, + file: action.file, + message: action.message, + }, + ], + } + } + case FileUploadActionType.OverwriteUploadedFiles: + return { + ...state, + fileUploadObjects: [ + ...action.files + .slice(0, state.options?.multipleFiles ? undefined : 1) + .map((file) => { + const existingObjectLocal = state.fileUploadObjects.find( + (fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + file, + FileSpace.Local, + ), + ) + + if (existingObjectLocal) { + return { + id: existingObjectLocal.id, + state: FileState.Uploaded, + space: FileSpace.Remote, + prevState: + existingObjectLocal.state === FileState.Uploaded + ? existingObjectLocal.prevState + : existingObjectLocal.state, + file, + } as const + } + + const existingObjectRemote = state.fileUploadObjects.find( + (fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + file, + FileSpace.Remote, + ), + ) + + if (existingObjectRemote) { + return { + ...existingObjectRemote, + state: FileState.Uploaded, + space: FileSpace.Remote, + file, + } as const + } + + return { + id: uniqueID(), + state: FileState.Uploaded, + space: FileSpace.Remote, + prevState: undefined, + file, + } as const + }), + ...state.fileUploadObjects.filter( + (fileUploadObject) => + fileUploadObject.state !== FileState.Uploaded, + ), + ], + } + case FileUploadActionType.SetFileToUploading: + return { + ...state, + fileUploadObjects: state.fileUploadObjects.map((fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ) + ? { + id: fileUploadObject.id, + state: FileState.Uploading, + space: FileSpace.Local, + prevState: fileUploadObject.state, + file: action.file, + progress: action.progress, + } + : fileUploadObject, + ), + } + case FileUploadActionType.SetFileToUploaded: { + const filteredFileUploadObjects = state.options?.multipleFiles + ? state.fileUploadObjects + : state.fileUploadObjects.filter( + (fileUploadObject) => + fileUploadObject.state !== FileState.Uploaded, + ) + + return { + ...state, + fileUploadObjects: filteredFileUploadObjects.map( + (fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ) + ? 'uploadedFile' in action && action.uploadedFile + ? { + id: fileUploadObject.id, + state: FileState.Uploaded, + space: FileSpace.Remote, + prevState: fileUploadObject.state, + file: action.uploadedFile, + } + : { + id: fileUploadObject.id, + state: FileState.Uploaded, + space: FileSpace.Local, + prevState: fileUploadObject.state, + file: action.file, + } + : fileUploadObject, + ), + } + } + case FileUploadActionType.SetFileToFailed: + return { + ...state, + fileUploadObjects: state.fileUploadObjects.map((fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ) + ? { + id: fileUploadObject.id, + state: FileState.Failed, + space: FileSpace.Local, + prevState: fileUploadObject.state, + file: action.file, + message: action.message, + } + : fileUploadObject, + ), + } + case FileUploadActionType.UpdateFileUploadProgress: + return { + ...state, + fileUploadObjects: state.fileUploadObjects.map((fileUploadObject) => + fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ) + ? { + id: fileUploadObject.id, + state: FileState.Uploading, + space: FileSpace.Local, + prevState: fileUploadObject.state, + file: action.file, + progress: action.progress, + } + : fileUploadObject, + ), + } + case FileUploadActionType.RemoveFileLocal: { + return { + ...state, + fileUploadObjects: state.fileUploadObjects.filter( + (fileUploadObject) => + !fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Local, + ), + ), + } + } + case FileUploadActionType.RemoveFileRemote: { + return { + ...state, + fileUploadObjects: state.fileUploadObjects.filter( + (fileUploadObject) => + !fileUploadObjectIsFileInSpace( + fileUploadObject, + action.file, + FileSpace.Remote, + ), + ), + } + } + } + } +} + +function uniqueID() { + return String(Math.floor(Math.random() * Date.now())) +} diff --git a/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx new file mode 100644 index 00000000..fbfe884b --- /dev/null +++ b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx @@ -0,0 +1,172 @@ +/* eslint-disable react-hooks/rules-of-hooks */ + +import { Canvas, Controls, Description, Story, Title } from '@storybook/blocks' +import { Meta, StoryObj } from '@storybook/react' +import { FC, useCallback, useState } from 'react' +import { ConfirmationDialog, ConfirmationDialogVariant } from '../dialog' +import { DeleteFileAction } from './DeleteFileAction' +import { FileDropZone } from './FileDropZone' +import { FileList } from './FileList' +import { FileListItem } from './FileListItem' +import { FileSpace, FileState, FileUploadObject } from './FileUploadState' +import { FileUploadOnUploadSingle, useFileUpload } from './useFileUpload' +import { useMockedUploadApi } from './useMockedUploadApi' + +export type CustomRemoteFile = { + id: number + name: string + size: number +} + +export type DeleteUploadedFileDialogState = + | { + isOpen: false + } + | { + isOpen: true + fileItem: FileUploadObject<CustomRemoteFile> + } + +type DemoComponentType = FC<{ + autoUpload: boolean +}> +const DemoComponent = () => null + +const meta = { + component: DemoComponent, + parameters: { + docs: { + page: () => ( + <> + <Title /> + <Description /> + <Canvas> + <Story of={WithDeleteDialog} /> + </Canvas> + <Controls of={WithDeleteDialog} /> + </> + ), + }, + }, +} satisfies Meta<DemoComponentType> + +export default meta +type Story = StoryObj<DemoComponentType> + +export const WithDeleteDialog: Story = { + render: () => { + const { remoteFile, mutateRemoteFiles, axiosInstance } = useMockedUploadApi( + { + multipleFiles: false, + initialFile: { id: 1, name: 'file1.pdf', size: 1024 }, + }, + ) + + const onUpload = useCallback<FileUploadOnUploadSingle>( + async (file, { onProgress, onError, onSuccess }) => { + const formData = new FormData() + formData.append('file', file) + + try { + await axiosInstance.post('/upload', formData, { + onUploadProgress: (event) => { + if (event.total) { + onProgress(event.loaded / event.total) + } + }, + }) + onSuccess() + } catch { + onError('Datei konnte nicht hochgeladen werden') + } + + await mutateRemoteFiles() + }, + [axiosInstance, mutateRemoteFiles], + ) + + const { fileUploadObjects, isUploading, removeFile, addFilesToUpload } = + useFileUpload<CustomRemoteFile>({ + remoteFile, + fileUploadObjectIsFile: (fileUploadObject, file) => + fileUploadObject.file.name === file.name, + onUpload, + }) + + const handleDelete = ( + fileUploadObject: FileUploadObject<CustomRemoteFile>, + ) => { + if (fileUploadObject.space === FileSpace.Local) { + removeFile(fileUploadObject.file) + } else { + setDeleteDialogState({ + isOpen: true, + fileItem: fileUploadObject, + }) + } + } + + const onDelete = async ( + fileUploadObject: FileUploadObject<CustomRemoteFile>, + ) => { + setIsDeleting(true) + await axiosInstance.post('/delete') + removeFile(fileUploadObject.file) + setIsDeleting(false) + setDeleteDialogState({ isOpen: false }) + } + + const [deleteDialogState, setDeleteDialogState] = + useState<DeleteUploadedFileDialogState>({ isOpen: false }) + + const [isDeleting, setIsDeleting] = useState(false) + + return ( + <div className="flex flex-col gap-4"> + {fileUploadObjects.length === 0 && ( + <FileDropZone + onSelect={addFilesToUpload} + fileTypes={['pdf']} + maxFileSize={1000000} + /> + )} + {fileUploadObjects.length > 0 && ( + <FileList> + {fileUploadObjects.map((fileUploadObject) => ( + <FileListItem + key={fileUploadObject.id} + fileUploadObject={fileUploadObject} + renderRemoteFileName={(remoteFile) => remoteFile.name} + renderRemoteFileSize={(remoteFile) => remoteFile.size} + disabled={ + isUploading && fileUploadObject.state !== FileState.Uploading + } + fileActions={ + <DeleteFileAction + fileUploadObject={fileUploadObject} + onDelete={handleDelete} + /> + } + /> + ))} + </FileList> + )} + {deleteDialogState.isOpen && ( + <ConfirmationDialog + variant={ConfirmationDialogVariant.Critical} + disableConfirm={isDeleting} + disableDismiss={isDeleting} + onConfirm={() => void onDelete(deleteDialogState.fileItem)} + onDismiss={() => { + setDeleteDialogState({ isOpen: false }) + }} + title="Delete file" + body="Are you sure you want to delete the file?" + confirmButtonText="Delete" + dismissButtonText="Cancel" + /> + )} + </div> + ) + }, +} diff --git a/src/components/files/index.tsx b/src/components/files/index.tsx new file mode 100644 index 00000000..d5ff59dc --- /dev/null +++ b/src/components/files/index.tsx @@ -0,0 +1,6 @@ +export * from './FileDropZone' +export * from './FileList' +export * from './FileListItem' +export * from './FileUploadState' +export * from './useFileDropZone' +export * from './useFileUpload' diff --git a/src/components/files/theme.ts b/src/components/files/theme.ts new file mode 100644 index 00000000..d30fc967 --- /dev/null +++ b/src/components/files/theme.ts @@ -0,0 +1,43 @@ +export default { + fileDropzone: { + container: { + base: 'flex flex-col items-stretch gap-4', + }, + uploadButton: { + base: 'flex items-center gap-4 rounded-xl border border-dashed border-neutral-400 px-6 py-4 hover:bg-primary-50', + fileHovering: 'bg-primary-50', + }, + }, + text: { + base: 'flex-grow flex flex-col items-start gap-1', + underline: 'font-semibold underline', + bold: 'font-semibold', + info: 'text-xs text-neutral-500', + error: 'text-xs text-critical-500', + }, + icon: { + container: { + base: 'rounded-full p-3.5', + error: 'bg-critical-500/10', + default: 'bg-primary-500/10', + success: 'bg-success-500/10', + disabled: 'bg-neutral-500/10', + }, + size: 'h-6 w-6', + error: 'fill-critical-800', + default: 'fill-primary-800', + success: 'fill-success-800', + disabled: 'fill-neutral-800', + }, + fileList: { + container: 'flex flex-col rounded-xl border border-neutral-400', + item: { + container: + 'relative flex justify-between gap-4 border-b border-neutral-200 px-6 py-4 last:border-none', + content: 'flex grow items-center gap-4 truncate', + textContainer: 'flex grow flex-col gap-1 truncate', + progress: 'bg-primary-800 absolute inset-x-0 bottom-0 h-1 rounded-xl', + actions: 'flex items-center gap-2', + }, + }, +} diff --git a/src/components/files/useFileDropZone.ts b/src/components/files/useFileDropZone.ts new file mode 100644 index 00000000..186c725d --- /dev/null +++ b/src/components/files/useFileDropZone.ts @@ -0,0 +1,83 @@ +import { RefObject, useEffect, useRef, useState } from 'react' + +export type UseFileDropZoneProps<InputElement extends HTMLInputElement> = { + disabled?: boolean + inputRef: RefObject<InputElement> +} + +export function useFileDropZone< + Element extends HTMLElement, + InputElement extends HTMLInputElement = HTMLInputElement, +>({ disabled, inputRef }: UseFileDropZoneProps<InputElement>) { + const dropZoneRef = useRef<Element>(null) + + const enterTarget = useRef<EventTarget | null>(null) + const [isFileHovering, setIsFileHovering] = useState(false) + + useEffect(() => { + const element = dropZoneRef.current + + if (!element || disabled) { + return + } + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + enterTarget.current = event.target + setIsFileHovering(true) + } + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + if (enterTarget.current === event.target) { + setIsFileHovering(false) + enterTarget.current = null + } + } + + const handleDragOver = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const handleDrop = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + setIsFileHovering(false) + enterTarget.current = null + + if ( + inputRef.current && + event.dataTransfer?.files && + event.dataTransfer.files.length > 0 + ) { + inputRef.current.files = event.dataTransfer.files + + const changeEvent = new Event('change', { bubbles: true }) + inputRef.current.dispatchEvent(changeEvent) + } + } + + element.addEventListener('dragenter', handleDragEnter) + element.addEventListener('dragleave', handleDragLeave) + element.addEventListener('dragover', handleDragOver) + element.addEventListener('drop', handleDrop) + + return () => { + element.removeEventListener('dragenter', handleDragEnter) + element.removeEventListener('dragleave', handleDragLeave) + element.removeEventListener('dragover', handleDragOver) + element.removeEventListener('drop', handleDrop) + } + }, [disabled, inputRef]) + + return { + dropZoneRef, + isFileHovering, + } +} diff --git a/src/components/files/useFileUpload.tsx b/src/components/files/useFileUpload.tsx new file mode 100644 index 00000000..8d42df75 --- /dev/null +++ b/src/components/files/useFileUpload.tsx @@ -0,0 +1,275 @@ +import { Dispatch, useCallback, useEffect, useMemo, useReducer } from 'react' +import { + FileState, + FileUploadAction, + FileUploadActionType, + FileUploadObject, + getFileUploadReducer, +} from './FileUploadState' + +export type FileUploadOnUploadSingle<TRemoteFile = never> = ( + file: File, + events: { + onProgress: (progress: number | undefined) => void + onError: (errorMessage: string) => void + onSuccess: (uploadedFile?: TRemoteFile) => void + }, +) => Promise<void> | Promise<TRemoteFile> + +export type FileUploadOnUploadMulitple<TRemoteFile = never> = ( + files: File[], + events: { + onProgress: (file: File, progress: number | undefined) => void + onError: (file: File, errorMessage: string) => void + onSuccess: (file: File, uploadedFile?: TRemoteFile) => void + }, +) => Promise<void> | Promise<TRemoteFile> + +export type UseFileUploadPropsSingle<TRemoteFile> = { + fileUploadObjectIsFile: ( + fileUploadObject: FileUploadObject<TRemoteFile>, + file: File | TRemoteFile, + ) => boolean + onUpload: FileUploadOnUploadSingle<TRemoteFile> + autoUpload?: boolean + multipleFiles?: false + remoteFile?: TRemoteFile +} + +export type UseFileUploadReturnSingle<TRemoteFile> = { + fileUploadObjects: FileUploadObject<TRemoteFile>[] + isUploading: boolean + dispatch: Dispatch<FileUploadAction<TRemoteFile>> + removeFile: (file: File | TRemoteFile) => void + triggerUpload: () => Promise<void> + overwriteUploadedFiles: (files: TRemoteFile[]) => void + addFilesToUpload: (file: File) => void +} + +export type UseFileUploadPropsMultiple<TRemoteFile> = { + fileUploadObjectIsFile: ( + fileUploadObject: FileUploadObject<TRemoteFile>, + file: File | TRemoteFile, + ) => boolean + onUpload: FileUploadOnUploadMulitple<TRemoteFile> + autoUpload?: boolean + multipleFiles: true + remoteFiles?: TRemoteFile[] +} + +export type UseFileUploadReturnMultiple<TRemoteFile> = { + fileUploadObjects: FileUploadObject<TRemoteFile>[] + isUploading: boolean + dispatch: Dispatch<FileUploadAction<TRemoteFile>> + removeFile: (file: File | TRemoteFile) => void + triggerUpload: () => Promise<void> + overwriteUploadedFiles: (files: TRemoteFile[]) => void + addFilesToUpload: (files: File[]) => void +} + +export function useFileUpload<TRemoteFile = unknown>( + props: UseFileUploadPropsMultiple<TRemoteFile>, +): UseFileUploadReturnMultiple<TRemoteFile> + +export function useFileUpload<TRemoteFile = unknown>( + props: UseFileUploadPropsSingle<TRemoteFile>, +): UseFileUploadReturnSingle<TRemoteFile> + +export function useFileUpload<TRemoteFile = unknown>({ + remoteFile, + remoteFiles, + fileUploadObjectIsFile, + onUpload, + multipleFiles, + autoUpload = true, +}: { + fileUploadObjectIsFile: ( + fileUploadObject: FileUploadObject<TRemoteFile>, + file: File | TRemoteFile, + ) => boolean + onUpload: + | FileUploadOnUploadSingle<TRemoteFile> + | FileUploadOnUploadMulitple<TRemoteFile> + autoUpload?: boolean + multipleFiles?: boolean + remoteFile?: TRemoteFile + remoteFiles?: TRemoteFile[] +}) { + const [state, dispatch] = useReducer( + getFileUploadReducer<TRemoteFile>({ fileUploadObjectIsFile }), + { + fileUploadObjects: [], + options: { + multipleFiles, + }, + }, + ) + + const removeFile = useCallback( + (file: File | TRemoteFile) => { + if (file instanceof File) { + dispatch({ + type: FileUploadActionType.RemoveFileLocal, + file, + }) + } else { + dispatch({ + type: FileUploadActionType.RemoveFileRemote, + file, + }) + } + }, + [dispatch], + ) + + const overwriteUploadedFiles = useCallback( + (files: TRemoteFile[]) => { + dispatch({ + type: FileUploadActionType.OverwriteUploadedFiles, + files, + }) + }, + [dispatch], + ) + + const addFilesToUpload = useCallback( + (fileOrFiles: File | File[]) => { + if (Array.isArray(fileOrFiles)) { + fileOrFiles.forEach((file) => { + dispatch({ + type: FileUploadActionType.AddFileToUpload, + file, + }) + }) + } else { + dispatch({ + type: FileUploadActionType.AddFileToUpload, + file: fileOrFiles, + }) + } + }, + [dispatch], + ) + + const triggerUpload = useCallback(async () => { + const filesItemsToUpload = state.fileUploadObjects.filter( + ( + fileUploadObject, + ): fileUploadObject is Extract< + FileUploadObject<TRemoteFile>, + { state: FileState.ToUpload } + > => fileUploadObject.state === FileState.ToUpload, + ) + + filesItemsToUpload.forEach(({ file }) => { + dispatch({ + type: FileUploadActionType.SetFileToUploading, + file, + progress: undefined, + }) + }) + + if (multipleFiles) { + await (onUpload as FileUploadOnUploadMulitple<TRemoteFile>)( + filesItemsToUpload.map(({ file }) => file), + { + onProgress: (file, progress) => { + dispatch({ + type: FileUploadActionType.UpdateFileUploadProgress, + file, + progress, + }) + }, + onError: (file, errorMessage) => { + dispatch({ + type: FileUploadActionType.SetFileToFailed, + file, + message: errorMessage, + }) + }, + onSuccess: (file, uploadedFile) => { + dispatch({ + type: FileUploadActionType.SetFileToUploaded, + file, + uploadedFile, + }) + }, + }, + ) + } else { + const fileToUpload = filesItemsToUpload[0] + + if (!fileToUpload) { + return + } + + await (onUpload as FileUploadOnUploadSingle<TRemoteFile>)( + fileToUpload.file, + { + onProgress: (progress) => { + dispatch({ + type: FileUploadActionType.UpdateFileUploadProgress, + file: fileToUpload.file, + progress, + }) + }, + onError: (errorMessage) => { + dispatch({ + type: FileUploadActionType.SetFileToFailed, + file: fileToUpload.file, + message: errorMessage, + }) + }, + onSuccess: (uploadedFile) => { + dispatch({ + type: FileUploadActionType.SetFileToUploaded, + file: fileToUpload.file, + uploadedFile, + }) + }, + }, + ) + } + }, [dispatch, state, onUpload, multipleFiles]) + + useEffect(() => { + if (remoteFiles !== undefined) { + overwriteUploadedFiles(remoteFiles) + } + if (remoteFile !== undefined) { + overwriteUploadedFiles([remoteFile]) + } + }, [overwriteUploadedFiles, remoteFile, remoteFiles]) + + useEffect(() => { + if (!autoUpload) { + return + } + + const hasFilesToUpload = state.fileUploadObjects.some( + (fileUploadObject) => fileUploadObject.state === FileState.ToUpload, + ) + + if (hasFilesToUpload) { + void triggerUpload() + } + }, [state.fileUploadObjects, autoUpload, triggerUpload]) + + const isUploading = useMemo( + () => + state.fileUploadObjects.some( + (fileUploadObject) => fileUploadObject.state === FileState.Uploading, + ), + [state.fileUploadObjects], + ) + + return { + fileUploadObjects: state.fileUploadObjects, + isUploading, + dispatch, + removeFile, + triggerUpload, + overwriteUploadedFiles, + addFilesToUpload, + } +} diff --git a/src/components/files/useMockedUploadApi.ts b/src/components/files/useMockedUploadApi.ts new file mode 100644 index 00000000..297cef64 --- /dev/null +++ b/src/components/files/useMockedUploadApi.ts @@ -0,0 +1,123 @@ +import axios, { AxiosRequestConfig } from 'axios' +import MockAdapter from 'axios-mock-adapter' +import { useCallback, useRef, useState } from 'react' +import { CustomRemoteFile } from './FileUpload.stories' + +export function useMockedUploadApi(options: { + multipleFiles?: false + initialFile: CustomRemoteFile +}): { + remoteFile: CustomRemoteFile + mutateRemoteFiles: () => Promise<void> + axiosInstance: typeof axios +} + +export function useMockedUploadApi(options: { + multipleFiles: true + initialFiles: CustomRemoteFile[] +}): { + remoteFiles: CustomRemoteFile[] + mutateRemoteFiles: () => Promise<void> + axiosInstance: typeof axios +} + +export function useMockedUploadApi({ + multipleFiles, + initialFile, + initialFiles, +}: { + multipleFiles?: boolean + initialFile?: CustomRemoteFile + initialFiles?: CustomRemoteFile[] +}) { + const serverFiles = useRef<CustomRemoteFile[]>( + initialFiles ?? (initialFile ? [initialFile] : []), + ) + + const axiosInstance = axios.create() + + const mock = new MockAdapter(axiosInstance) + + const randomNumber = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min + + const sleep = (value: number) => + new Promise((resolve) => setTimeout(resolve, value)) + + mock + .onPost('/upload') + .reply(async ({ data, onUploadProgress }: AxiosRequestConfig<FormData>) => { + const total = 1024 + const speed = randomNumber(100, 400) + + await sleep(speed) + + const fileName = (data?.get('file') as File | undefined)?.name + + for (const progress of [ + 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, + ]) { + onUploadProgress?.({ + loaded: total * progress, + bytes: progress * total, + total, + }) + await sleep(speed) + + if (fileName === 'error.pdf' && progress > 0.5) { + return [500, null] + } + } + + serverFiles.current = [ + ...(multipleFiles + ? serverFiles.current.filter((file) => file.name !== fileName) + : []), + ...Array.from(data?.values() as IterableIterator<File>).map( + (file) => + ({ + id: 1, + name: file.name, + size: file.size, + }) satisfies CustomRemoteFile, + ), + ].sort((a, b) => a.name.localeCompare(b.name)) + + return [200, null] + }) + .onPost('/delete') + .reply(async ({ data }: AxiosRequestConfig<CustomRemoteFile>) => { + await sleep(1000) + + serverFiles.current = serverFiles.current.filter( + (file) => file.id !== data?.id, + ) + + return [200, null] + }) + + const [remoteFiles, setRemoteFiles] = useState<CustomRemoteFile[]>( + serverFiles.current, + ) + + const mutateRemoteFiles = useCallback(async () => { + await sleep(500) + setRemoteFiles( + JSON.parse( + JSON.stringify(serverFiles.current), + ) as typeof serverFiles.current, + ) + }, []) + + return multipleFiles + ? { + remoteFiles, + mutateRemoteFiles, + axiosInstance, + } + : { + remoteFile: remoteFiles[0], + mutateRemoteFiles, + axiosInstance, + } +} diff --git a/src/components/files/utils.ts b/src/components/files/utils.ts new file mode 100644 index 00000000..e6b211ed --- /dev/null +++ b/src/components/files/utils.ts @@ -0,0 +1,64 @@ +import { useCallback } from 'react' + +export function getFileType(file: File) { + const splitName = file.name.split('.') + + if (splitName.length < 1) { + return '' + } + + return splitName.pop()?.toUpperCase() ?? '' +} + +const UNITS = [ + 'byte', + 'kilobyte', + 'megabyte', + 'gigabyte', + 'terabyte', + 'petabyte', +] as const + +export function useFormattedBytes() { + return useCallback((value: number, unit: (typeof UNITS)[number]) => { + const formattedValue = value.toFixed(1) + switch (unit) { + case 'byte': + return `${formattedValue} B` + case 'kilobyte': + return `${formattedValue} KB` + case 'megabyte': + return `${formattedValue} MB` + case 'gigabyte': + return `${formattedValue} GB` + case 'terabyte': + return `${formattedValue} TB` + case 'petabyte': + return `${formattedValue} PB` + } + }, []) +} + +const BYTES_PER_KB = 1_000 + +export function useHumanReadableFileSize() { + const formatBytes = useFormattedBytes() + + return useCallback( + (bytes: number) => { + if (bytes < 0) { + return undefined + } + + let unitIndex = 0 + while (bytes >= Number(BYTES_PER_KB) && unitIndex < UNITS.length - 1) { + bytes /= BYTES_PER_KB + unitIndex++ + } + + const unit = UNITS[unitIndex] + return unit ? formatBytes(bytes, unit) : undefined + }, + [formatBytes], + ) +} diff --git a/src/components/loading/IconSpinner.tsx b/src/components/loading/IconSpinner.tsx new file mode 100644 index 00000000..b2283de0 --- /dev/null +++ b/src/components/loading/IconSpinner.tsx @@ -0,0 +1,37 @@ +import classNames from 'classnames' +import React from 'react' +import { useTheme } from '../../framework' +import { IconProps } from '../types' + +export const IconSpinner: React.FC<IconProps> = ({ + title, + currentColor, + ...props +}) => { + const { loading } = useTheme() + return ( + <svg + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + aria-label={title} + {...props} + > + {title && <title>{title}} + + + ) +} diff --git a/src/components/loading/theme.ts b/src/components/loading/theme.ts index 1df4b6c7..49af3a8c 100644 --- a/src/components/loading/theme.ts +++ b/src/components/loading/theme.ts @@ -11,4 +11,9 @@ export default { base: 'p-5 w-full', }, }, + iconSpinner: { + base: 'animate-spinner', + currentColor: 'stroke-current', + defaultColor: 'stroke-primary-500', + }, } diff --git a/src/components/types.ts b/src/components/types.ts index 9d85a2b4..e508a046 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -66,4 +66,7 @@ export type UseSearchQuery = { actions: { search: (query: string) => void; clear: () => void } } -export type IconProps = React.SVGProps +export type IconProps = { + title?: string + currentColor?: boolean +} & React.SVGProps diff --git a/src/framework/internationalization/defaultMessages.en.ts b/src/framework/internationalization/defaultMessages.en.ts index 5299f5b1..aad4209e 100644 --- a/src/framework/internationalization/defaultMessages.en.ts +++ b/src/framework/internationalization/defaultMessages.en.ts @@ -22,4 +22,14 @@ export const defaultMessages = { 'error.api': 'The backend failed to handle the request.', 'form.saved': 'Saved successfully', 'form.submit': 'Save', + 'files.dropzone.description.part2.text': ' or drag here', + 'files.dropzone.description.part1.text': 'Select file', + 'files.dropzone.select.button': 'upload document', + 'files.dropzone.allowedFileTypes.text': 'Allowed files:', + 'files.item.deleteConfirmation.part1.text': 'Do you want to', + 'files.item.deleteConfirmation.part2.text': 'delete?', + 'files.item.uploading': 'Uploading file...', + 'files.dropzone.maxFileSize.text': 'max.', + 'files.action.delete': 'Delete', + 'files.action.download': 'Download', } diff --git a/src/framework/theme/theme.ts b/src/framework/theme/theme.ts index 5435997a..a4006e8f 100644 --- a/src/framework/theme/theme.ts +++ b/src/framework/theme/theme.ts @@ -6,6 +6,7 @@ import breadcrumbs from '../../components/breadcrumbs/theme' import button from '../../components/button/theme' import content from '../../components/content/theme' import dialog from '../../components/dialog/theme' +import files from '../../components/files/theme' import form from '../../components/form/theme' import header from '../../components/header/theme' import link from '../../components/link/theme' @@ -30,4 +31,5 @@ export const defaultTheme = { menu, pagination, tabs, + files, } diff --git a/tailwind-preset.ts b/tailwind-preset.ts index 03f0266a..7fc724a2 100644 --- a/tailwind-preset.ts +++ b/tailwind-preset.ts @@ -102,6 +102,29 @@ export default { lineHeight: { 12: '3rem', }, + keyframes: { + spinner: { + '0%': { + strokeDasharray: '0 100', + strokeDashoffset: '25', + }, + '50%': { + strokeDasharray: `100 0`, + strokeDashoffset: '25', + }, + '50.1%': { + strokeDasharray: '100 0', + strokeDashoffset: '125', + }, + '100%': { + strokeDasharray: '0 100', + strokeDashoffset: '25', + }, + }, + }, + animation: { + spinner: 'spinner 2s cubic-bezier(0.88, 0, 0.58, 1) infinite', + }, }, }, } satisfies Config From 6575fafc711ff80f8fd9e0193fec5301ddc31a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Mon, 2 Sep 2024 08:08:38 +0200 Subject: [PATCH 05/14] remove dialog stories from file upload stories --- src/components/files/FileUpload.stories.tsx | 103 -------------------- 1 file changed, 103 deletions(-) diff --git a/src/components/files/FileUpload.stories.tsx b/src/components/files/FileUpload.stories.tsx index 004403af..f71a671d 100644 --- a/src/components/files/FileUpload.stories.tsx +++ b/src/components/files/FileUpload.stories.tsx @@ -64,11 +64,6 @@ const meta = { - With delete dialog - - - - ), }, @@ -275,101 +270,3 @@ export const Multiple: Story = { ) }, } - -export const WithDeleteDialog: Story = { - render: ({ autoUpload }) => { - const { remoteFile, mutateRemoteFiles, axiosInstance } = useMockedUploadApi( - { - multipleFiles: false, - initialFile: { id: 1, name: 'file1.pdf', size: 1024 }, - }, - ) - - const onUpload = useCallback( - async (file, { onProgress, onError, onSuccess }) => { - const formData = new FormData() - formData.append('file', file) - - try { - await axiosInstance.post('/upload', formData, { - onUploadProgress: (event) => { - if (event.total) { - onProgress(event.loaded / event.total) - } - }, - }) - onSuccess() - } catch { - onError('Datei konnte nicht hochgeladen werden') - } - - await mutateRemoteFiles() - }, - [axiosInstance, mutateRemoteFiles], - ) - - const { - fileUploadObjects, - isUploading, - triggerUpload, - removeFile, - addFilesToUpload, - } = useFileUpload({ - remoteFile, - fileUploadObjectIsFile: (fileUploadObject, file) => - fileUploadObject.file.name === file.name, - onUpload, - autoUpload, - }) - - const handleDelete = async ( - fileUploadObject: FileUploadObject, - ) => { - if (fileUploadObject.space === FileSpace.Local) { - removeFile(fileUploadObject.file) - } else { - await axiosInstance.post('/delete') - removeFile(fileUploadObject.file) - } - } - - return ( -
- {fileUploadObjects.length === 0 && ( - - )} - {fileUploadObjects.length > 0 && ( - - {fileUploadObjects.map((fileUploadObject) => ( - remoteFile.name} - renderRemoteFileSize={(remoteFile) => remoteFile.size} - disabled={ - isUploading && fileUploadObject.state !== FileState.Uploading - } - fileActions={ - - } - /> - ))} - - )} - - {!autoUpload && ( - - )} -
- ) - }, -} From 81e77ddb5151646bef9a7ff2ee77a46131db50a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Mon, 2 Sep 2024 08:11:03 +0200 Subject: [PATCH 06/14] replace german text --- src/components/files/FileUpload.stories.tsx | 4 ++-- .../files/FileUploadWithDeleteDialogAction.stories.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/files/FileUpload.stories.tsx b/src/components/files/FileUpload.stories.tsx index f71a671d..2740db45 100644 --- a/src/components/files/FileUpload.stories.tsx +++ b/src/components/files/FileUpload.stories.tsx @@ -97,7 +97,7 @@ export const Single: Story = { }) onSuccess() } catch { - onError('Datei konnte nicht hochgeladen werden') + onError('File could not be uploaded') } await mutateRemoteFiles() @@ -199,7 +199,7 @@ export const Multiple: Story = { }) onSuccess(file) } catch { - onError(file, 'Datei konnte nicht hochgeladen werden') + onError(file, 'File could not be uploaded') } }), ) diff --git a/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx index fbfe884b..65cbda9d 100644 --- a/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx +++ b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx @@ -77,7 +77,7 @@ export const WithDeleteDialog: Story = { }) onSuccess() } catch { - onError('Datei konnte nicht hochgeladen werden') + onError('File upload failed') } await mutateRemoteFiles() From c5584f34ff1943f4f0b0bb5c338f3ead9c533a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Mon, 2 Sep 2024 08:56:50 +0200 Subject: [PATCH 07/14] add overflow hidden to list container --- src/components/files/theme.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/files/theme.ts b/src/components/files/theme.ts index d30fc967..24639b1d 100644 --- a/src/components/files/theme.ts +++ b/src/components/files/theme.ts @@ -30,7 +30,8 @@ export default { disabled: 'fill-neutral-800', }, fileList: { - container: 'flex flex-col rounded-xl border border-neutral-400', + container: + 'flex flex-col rounded-xl border border-neutral-400 overflow-hidden', item: { container: 'relative flex justify-between gap-4 border-b border-neutral-200 px-6 py-4 last:border-none', From 7c7e4e60b9197d655d1ca465834c780bf58da16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 3 Sep 2024 10:03:18 +0200 Subject: [PATCH 08/14] add download method example --- src/components/button/theme.ts | 4 -- src/components/files/DeleteFileAction.tsx | 38 ++++++++-------- src/components/files/DownloadFileAction.tsx | 37 ++++++++++++---- src/components/files/FileListItem.tsx | 42 +++--------------- src/components/files/FileUpload.stories.tsx | 43 ++++++++++++++++--- src/components/files/FileUploadContainer.tsx | 18 ++++++++ ...leUploadWithDeleteDialogAction.stories.tsx | 18 +++++--- .../ResponsiveButtonIcon.tsx | 18 +++++--- src/components/files/index.tsx | 2 + src/components/files/theme.ts | 7 +++ src/components/files/useMockedUploadApi.ts | 27 ++++++++++++ src/components/loading/IconSpinner.tsx | 35 +++++---------- src/components/loading/theme.ts | 4 +- src/components/types.ts | 5 +-- 14 files changed, 180 insertions(+), 118 deletions(-) create mode 100644 src/components/files/FileUploadContainer.tsx rename src/components/{button => files}/ResponsiveButtonIcon.tsx (67%) diff --git a/src/components/button/theme.ts b/src/components/button/theme.ts index 157b0b4e..bf70370e 100644 --- a/src/components/button/theme.ts +++ b/src/components/button/theme.ts @@ -77,10 +77,6 @@ export default { }, }, }, - buttonIconResponsive: { - button: 'hidden md:flex', - buttonIcon: 'md:hidden', - }, modeVariantTone: { [Mode.Light]: { [ButtonVariant.Solid]: { diff --git a/src/components/files/DeleteFileAction.tsx b/src/components/files/DeleteFileAction.tsx index 40c1f350..5714267f 100644 --- a/src/components/files/DeleteFileAction.tsx +++ b/src/components/files/DeleteFileAction.tsx @@ -2,10 +2,10 @@ import IconDelete from '@aboutbits/react-material-icons/dist/IconDelete' import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' -import { ResponsiveButtonIcon } from '../button/ResponsiveButtonIcon' import { IconSpinner } from '../loading/IconSpinner' import { Tone } from '../types' import { FileUploadObject } from './FileUploadState' +import { ResponsiveButtonIcon } from './ResponsiveButtonIcon' type DeleteFileActionProps = { fileUploadObject: FileUploadObject @@ -21,24 +21,22 @@ export function DeleteFileAction({ const [isDeleting, setIsDeleting] = useState(false) const { messages } = useInternationalization() return ( - <> - { - setIsDeleting(true) - Promise.resolve(onDelete(fileUploadObject)) - .then(() => { - setIsDeleting(false) - }) - .catch(() => { - setIsDeleting(false) - }) - }} - icon={isDeleting ? IconSpinner : IconDelete} - label={messages['files.action.delete']} - /> - + { + setIsDeleting(true) + Promise.resolve(onDelete(fileUploadObject)) + .then(() => { + setIsDeleting(false) + }) + .catch(() => { + setIsDeleting(false) + }) + }} + icon={isDeleting ? IconSpinner : IconDelete} + label={messages['files.action.delete']} + /> ) } diff --git a/src/components/files/DownloadFileAction.tsx b/src/components/files/DownloadFileAction.tsx index 71509167..33502bd5 100644 --- a/src/components/files/DownloadFileAction.tsx +++ b/src/components/files/DownloadFileAction.tsx @@ -1,22 +1,41 @@ import IconDownload from '@aboutbits/react-material-icons/dist/IconDownload' +import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' -import { - ResponsiveButtonIcon, - ResponsiveButtonIconProps, -} from '../button/ResponsiveButtonIcon' +import { IconSpinner } from '../loading/IconSpinner' import { Tone } from '../types' +import { FileUploadObject } from './FileUploadState' +import { ResponsiveButtonIcon } from './ResponsiveButtonIcon' -export function DownloadFileAction({ - onClick, -}: Pick) { +type DownloadFileActionProps = { + fileUploadObject: FileUploadObject + onDownload: ( + fileUploadObject: FileUploadObject, + ) => void | Promise +} + +export function DownloadFileAction({ + onDownload, + fileUploadObject, +}: DownloadFileActionProps) { const { messages } = useInternationalization() + const [isDownloading, setIsDownloading] = useState(false) return ( { + setIsDownloading(true) + Promise.resolve(onDownload(fileUploadObject)) + .then(() => { + setIsDownloading(false) + }) + .catch(() => { + setIsDownloading(false) + }) + }} label={messages['files.action.download']} /> ) diff --git a/src/components/files/FileListItem.tsx b/src/components/files/FileListItem.tsx index 5d93582c..5f8e587d 100644 --- a/src/components/files/FileListItem.tsx +++ b/src/components/files/FileListItem.tsx @@ -28,6 +28,7 @@ export function FileListItem({ }: FileListItemProps) { const [recentlyUploaded, setRecentlyUploaded] = useState() const formatFileSize = useHumanReadableFileSize() + const { messages } = useInternationalization() const fileSize = fileUploadObject.space === FileSpace.Remote @@ -36,6 +37,8 @@ export function FileListItem({ const formattedFileSize = fileSize ? formatFileSize(fileSize) : undefined + const fileState = fileUploadObject.state + const name = fileUploadObject.space === FileSpace.Remote ? renderRemoteFileName(fileUploadObject.file) @@ -57,39 +60,6 @@ export function FileListItem({ return (
- -
- ) -} - -function FileListItemContent({ - fileName, - fileSize, - fileUploadObject, - recentlyUploaded, - disabled, - fileActions, -}: { - fileName: string - fileSize?: string - fileUploadObject: FileUploadObject - recentlyUploaded?: boolean - disabled?: boolean - fileActions?: ReactNode -}) { - const { files } = useTheme() - const { messages } = useInternationalization() - - const fileState = fileUploadObject.state - return ( - <>
{fileUploadObject.state === FileState.Uploading ? (
({
)}
-
{fileName}
+
{name}
({ : fileState === FileState.Uploading ? messages['files.item.uploading'] : fileState === FileState.Uploaded && fileSize - ? fileSize + ? formattedFileSize : ''}
@@ -161,6 +131,6 @@ function FileListItemContent({ style={{ width: `${fileUploadObject.progress * 100}%` }} /> )} - +
) } diff --git a/src/components/files/FileUpload.stories.tsx b/src/components/files/FileUpload.stories.tsx index 2740db45..44d40b1f 100644 --- a/src/components/files/FileUpload.stories.tsx +++ b/src/components/files/FileUpload.stories.tsx @@ -9,6 +9,7 @@ import { Title, } from '@storybook/blocks' import { Meta, StoryObj } from '@storybook/react' +import { AxiosResponse } from 'axios' import { FC, useCallback } from 'react' import { Button } from '../button' import { Size } from '../types' @@ -17,6 +18,7 @@ import { DownloadFileAction } from './DownloadFileAction' import { FileDropZone } from './FileDropZone' import { FileList } from './FileList' import { FileListItem } from './FileListItem' +import { FileUploadContainer } from './FileUploadContainer' import { FileSpace, FileState, FileUploadObject } from './FileUploadState' import { FileUploadOnUploadMulitple, @@ -131,7 +133,7 @@ export const Single: Story = { } return ( -
+ {fileUploadObjects.length === 0 && ( )} -
+ ) }, } @@ -222,15 +224,41 @@ export const Multiple: Story = { fileUploadObject: FileUploadObject, ) => { if (fileUploadObject.space === FileSpace.Local) { - removeFile(fileUploadObject.file) // Move this logic into FileListItem + removeFile(fileUploadObject.file) } else { await axiosInstance.post('/delete') removeFile(fileUploadObject.file) } } + const handleDownload = async ( + fileUploadObject: FileUploadObject, + ): Promise => { + try { + const fileName: string = fileUploadObject.file.name + const response: AxiosResponse = await axiosInstance.get( + '/download', + { + params: { fileName }, + responseType: 'blob', + }, + ) + + const downloadUrl: string = URL.createObjectURL(response.data) + const a: HTMLAnchorElement = document.createElement('a') + a.href = downloadUrl + a.download = fileName + document.body.appendChild(a) + a.click() + a.remove() + } catch (error) { + // Handle error here + console.error('Download failed:', error) + } + } + return ( -
+ remoteFile.size} fileActions={ <> - void triggerUpload()} /> + )} -
+ ) }, } diff --git a/src/components/files/FileUploadContainer.tsx b/src/components/files/FileUploadContainer.tsx new file mode 100644 index 00000000..5f599b2d --- /dev/null +++ b/src/components/files/FileUploadContainer.tsx @@ -0,0 +1,18 @@ +import classNames from 'classnames' +import { ComponentProps, PropsWithChildren } from 'react' +import { useTheme } from '../../framework' + +type FileUploadContainerProps = PropsWithChildren> + +export function FileUploadContainer({ + className, + children, + ...props +}: FileUploadContainerProps) { + const { files } = useTheme() + return ( +
+ {children} +
+ ) +} diff --git a/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx index 65cbda9d..3f51bf61 100644 --- a/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx +++ b/src/components/files/FileUploadWithDeleteDialogAction.stories.tsx @@ -8,6 +8,7 @@ import { DeleteFileAction } from './DeleteFileAction' import { FileDropZone } from './FileDropZone' import { FileList } from './FileList' import { FileListItem } from './FileListItem' +import { FileUploadContainer } from './FileUploadContainer' import { FileSpace, FileState, FileUploadObject } from './FileUploadState' import { FileUploadOnUploadSingle, useFileUpload } from './useFileUpload' import { useMockedUploadApi } from './useMockedUploadApi' @@ -110,10 +111,15 @@ export const WithDeleteDialog: Story = { fileUploadObject: FileUploadObject, ) => { setIsDeleting(true) - await axiosInstance.post('/delete') - removeFile(fileUploadObject.file) - setIsDeleting(false) - setDeleteDialogState({ isOpen: false }) + try { + await axiosInstance.post('/delete') + removeFile(fileUploadObject.file) + } catch { + // handle error here + } finally { + setIsDeleting(false) + setDeleteDialogState({ isOpen: false }) + } } const [deleteDialogState, setDeleteDialogState] = @@ -122,7 +128,7 @@ export const WithDeleteDialog: Story = { const [isDeleting, setIsDeleting] = useState(false) return ( -
+ {fileUploadObjects.length === 0 && ( )} -
+ ) }, } diff --git a/src/components/button/ResponsiveButtonIcon.tsx b/src/components/files/ResponsiveButtonIcon.tsx similarity index 67% rename from src/components/button/ResponsiveButtonIcon.tsx rename to src/components/files/ResponsiveButtonIcon.tsx index a77c8816..415d8581 100644 --- a/src/components/button/ResponsiveButtonIcon.tsx +++ b/src/components/files/ResponsiveButtonIcon.tsx @@ -1,15 +1,15 @@ import classNames from 'classnames' -import { ComponentType, ReactNode } from 'react' +import { ComponentType } from 'react' import { useTheme } from '../../framework' +import { Button, ButtonProps } from '../button/Button' +import { ButtonIcon, ButtonIconProps } from '../button/ButtonIcon' import { IconProps } from '../types' -import { Button, ButtonProps } from './Button' -import { ButtonIcon, ButtonIconProps } from './ButtonIcon' export type ResponsiveButtonIconProps = Omit< ButtonProps & ButtonIconProps, 'ref' | 'children' | 'icon' | 'iconStart' | 'iconEnd' | 'label' > & { - label: ReactNode + label: string } & ( | { icon: ComponentType @@ -28,22 +28,26 @@ export function ResponsiveButtonIcon({ iconEnd, ...props }: ResponsiveButtonIconProps) { - const { button } = useTheme() + const { files } = useTheme() return ( <> diff --git a/src/components/files/index.tsx b/src/components/files/index.tsx index d5ff59dc..60725b1c 100644 --- a/src/components/files/index.tsx +++ b/src/components/files/index.tsx @@ -4,3 +4,5 @@ export * from './FileListItem' export * from './FileUploadState' export * from './useFileDropZone' export * from './useFileUpload' +export * from './DeleteFileAction' +export * from './DownloadFileAction' diff --git a/src/components/files/theme.ts b/src/components/files/theme.ts index 24639b1d..2f8efe45 100644 --- a/src/components/files/theme.ts +++ b/src/components/files/theme.ts @@ -1,4 +1,5 @@ export default { + container: 'flex flex-col gap-4', fileDropzone: { container: { base: 'flex flex-col items-stretch gap-4', @@ -41,4 +42,10 @@ export default { actions: 'flex items-center gap-2', }, }, + action: { + buttonIconResponsive: { + button: 'max-md:hidden', + buttonIcon: 'md:hidden', + }, + }, } diff --git a/src/components/files/useMockedUploadApi.ts b/src/components/files/useMockedUploadApi.ts index 297cef64..ee50c110 100644 --- a/src/components/files/useMockedUploadApi.ts +++ b/src/components/files/useMockedUploadApi.ts @@ -96,6 +96,33 @@ export function useMockedUploadApi({ return [200, null] }) + mock + .onGet('/download') + .reply( + async (config: AxiosRequestConfig): Promise<[number, Blob | null]> => { + // Ensure that params exist and have the correct type + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!config.params || typeof config.params.fileName !== 'string') { + return [400, null] + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { fileName } = config.params + + await sleep(1000) + const file = serverFiles.current.find((f) => f.name === fileName) + + if (!file) { + return [404, null] + } + + const fileContent = new Blob([`Content of ${file.name}`], { + type: 'text/plain', + }) + + return [200, fileContent] + }, + ) const [remoteFiles, setRemoteFiles] = useState( serverFiles.current, ) diff --git a/src/components/loading/IconSpinner.tsx b/src/components/loading/IconSpinner.tsx index b2283de0..aa836e75 100644 --- a/src/components/loading/IconSpinner.tsx +++ b/src/components/loading/IconSpinner.tsx @@ -1,37 +1,24 @@ import classNames from 'classnames' -import React from 'react' import { useTheme } from '../../framework' import { IconProps } from '../types' -export const IconSpinner: React.FC = ({ - title, - currentColor, - ...props -}) => { +export function IconSpinner({ className, ...props }: IconProps) { const { loading } = useTheme() return ( - {title && {title}} - + ) } diff --git a/src/components/loading/theme.ts b/src/components/loading/theme.ts index 49af3a8c..5ce2a2f7 100644 --- a/src/components/loading/theme.ts +++ b/src/components/loading/theme.ts @@ -12,8 +12,8 @@ export default { }, }, iconSpinner: { - base: 'animate-spinner', + base: 'animate-spin', currentColor: 'stroke-current', - defaultColor: 'stroke-primary-500', + defaultColor: 'stroke-primary-800', }, } diff --git a/src/components/types.ts b/src/components/types.ts index e508a046..9d85a2b4 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -66,7 +66,4 @@ export type UseSearchQuery = { actions: { search: (query: string) => void; clear: () => void } } -export type IconProps = { - title?: string - currentColor?: boolean -} & React.SVGProps +export type IconProps = React.SVGProps From 4671f161ed97315e1d941286fe190f0feb5eae65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 3 Sep 2024 16:06:24 +0200 Subject: [PATCH 09/14] use symbols instead of icons --- package-lock.json | 5 ++--- package.json | 6 +++--- src/components/alert/Alert.stories.tsx | 6 +++--- src/components/alert/ConvenientAlerts.tsx | 6 +++--- src/components/button/Button.stories.tsx | 4 ++-- src/components/button/ButtonIcon.stories.tsx | 10 +++++----- src/components/button/ButtonIconLink.stories.tsx | 4 ++-- src/components/button/ButtonLink.stories.tsx | 4 ++-- src/components/button/SubmitButton.stories.tsx | 4 ++-- .../content/ContentMessage/ContentMessage.stories.tsx | 6 +++--- .../dialog/Dialog/ConvenientDialogContentMessage.tsx | 4 ++-- .../dialog/DialogHeader/DialogHeaderCloseAction.tsx | 2 +- .../dialog/DialogItem/DialogListItemButton.tsx | 2 +- src/components/files/DeleteFileAction.tsx | 2 +- src/components/files/DownloadFileAction.tsx | 2 +- src/components/files/FileDropZone.tsx | 2 +- src/components/files/FileListItem.tsx | 6 +++--- src/components/files/theme.ts | 2 +- src/components/form/InputField.stories.tsx | 4 ++-- src/components/form/SearchField.tsx | 2 +- src/components/form/primitive/Input.stories.tsx | 4 ++-- .../form/primitive/InputIcon/InputIcon.stories.tsx | 4 ++-- src/components/header/actions/HeaderBackAction.tsx | 2 +- src/components/header/actions/HeaderCloseAction.tsx | 2 +- src/components/menu/Menu.stories.tsx | 4 ++-- .../pagination/PaginationPreviousNextContent.tsx | 4 ++-- .../react-hook-form/DateFormField.stories.tsx | 4 ++-- .../react-hook-form/FormSubmitFeedback.stories.mdx | 4 ++-- src/components/react-hook-form/FormSubmitFeedback.tsx | 2 +- .../react-hook-form/InputFormField.stories.tsx | 4 ++-- .../react-hook-form/NumberFormField.stories.tsx | 4 ++-- src/components/react-hook-form/SearchFormField.tsx | 2 +- .../SelectItemFormField/SelectItemFormFieldDialog.tsx | 2 +- .../SelectItemFormField/SelectItemFormFieldInput.tsx | 4 ++-- .../Section/ConvenientSectionContentMessage.tsx | 4 ++-- src/components/section/SectionItem/SectionListItem.tsx | 2 +- src/examples/Form.stories.tsx | 2 +- src/examples/List.stories.tsx | 2 +- 38 files changed, 69 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76d0197c..34de138e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.8.2", "dependencies": { "@aboutbits/pagination": "^2.0.2", - "@aboutbits/react-material-icons": "^1.2.5", + "@aboutbits/react-material-icons": "github:aboutbits/react-material-icons#update-to-symbols", "@aboutbits/react-toolbox": "^0.2.5", "@floating-ui/react": "^0.26.10", "@headlessui/react": "^1.7.18", @@ -170,8 +170,7 @@ }, "node_modules/@aboutbits/react-material-icons": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@aboutbits/react-material-icons/-/react-material-icons-1.2.5.tgz", - "integrity": "sha512-LX2BSqAPpE5e+caB6dPxahpBqxxkBi/3BMdQJfjEoHpSG5jx1GS4JXFCONuxejlu+tS2S8swYjDTl//B9dbnwQ==", + "resolved": "git+ssh://git@github.com/aboutbits/react-material-icons.git#40fd62e527efaea680b39462fcda610b9048741c", "engines": { "node": ">=16", "npm": ">=8" diff --git a/package.json b/package.json index dba686ff..3b53c3f1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@aboutbits/pagination": "^2.0.2", - "@aboutbits/react-material-icons": "^1.2.5", + "@aboutbits/react-material-icons": "github:aboutbits/react-material-icons#update-to-symbols", "@aboutbits/react-toolbox": "^0.2.5", "@floating-ui/react": "^0.26.10", "@headlessui/react": "^1.7.18", @@ -89,6 +89,7 @@ "@typescript-eslint/eslint-plugin": "^7.4.0", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", + "axios-mock-adapter": "^1.22.0", "cssnano": "^6.1.1", "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", @@ -113,8 +114,7 @@ "typescript": "^5.4.3", "vite": "^4.4.3", "vitest": "^0.33.0", - "zod": "^3.22.4", - "axios-mock-adapter": "^1.22.0" + "zod": "^3.22.4" }, "peerDependencies": { "@aboutbits/react-pagination": "^3.0.3", diff --git a/src/components/alert/Alert.stories.tsx b/src/components/alert/Alert.stories.tsx index dd65dfb5..124bfa61 100644 --- a/src/components/alert/Alert.stories.tsx +++ b/src/components/alert/Alert.stories.tsx @@ -1,6 +1,6 @@ -import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import IconCheck from '@aboutbits/react-material-icons/dist/IconCheckOutlined' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoIOutlined' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarningOutlined' import { Controls, Markdown, diff --git a/src/components/alert/ConvenientAlerts.tsx b/src/components/alert/ConvenientAlerts.tsx index 00646b9d..8e0cf59c 100644 --- a/src/components/alert/ConvenientAlerts.tsx +++ b/src/components/alert/ConvenientAlerts.tsx @@ -1,6 +1,6 @@ -import IconCheck from '@aboutbits/react-material-icons/dist/IconCheck' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import IconCheck from '@aboutbits/react-material-icons/dist/IconCheckRoundedFilled' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRoundedFilled' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarningRoundedFilled' import { ReactElement } from 'react' import { Tone } from '../types' import { Alert } from './Alert' diff --git a/src/components/button/Button.stories.tsx b/src/components/button/Button.stories.tsx index def02892..6a82c348 100644 --- a/src/components/button/Button.stories.tsx +++ b/src/components/button/Button.stories.tsx @@ -1,5 +1,5 @@ -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAddRounded' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRounded' import { Controls, Description, diff --git a/src/components/button/ButtonIcon.stories.tsx b/src/components/button/ButtonIcon.stories.tsx index 88e08138..271b319b 100644 --- a/src/components/button/ButtonIcon.stories.tsx +++ b/src/components/button/ButtonIcon.stories.tsx @@ -1,5 +1,5 @@ -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconAddRounded from '@aboutbits/react-material-icons/dist/IconAddRounded' +import IconInfoRounded from '@aboutbits/react-material-icons/dist/IconInfoRounded' import { Controls, Description, @@ -19,8 +19,8 @@ import { ButtonVariant } from './types' const icons = { options: ['Info', 'Add'], mapping: { - Info: IconInfo, - Add: IconAdd, + Info: IconInfoRounded, + Add: IconAddRounded, }, } @@ -32,7 +32,7 @@ const meta = { variant: ButtonVariant.Solid, size: Size.Md, tone: Tone.Primary, - icon: IconAdd, + icon: IconAddRounded, }, argTypes: { icon: icons, diff --git a/src/components/button/ButtonIconLink.stories.tsx b/src/components/button/ButtonIconLink.stories.tsx index 1503005a..561d6d94 100644 --- a/src/components/button/ButtonIconLink.stories.tsx +++ b/src/components/button/ButtonIconLink.stories.tsx @@ -1,5 +1,5 @@ -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAddRounded' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRounded' import { Controls, Description, diff --git a/src/components/button/ButtonLink.stories.tsx b/src/components/button/ButtonLink.stories.tsx index c190c93f..5d6d46fc 100644 --- a/src/components/button/ButtonLink.stories.tsx +++ b/src/components/button/ButtonLink.stories.tsx @@ -1,5 +1,5 @@ -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAddRounded' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRounded' import { Controls, Description, diff --git a/src/components/button/SubmitButton.stories.tsx b/src/components/button/SubmitButton.stories.tsx index fa2ecbb2..84d0dba5 100644 --- a/src/components/button/SubmitButton.stories.tsx +++ b/src/components/button/SubmitButton.stories.tsx @@ -1,5 +1,5 @@ -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAddRounded' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRounded' import { Controls, Description, diff --git a/src/components/content/ContentMessage/ContentMessage.stories.tsx b/src/components/content/ContentMessage/ContentMessage.stories.tsx index a9840315..0757f411 100644 --- a/src/components/content/ContentMessage/ContentMessage.stories.tsx +++ b/src/components/content/ContentMessage/ContentMessage.stories.tsx @@ -1,6 +1,6 @@ -import IconError from '@aboutbits/react-material-icons/dist/IconError' -import IconInfo from '@aboutbits/react-material-icons/dist/IconInfo' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import IconError from '@aboutbits/react-material-icons/dist/IconErrorRounded' +import IconInfo from '@aboutbits/react-material-icons/dist/IconInfoRounded' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarningRounded' import { Controls, Description, diff --git a/src/components/dialog/Dialog/ConvenientDialogContentMessage.tsx b/src/components/dialog/Dialog/ConvenientDialogContentMessage.tsx index 77d1ff99..4a77a2e7 100644 --- a/src/components/dialog/Dialog/ConvenientDialogContentMessage.tsx +++ b/src/components/dialog/Dialog/ConvenientDialogContentMessage.tsx @@ -1,5 +1,5 @@ -import IconList from '@aboutbits/react-material-icons/dist/IconList' -import IconWarning from '@aboutbits/react-material-icons/dist/IconWarning' +import IconList from '@aboutbits/react-material-icons/dist/IconListRoundedFilled' +import IconWarning from '@aboutbits/react-material-icons/dist/IconWarningRoundedFilled' import { ReactElement } from 'react' import { Tone } from '../../types' import { diff --git a/src/components/dialog/DialogHeader/DialogHeaderCloseAction.tsx b/src/components/dialog/DialogHeader/DialogHeaderCloseAction.tsx index 366c63ee..d3d2ae26 100644 --- a/src/components/dialog/DialogHeader/DialogHeaderCloseAction.tsx +++ b/src/components/dialog/DialogHeader/DialogHeaderCloseAction.tsx @@ -1,4 +1,4 @@ -import IconClose from '@aboutbits/react-material-icons/dist/IconClose' +import IconClose from '@aboutbits/react-material-icons/dist/IconCloseRounded' import { ComponentType, ReactElement } from 'react' import { useInternationalization } from '../../../framework' import { IconProps } from '../../types' diff --git a/src/components/dialog/DialogItem/DialogListItemButton.tsx b/src/components/dialog/DialogItem/DialogListItemButton.tsx index a2686d71..9de8eb35 100644 --- a/src/components/dialog/DialogItem/DialogListItemButton.tsx +++ b/src/components/dialog/DialogItem/DialogListItemButton.tsx @@ -1,4 +1,4 @@ -import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRight' +import IconKeyboardArrowRight from '@aboutbits/react-material-icons/dist/IconKeyboardArrowRightRounded' import classNames from 'classnames' import { MouseEventHandler, forwardRef } from 'react' import { useTheme } from '../../../framework' diff --git a/src/components/files/DeleteFileAction.tsx b/src/components/files/DeleteFileAction.tsx index 5714267f..b4fc522e 100644 --- a/src/components/files/DeleteFileAction.tsx +++ b/src/components/files/DeleteFileAction.tsx @@ -1,4 +1,4 @@ -import IconDelete from '@aboutbits/react-material-icons/dist/IconDelete' +import IconDelete from '@aboutbits/react-material-icons/dist/IconDeleteRoundedFilled' import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' diff --git a/src/components/files/DownloadFileAction.tsx b/src/components/files/DownloadFileAction.tsx index 33502bd5..6996892f 100644 --- a/src/components/files/DownloadFileAction.tsx +++ b/src/components/files/DownloadFileAction.tsx @@ -1,4 +1,4 @@ -import IconDownload from '@aboutbits/react-material-icons/dist/IconDownload' +import IconDownload from '@aboutbits/react-material-icons/dist/IconDownloadRounded' import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' diff --git a/src/components/files/FileDropZone.tsx b/src/components/files/FileDropZone.tsx index 467f3994..c62b02dc 100644 --- a/src/components/files/FileDropZone.tsx +++ b/src/components/files/FileDropZone.tsx @@ -1,4 +1,4 @@ -import IconUploadFile from '@aboutbits/react-material-icons/dist/IconUploadFile' +import IconUploadFile from '@aboutbits/react-material-icons/dist/IconUploadFileRounded' import classNames from 'classnames' import { ChangeEventHandler, useRef } from 'react' import { useInternationalization, useTheme } from '../../framework' diff --git a/src/components/files/FileListItem.tsx b/src/components/files/FileListItem.tsx index 5f8e587d..8b6aaf1c 100644 --- a/src/components/files/FileListItem.tsx +++ b/src/components/files/FileListItem.tsx @@ -1,5 +1,5 @@ -import IconCheckCircle from '@aboutbits/react-material-icons/dist/IconCheckCircle' -import IconInsertDriveFile from '@aboutbits/react-material-icons/dist/IconInsertDriveFile' +import IconCheckCircle from '@aboutbits/react-material-icons/dist/IconCheckCircleRoundedFilled' +import IconDraftRounded from '@aboutbits/react-material-icons/dist/IconDraftRoundedFilled' import classNames from 'classnames' import { ReactNode, useEffect, useState } from 'react' import { useInternationalization, useTheme } from '../../framework' @@ -88,7 +88,7 @@ export function FileListItem({ className={classNames(files.icon.size, files.icon.success)} /> ) : ( - Date: Tue, 3 Sep 2024 16:20:26 +0200 Subject: [PATCH 10/14] add icon from lib for spinner --- src/components/files/FileListItem.tsx | 4 +++- src/components/loading/IconSpinner.tsx | 18 ++++----------- .../react-hook-form/FormSubmitFeedback.tsx | 2 +- tailwind-preset.ts | 23 ------------------- 4 files changed, 8 insertions(+), 39 deletions(-) diff --git a/src/components/files/FileListItem.tsx b/src/components/files/FileListItem.tsx index 8b6aaf1c..c8f8bed8 100644 --- a/src/components/files/FileListItem.tsx +++ b/src/components/files/FileListItem.tsx @@ -68,7 +68,9 @@ export function FileListItem({ files.icon.container.default, )} > - +
) : (
- - + /> ) } diff --git a/src/components/react-hook-form/FormSubmitFeedback.tsx b/src/components/react-hook-form/FormSubmitFeedback.tsx index 4c19dde0..53bb9f49 100644 --- a/src/components/react-hook-form/FormSubmitFeedback.tsx +++ b/src/components/react-hook-form/FormSubmitFeedback.tsx @@ -1,4 +1,4 @@ -import IconCheckCircle from '@aboutbits/react-material-icons/dist/IconCheckCircleRounded' +import IconCheckCircle from '@aboutbits/react-material-icons/dist/IconCheckCircleRoundedFilled' import classNames from 'classnames' import { ComponentType, ReactElement, ReactNode } from 'react' import { useInternationalization, useTheme } from '../../framework' diff --git a/tailwind-preset.ts b/tailwind-preset.ts index 7fc724a2..03f0266a 100644 --- a/tailwind-preset.ts +++ b/tailwind-preset.ts @@ -102,29 +102,6 @@ export default { lineHeight: { 12: '3rem', }, - keyframes: { - spinner: { - '0%': { - strokeDasharray: '0 100', - strokeDashoffset: '25', - }, - '50%': { - strokeDasharray: `100 0`, - strokeDashoffset: '25', - }, - '50.1%': { - strokeDasharray: '100 0', - strokeDashoffset: '125', - }, - '100%': { - strokeDasharray: '0 100', - strokeDashoffset: '25', - }, - }, - }, - animation: { - spinner: 'spinner 2s cubic-bezier(0.88, 0, 0.58, 1) infinite', - }, }, }, } satisfies Config From 6976422ec0575dcba889a19cdb0701c6e64e92a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Tue, 3 Sep 2024 16:36:02 +0200 Subject: [PATCH 11/14] use new icons in mdx files --- .../header/actions/HeaderBackAction.stories.mdx | 4 ++-- .../header/actions/HeaderButtonIcon.stories.mdx | 6 +++--- .../header/actions/HeaderCloseAction.stories.mdx | 6 +++--- .../header/actions/HeaderLeftActionIcon.stories.mdx | 6 +++--- .../actions/HeaderRightActionIcon.stories.mdx | 6 +++--- .../header/areas/HeaderLeftArea.stories.mdx | 4 ++-- .../header/areas/HeaderRightArea.stories.mdx | 6 +++--- .../Section/SectionContentMessage.stories.mdx | 9 +++------ .../SectionListItemWithAction.stories.mdx | 13 ++++--------- 9 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/components/header/actions/HeaderBackAction.stories.mdx b/src/components/header/actions/HeaderBackAction.stories.mdx index dc9bb268..e562489f 100644 --- a/src/components/header/actions/HeaderBackAction.stories.mdx +++ b/src/components/header/actions/HeaderBackAction.stories.mdx @@ -1,5 +1,5 @@ -import IconClose from '@aboutbits/react-material-icons/dist/IconClose' -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' +import IconClose from '@aboutbits/react-material-icons/dist/IconCloseRounded' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' import { InternationalizationMessages } from '../../../../.storybook/components' import { HeaderBackAction } from './HeaderBackAction' diff --git a/src/components/header/actions/HeaderButtonIcon.stories.mdx b/src/components/header/actions/HeaderButtonIcon.stories.mdx index 1e634347..56b3be3d 100644 --- a/src/components/header/actions/HeaderButtonIcon.stories.mdx +++ b/src/components/header/actions/HeaderButtonIcon.stories.mdx @@ -1,6 +1,6 @@ -import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBack' -import IconClose from '@aboutbits/react-material-icons/dist/IconClose' -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' +import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBackRounded' +import IconClose from '@aboutbits/react-material-icons/dist/IconCloseRounded' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' import { Theme } from '../../../../.storybook/components' import { HeaderButtonIcon } from './HeaderButtonIcon' diff --git a/src/components/header/actions/HeaderCloseAction.stories.mdx b/src/components/header/actions/HeaderCloseAction.stories.mdx index 1fabf53a..a5adb32c 100644 --- a/src/components/header/actions/HeaderCloseAction.stories.mdx +++ b/src/components/header/actions/HeaderCloseAction.stories.mdx @@ -1,6 +1,6 @@ -import IconArrowBackIos from '@aboutbits/react-material-icons/dist/IconArrowBackIos' -import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBack' -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' +import IconArrowBackIos from '@aboutbits/react-material-icons/dist/IconArrowBackIosRounded' +import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBackRounded' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' import { InternationalizationMessages } from '../../../../.storybook/components' import { HeaderCloseAction } from './HeaderCloseAction' diff --git a/src/components/header/actions/HeaderLeftActionIcon.stories.mdx b/src/components/header/actions/HeaderLeftActionIcon.stories.mdx index 0a4e2cdc..b1814f1c 100644 --- a/src/components/header/actions/HeaderLeftActionIcon.stories.mdx +++ b/src/components/header/actions/HeaderLeftActionIcon.stories.mdx @@ -1,6 +1,6 @@ -import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBack' -import IconClose from '@aboutbits/react-material-icons/dist/IconClose' -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' +import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBackRounded' +import IconClose from '@aboutbits/react-material-icons/dist/IconCloseRounded' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' import { Theme } from '../../../../.storybook/components' import { HeaderLeftActionIcon } from './HeaderLeftActionIcon' diff --git a/src/components/header/actions/HeaderRightActionIcon.stories.mdx b/src/components/header/actions/HeaderRightActionIcon.stories.mdx index a27c23c2..ea36ea5b 100644 --- a/src/components/header/actions/HeaderRightActionIcon.stories.mdx +++ b/src/components/header/actions/HeaderRightActionIcon.stories.mdx @@ -1,6 +1,6 @@ -import IconSave from '@aboutbits/react-material-icons/dist/IconSave' -import IconForward from '@aboutbits/react-material-icons/dist/IconForward' -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' +import IconSave from '@aboutbits/react-material-icons/dist/IconSaveRounded' +import IconForward from '@aboutbits/react-material-icons/dist/IconForwardRounded' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' import { Theme } from '../../../../.storybook/components' import { HeaderRightActionIcon } from './HeaderRightActionIcon' diff --git a/src/components/header/areas/HeaderLeftArea.stories.mdx b/src/components/header/areas/HeaderLeftArea.stories.mdx index 19905fc8..1df4d7ed 100644 --- a/src/components/header/areas/HeaderLeftArea.stories.mdx +++ b/src/components/header/areas/HeaderLeftArea.stories.mdx @@ -1,5 +1,5 @@ -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' -import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBack' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' +import IconArrowBack from '@aboutbits/react-material-icons/dist/IconArrowBackRounded' import { action } from '@storybook/addon-actions' import { Theme } from '../../../../.storybook/components' import { HeaderButtonIcon } from '../actions/HeaderButtonIcon' diff --git a/src/components/header/areas/HeaderRightArea.stories.mdx b/src/components/header/areas/HeaderRightArea.stories.mdx index c9f4776a..244ddf8a 100644 --- a/src/components/header/areas/HeaderRightArea.stories.mdx +++ b/src/components/header/areas/HeaderRightArea.stories.mdx @@ -1,7 +1,7 @@ -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' -import IconEdit from '@aboutbits/react-material-icons/dist/IconEdit' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' +import IconEdit from '@aboutbits/react-material-icons/dist/IconEditRounded' import { action } from '@storybook/addon-actions' -import IconAdd from '@aboutbits/react-material-icons/dist/IconAdd' +import IconAdd from '@aboutbits/react-material-icons/dist/IconAddRounded' import { Theme } from '../../../../.storybook/components' import { HeaderButtonIcon } from '../actions/HeaderButtonIcon' import { HeaderRightArea } from './HeaderRightArea' diff --git a/src/components/section/Section/SectionContentMessage.stories.mdx b/src/components/section/Section/SectionContentMessage.stories.mdx index 4b92a168..7c4fdd36 100644 --- a/src/components/section/Section/SectionContentMessage.stories.mdx +++ b/src/components/section/Section/SectionContentMessage.stories.mdx @@ -1,12 +1,9 @@ -import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs' -import IconList from '@aboutbits/react-material-icons/dist/IconList' +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' +import IconList from '@aboutbits/react-material-icons/dist/IconListRoundedFilled' import { Theme } from '../../../../.storybook/components' import { Tone } from '../../types' import { SectionContentMessage } from './SectionContentMessage' -import { - SectionContentEmpty, - SectionContentError, -} from './ConvenientSectionContentMessage' +import { SectionContentEmpty, SectionContentError, } from './ConvenientSectionContentMessage' Date: Wed, 4 Sep 2024 08:04:04 +0200 Subject: [PATCH 12/14] add loading spinner component --- src/components/files/DeleteFileAction.tsx | 4 ++-- src/components/files/DownloadFileAction.tsx | 4 ++-- src/components/files/FileListItem.tsx | 4 ++-- .../loading/{IconSpinner.tsx => LoadingSpinner.tsx} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/components/loading/{IconSpinner.tsx => LoadingSpinner.tsx} (85%) diff --git a/src/components/files/DeleteFileAction.tsx b/src/components/files/DeleteFileAction.tsx index b4fc522e..696ca561 100644 --- a/src/components/files/DeleteFileAction.tsx +++ b/src/components/files/DeleteFileAction.tsx @@ -2,7 +2,7 @@ import IconDelete from '@aboutbits/react-material-icons/dist/IconDeleteRoundedFi import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' -import { IconSpinner } from '../loading/IconSpinner' +import { LoadingSpinner } from '../loading/LoadingSpinner' import { Tone } from '../types' import { FileUploadObject } from './FileUploadState' import { ResponsiveButtonIcon } from './ResponsiveButtonIcon' @@ -35,7 +35,7 @@ export function DeleteFileAction({ setIsDeleting(false) }) }} - icon={isDeleting ? IconSpinner : IconDelete} + icon={isDeleting ? LoadingSpinner : IconDelete} label={messages['files.action.delete']} /> ) diff --git a/src/components/files/DownloadFileAction.tsx b/src/components/files/DownloadFileAction.tsx index 6996892f..ac5299cb 100644 --- a/src/components/files/DownloadFileAction.tsx +++ b/src/components/files/DownloadFileAction.tsx @@ -2,7 +2,7 @@ import IconDownload from '@aboutbits/react-material-icons/dist/IconDownloadRound import { useState } from 'react' import { useInternationalization } from '../../framework' import { ButtonVariant } from '../button' -import { IconSpinner } from '../loading/IconSpinner' +import { LoadingSpinner } from '../loading/LoadingSpinner' import { Tone } from '../types' import { FileUploadObject } from './FileUploadState' import { ResponsiveButtonIcon } from './ResponsiveButtonIcon' @@ -25,7 +25,7 @@ export function DownloadFileAction({ variant={ButtonVariant.Transparent} tone={Tone.Neutral} disabled={isDownloading} - icon={isDownloading ? IconSpinner : IconDownload} + icon={isDownloading ? LoadingSpinner : IconDownload} onClick={() => { setIsDownloading(true) Promise.resolve(onDownload(fileUploadObject)) diff --git a/src/components/files/FileListItem.tsx b/src/components/files/FileListItem.tsx index c8f8bed8..8272e4c5 100644 --- a/src/components/files/FileListItem.tsx +++ b/src/components/files/FileListItem.tsx @@ -3,7 +3,7 @@ import IconDraftRounded from '@aboutbits/react-material-icons/dist/IconDraftRoun import classNames from 'classnames' import { ReactNode, useEffect, useState } from 'react' import { useInternationalization, useTheme } from '../../framework' -import { IconSpinner } from '../loading/IconSpinner' +import { LoadingSpinner } from '../loading/LoadingSpinner' import { FileSpace, FileState, FileUploadObject } from './FileUploadState' import { useHumanReadableFileSize } from './utils' @@ -68,7 +68,7 @@ export function FileListItem({ files.icon.container.default, )} > -
diff --git a/src/components/loading/IconSpinner.tsx b/src/components/loading/LoadingSpinner.tsx similarity index 85% rename from src/components/loading/IconSpinner.tsx rename to src/components/loading/LoadingSpinner.tsx index b3dfb510..b88e466c 100644 --- a/src/components/loading/IconSpinner.tsx +++ b/src/components/loading/LoadingSpinner.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames' import { useTheme } from '../../framework' import { IconProps } from '../types' -export function IconSpinner({ className, ...props }: IconProps) { +export function LoadingSpinner({ className, ...props }: IconProps) { const { loading } = useTheme() return ( Date: Wed, 4 Sep 2024 09:20:03 +0200 Subject: [PATCH 13/14] add loading spinner stories --- src/components/loading/LoadingSpinner.tsx | 5 ++ .../loading/LoadingSpinner.tsx.stories.tsx | 48 +++++++++++++++++++ src/components/loading/index.ts | 1 + src/components/loading/theme.ts | 2 +- 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/components/loading/LoadingSpinner.tsx.stories.tsx diff --git a/src/components/loading/LoadingSpinner.tsx b/src/components/loading/LoadingSpinner.tsx index b88e466c..cd735d75 100644 --- a/src/components/loading/LoadingSpinner.tsx +++ b/src/components/loading/LoadingSpinner.tsx @@ -3,6 +3,11 @@ import classNames from 'classnames' import { useTheme } from '../../framework' import { IconProps } from '../types' +/** + * + * The LoadingSpinner component displays a spinning icon to indicate something is loading. + * It uses the ```IconProps``` and therefore can be used in the same way as Icons. + */ export function LoadingSpinner({ className, ...props }: IconProps) { const { loading } = useTheme() return ( diff --git a/src/components/loading/LoadingSpinner.tsx.stories.tsx b/src/components/loading/LoadingSpinner.tsx.stories.tsx new file mode 100644 index 00000000..6234bf66 --- /dev/null +++ b/src/components/loading/LoadingSpinner.tsx.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, StoryObj } from '@storybook/react' +import { Button } from '../button' +import { LoadingSpinner } from './LoadingSpinner' + +const meta: Meta = { + title: 'Components/Loading/LoadingSpinner', + component: LoadingSpinner, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Additional CSS classes', + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { className: 'w-8 h-8' }, +} + +export const Large: Story = { + args: { + className: 'w-12 h-12', + }, +} + +export const CustomColor: Story = { + render: () => ( +
+ + + + + +
+ ), +} + +export const AsButtonIcon: Story = { + render: () => ( + + ), +} diff --git a/src/components/loading/index.ts b/src/components/loading/index.ts index e48522b0..62af4155 100644 --- a/src/components/loading/index.ts +++ b/src/components/loading/index.ts @@ -1,2 +1,3 @@ export * from './LoadingBar' export * from './LoadingInput' +export * from './LoadingSpinner' diff --git a/src/components/loading/theme.ts b/src/components/loading/theme.ts index 5ce2a2f7..e6ba5e1d 100644 --- a/src/components/loading/theme.ts +++ b/src/components/loading/theme.ts @@ -12,7 +12,7 @@ export default { }, }, iconSpinner: { - base: 'animate-spin', + base: 'animate-spin fill-current', currentColor: 'stroke-current', defaultColor: 'stroke-primary-800', }, From f1f933dbfef22c876afc46430b6cbc3d23f6f07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20N=C3=B6ssing?= Date: Wed, 9 Oct 2024 13:09:27 +0200 Subject: [PATCH 14/14] fix loading spinner filename --- ...{LoadingSpinner.tsx.stories.tsx => LoadingSpinner.stories.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/loading/{LoadingSpinner.tsx.stories.tsx => LoadingSpinner.stories.tsx} (100%) diff --git a/src/components/loading/LoadingSpinner.tsx.stories.tsx b/src/components/loading/LoadingSpinner.stories.tsx similarity index 100% rename from src/components/loading/LoadingSpinner.tsx.stories.tsx rename to src/components/loading/LoadingSpinner.stories.tsx