Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 93 additions & 160 deletions src/components/InviteUserModal/InviteUserModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { InviteUserModal, Role } from './InviteUserModal';
import { Button } from '../Button/Button';
import { InviteUserModal } from './InviteUserModal';

const meta: Meta<typeof InviteUserModal> = {
title: 'Components/InviteUserModal',
component: InviteUserModal,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
};

export default meta;
type Story = StoryObj<typeof InviteUserModal>;

const sampleRoles: Role[] = [
const sampleRoles = [
{
id: 'admin',
name: 'Administrator',
Expand All @@ -38,170 +24,117 @@ const sampleRoles: Role[] = [
},
];

// Interactive wrapper component
function InteractiveDemo({
entityName,
entityDisplayName,
roles = sampleRoles,
defaultRoleId,
}: {
entityName?: string;
entityDisplayName?: string;
roles?: Role[];
defaultRoleId?: string;
}) {
const [open, setOpen] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | undefined>();
const [success, setSuccess] = useState<string | undefined>();

const handleSubmit = async (data: {
email: string;
firstName?: string;
lastName?: string;
roleId: string;
message?: string;
}) => {
setIsSubmitting(true);
setError(undefined);
setSuccess(undefined);

// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));

// Simulate success or error
if (data.email.includes('error')) {
setError('User with this email already exists');
setIsSubmitting(false);
} else {
setSuccess(`Invitation sent to ${data.email}`);
setIsSubmitting(false);
// Close after showing success briefly
setTimeout(() => setOpen(false), 2000);
}
};
const meta: Meta<typeof InviteUserModal> = {
title: 'Components/InviteUserModal',
component: InviteUserModal,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
// Container with transform creates a new containing block for position:fixed
// This keeps the modal within this container in docs view
<div
className="flex min-h-[700px] items-center justify-center"
style={{ transform: 'translateZ(0)' }}
>
<Story />
</div>
),
],
args: {
open: true,
roles: sampleRoles,
},
argTypes: {
open: {
control: 'boolean',
description: 'Whether the modal is open',
},
onOpenChange: { action: 'onOpenChange' },
onSubmit: { action: 'onSubmit' },
roles: {
control: 'object',
description: 'Available roles to assign',
},
defaultRoleId: {
control: 'text',
description: 'Default role ID',
},
isSubmitting: {
control: 'boolean',
description: 'Whether submission is in progress',
},
entityName: {
control: 'text',
description: 'Entity name (e.g., "provider" or "organization")',
},
entityDisplayName: {
control: 'text',
description: 'Entity display name',
},
errorMessage: {
control: 'text',
description: 'Error message to display',
},
successMessage: {
control: 'text',
description: 'Success message to display',
},
},
};

return (
<div>
<Button onClick={() => setOpen(true)}>Invite User</Button>
<InviteUserModal
open={open}
onOpenChange={setOpen}
onSubmit={handleSubmit}
roles={roles}
defaultRoleId={defaultRoleId}
entityName={entityName}
entityDisplayName={entityDisplayName}
isSubmitting={isSubmitting}
errorMessage={error}
successMessage={success}
/>
</div>
);
}
export default meta;
type Story = StoryObj<typeof InviteUserModal>;

export const Default: Story = {
render: () => <InteractiveDemo />,
};
export const Default: Story = {};

export const WithProvider: Story = {
render: () => (
<InteractiveDemo
entityName="provider"
entityDisplayName="MedCare Health Services"
/>
),
args: {
entityName: 'provider',
entityDisplayName: 'MedCare Health Services',
},
};

export const WithEmployer: Story = {
render: () => (
<InteractiveDemo
entityName="employer"
entityDisplayName="Acme Corporation"
/>
),
args: {
entityName: 'employer',
entityDisplayName: 'Acme Corporation',
},
};

export const WithDefaultRole: Story = {
render: () => (
<InteractiveDemo
entityDisplayName="Healthcare Partners"
defaultRoleId="staff"
/>
),
args: {
entityDisplayName: 'Healthcare Partners',
defaultRoleId: 'staff',
},
};

// Wrapper for WithError story
function WithErrorWrapper() {
const [open, setOpen] = useState(true);

return (
<>
<Button onClick={() => setOpen(true)}>Show Modal with Error</Button>
<InviteUserModal
open={open}
onOpenChange={setOpen}
roles={sampleRoles}
errorMessage="User with this email already has an account"
/>
</>
);
}

// Wrapper for WithSuccess story
function WithSuccessWrapper() {
const [open, setOpen] = useState(true);

return (
<>
<Button onClick={() => setOpen(true)}>Show Modal with Success</Button>
<InviteUserModal
open={open}
onOpenChange={setOpen}
roles={sampleRoles}
successMessage="Invitation sent to john@example.com"
/>
</>
);
}

// Wrapper for Submitting story
function SubmittingWrapper() {
const [open, setOpen] = useState(true);

return (
<>
<Button onClick={() => setOpen(true)}>Show Submitting State</Button>
<InviteUserModal
open={open}
onOpenChange={setOpen}
roles={sampleRoles}
isSubmitting={true}
/>
</>
);
}

export const WithError: Story = {
render: () => <WithErrorWrapper />,
args: {
errorMessage: 'User with this email already has an account',
},
};

export const WithSuccess: Story = {
render: () => <WithSuccessWrapper />,
args: {
successMessage: 'Invitation sent to john@example.com',
},
};

export const Submitting: Story = {
render: () => <SubmittingWrapper />,
args: {
isSubmitting: true,
},
};

export const MinimalRoles: Story = {
render: () => (
<InteractiveDemo
entityDisplayName="Simple Provider"
roles={[
{ id: 'admin', name: 'Admin' },
{ id: 'user', name: 'User' },
]}
/>
),
args: {
entityDisplayName: 'Simple Provider',
roles: [
{ id: 'admin', name: 'Admin' },
{ id: 'user', name: 'User' },
],
},
};
24 changes: 15 additions & 9 deletions src/components/InviteUserModal/InviteUserModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
'use client';

import * as React from 'react';
import { Modal, ModalHeader, ModalTitle, ModalFooter } from '../Modal/Modal';
import {
Modal,
ModalHeader,
ModalTitle,
ModalBody,
ModalFooter,
} from '../Modal/Modal';
import { Button } from '../Button/Button';
import { Input } from '../Input/Input';
import { Select } from '../Select/Select';
Expand Down Expand Up @@ -95,12 +101,12 @@ export function InviteUserModal({
<ModalTitle>Invite User</ModalTitle>
</ModalHeader>

<div className="space-y-4">
<ModalBody className="space-y-4">
{entityDisplayName && (
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<p className="text-sm text-gray-600 dark:text-gray-400">
<div className="bg-muted rounded-lg p-3">
<p className="text-muted-foreground text-sm">
Inviting user to:{' '}
<span className="font-medium text-gray-900 dark:text-white">
<span className="text-foreground font-medium">
{entityDisplayName}
</span>
</p>
Expand Down Expand Up @@ -202,13 +208,13 @@ export function InviteUserModal({
<div>
<label
htmlFor="invite-message"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
className="text-foreground mb-1 block text-sm font-medium"
>
Personal Message (optional)
</label>
<textarea
id="invite-message"
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
className="border-input bg-background text-foreground focus:ring-primary w-full rounded-md border px-3 py-2 shadow-sm focus:ring-2 focus:outline-none"
rows={3}
value={message}
onChange={(e) => setMessage(e.target.value)}
Expand All @@ -217,12 +223,12 @@ export function InviteUserModal({
</div>

{/* Info text */}
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-muted-foreground text-xs">
An email invitation will be sent to this address. If the user
doesn&apos;t have an account, they&apos;ll be prompted to create
one.
</p>
</div>
</ModalBody>

<ModalFooter>
<Button
Expand Down
Loading