Files
jellyseerr/src/components/Common/ButtonWithDropdown/index.tsx
Michael Thomas 64f05bcad6 feat: add linked accounts page (#883)
* feat(linked-accounts): create page and display linked media server accounts

* feat(dropdown): add new shared Dropdown component

Adds a shared component for plain dropdown menus, based on the headlessui Menu component. Updates
the `ButtonWithDropdown` component to use the same inner components, ensuring that the only
difference between the two components is the trigger button, and both use the same components for
the actual dropdown menu.

* refactor(modal): add support for configuring button props

* feat(linked-accounts): add support for linking/unlinking jellyfin accounts

* feat(linked-accounts): support linking/unlinking plex accounts

* fix(linked-accounts): probibit unlinking accounts in certain cases

Prevents the primary administrator from unlinking their media server account (which would break
sync). Additionally, prevents users without a configured local email and password from unlinking
their accounts, which would render them unable to log in.

* feat(linked-accounts): support linking/unlinking emby accounts

* style(dropdown): improve style class application

* fix(server): improve error handling and API spec

* style(usersettings): improve syntax & performance of user password checks

* style(linkedaccounts): use applicationName in page description

* fix(linkedaccounts): resolve typo

* refactor(app): remove RequestError class
2025-02-23 00:16:25 +08:00

71 lines
2.7 KiB
TypeScript

import Dropdown from '@app/components/Common/Dropdown';
import { withProperties } from '@app/utils/typeHelpers';
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
type ButtonWithDropdownProps = {
text: React.ReactNode;
dropdownIcon?: React.ReactNode;
buttonType?: 'primary' | 'ghost';
} & (
| ({ as?: 'button' } & ButtonHTMLAttributes<HTMLButtonElement>)
| ({ as: 'a' } & AnchorHTMLAttributes<HTMLAnchorElement>)
);
const ButtonWithDropdown = ({
text,
children,
dropdownIcon,
className,
buttonType = 'primary',
...props
}: ButtonWithDropdownProps) => {
const styleClasses = {
mainButtonClasses: 'button-md text-white border',
dropdownSideButtonClasses: 'button-md border',
};
switch (buttonType) {
case 'ghost':
styleClasses.mainButtonClasses +=
' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses;
break;
default:
styleClasses.mainButtonClasses +=
' bg-indigo-600 border-indigo-500 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue';
styleClasses.dropdownSideButtonClasses +=
' bg-indigo-600 bg-opacity-80 border-indigo-500 hover:bg-opacity-100 active:bg-opacity-100 focus:ring-blue';
}
const TriggerElement = props.as ?? 'button';
return (
<Menu as="div" className="relative z-10 inline-flex">
<TriggerElement
type="button"
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
styleClasses.mainButtonClasses
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
{...(props as Record<string, string>)}
>
{text}
</TriggerElement>
{children && (
<span className="relative -ml-px block">
<Menu.Button
type="button"
className={`relative z-10 inline-flex h-full items-center rounded-r-md px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 focus:z-20 ${styleClasses.dropdownSideButtonClasses}`}
aria-label="Expand"
>
{dropdownIcon ? dropdownIcon : <ChevronDownIcon />}
</Menu.Button>
<Dropdown.Items dropdownType={buttonType}>{children}</Dropdown.Items>
</span>
)}
</Menu>
);
};
export default withProperties(ButtonWithDropdown, { Item: Dropdown.Item });