583da45296
Refactor the usage log filter toolbar into a shared reusable component for common, drawing, and task logs. Optimize desktop filters with a responsive grid, move secondary filters into a mobile drawer, standardize filter typography, remove redundant filter icons, and add the missing i18n translations for the new drawer description.
739 lines
26 KiB
TypeScript
Vendored
739 lines
26 KiB
TypeScript
Vendored
/*
|
||
Copyright (C) 2023-2026 QuantumNous
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU Affero General Public License as
|
||
published by the Free Software Foundation, either version 3 of the
|
||
License, or (at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU Affero General Public License for more details.
|
||
|
||
You should have received a copy of the GNU Affero General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
For commercial licensing, please contact support@quantumnous.com
|
||
*/
|
||
import { useEffect, useState } from 'react'
|
||
import { useForm, type Resolver } from 'react-hook-form'
|
||
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { CalendarClock, CreditCard, RefreshCw, Settings2 } from 'lucide-react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { toast } from 'sonner'
|
||
import { Button } from '@/components/ui/button'
|
||
import {
|
||
Form,
|
||
FormControl,
|
||
FormDescription,
|
||
FormField,
|
||
FormItem,
|
||
FormLabel,
|
||
FormMessage,
|
||
} from '@/components/ui/form'
|
||
import { Input } from '@/components/ui/input'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectGroup,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
import {
|
||
Sheet,
|
||
SheetClose,
|
||
SheetContent,
|
||
SheetDescription,
|
||
SheetFooter,
|
||
SheetHeader,
|
||
SheetTitle,
|
||
} from '@/components/ui/sheet'
|
||
import { Switch } from '@/components/ui/switch'
|
||
import {
|
||
SideDrawerSection,
|
||
sideDrawerContentClassName,
|
||
sideDrawerFooterClassName,
|
||
sideDrawerFormClassName,
|
||
sideDrawerHeaderClassName,
|
||
sideDrawerSwitchItemClassName,
|
||
} from '@/components/drawer-layout'
|
||
import {
|
||
createPlan,
|
||
updatePlan,
|
||
getGroups,
|
||
createWaffoPancakeSubscriptionProduct,
|
||
listWaffoPancakeSubscriptionProductOptions,
|
||
} from '../api'
|
||
import { getDurationUnitOptions, getResetPeriodOptions } from '../constants'
|
||
import {
|
||
getPlanFormSchema,
|
||
PLAN_FORM_DEFAULTS,
|
||
planToFormValues,
|
||
formValuesToPlanPayload,
|
||
type PlanFormValues,
|
||
} from '../lib'
|
||
import type { PlanRecord } from '../types'
|
||
import { useSubscriptions } from './subscriptions-provider'
|
||
|
||
interface Props {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
currentRow?: PlanRecord
|
||
}
|
||
|
||
export function SubscriptionsMutateDrawer({
|
||
open,
|
||
onOpenChange,
|
||
currentRow,
|
||
}: Props) {
|
||
const { t } = useTranslation()
|
||
const isEdit = !!currentRow?.plan?.id
|
||
const { triggerRefresh } = useSubscriptions()
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
const [groupOptions, setGroupOptions] = useState<string[]>([])
|
||
const [creatingPancakeProduct, setCreatingPancakeProduct] = useState(false)
|
||
const [pancakeProducts, setPancakeProducts] = useState<
|
||
{ id: string; name: string; status: string }[]
|
||
>([])
|
||
|
||
const schema = getPlanFormSchema(t)
|
||
const form = useForm<PlanFormValues>({
|
||
resolver: zodResolver(schema) as unknown as Resolver<PlanFormValues>,
|
||
defaultValues: PLAN_FORM_DEFAULTS,
|
||
})
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
if (currentRow?.plan) {
|
||
form.reset(planToFormValues(currentRow.plan))
|
||
} else {
|
||
form.reset(PLAN_FORM_DEFAULTS)
|
||
}
|
||
getGroups()
|
||
.then((res) => {
|
||
if (res.success) setGroupOptions(res.data || [])
|
||
})
|
||
.catch(() => {})
|
||
// Best-effort — empty list still lets the operator use "+ Create".
|
||
listWaffoPancakeSubscriptionProductOptions()
|
||
.then((res) => {
|
||
if (
|
||
res.message === 'success' &&
|
||
typeof res.data === 'object' &&
|
||
res.data &&
|
||
Array.isArray((res.data as { products?: unknown }).products)
|
||
) {
|
||
setPancakeProducts(
|
||
(res.data as { products: typeof pancakeProducts }).products
|
||
)
|
||
} else {
|
||
setPancakeProducts([])
|
||
}
|
||
})
|
||
.catch(() => setPancakeProducts([]))
|
||
}
|
||
}, [open, currentRow, form])
|
||
|
||
const durationUnit = form.watch('duration_unit')
|
||
const resetPeriod = form.watch('quota_reset_period')
|
||
// Gate "+ Create on Pancake" on the same checks the mint handler runs.
|
||
const watchedTitle = form.watch('title')
|
||
const watchedPrice = form.watch('price_amount')
|
||
const pancakeCreateReady =
|
||
typeof watchedTitle === 'string' &&
|
||
watchedTitle.trim().length > 0 &&
|
||
Number(watchedPrice ?? 0) > 0
|
||
|
||
const onSubmit = async (values: PlanFormValues) => {
|
||
setIsSubmitting(true)
|
||
try {
|
||
const payload = formValuesToPlanPayload(values)
|
||
if (isEdit && currentRow?.plan?.id) {
|
||
const res = await updatePlan(currentRow.plan.id, payload)
|
||
if (res.success) {
|
||
toast.success(t('Update succeeded'))
|
||
onOpenChange(false)
|
||
triggerRefresh()
|
||
}
|
||
} else {
|
||
const res = await createPlan(payload)
|
||
if (res.success) {
|
||
toast.success(t('Create succeeded'))
|
||
onOpenChange(false)
|
||
triggerRefresh()
|
||
}
|
||
}
|
||
} catch {
|
||
toast.error(t('Request failed'))
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// Mints a Pancake OnetimeProduct (not SubscriptionProduct — see
|
||
// controller) using persisted creds + the form's title/price, then
|
||
// pins the returned PROD_ ID into the form field.
|
||
const handleCreatePancakeProduct = async () => {
|
||
const title = form.getValues('title').trim()
|
||
const priceAmount = Number(form.getValues('price_amount') || 0)
|
||
if (!title) {
|
||
toast.error(t('Plan title is required'))
|
||
return
|
||
}
|
||
if (priceAmount <= 0) {
|
||
toast.error(t('Plan price must be greater than zero'))
|
||
return
|
||
}
|
||
setCreatingPancakeProduct(true)
|
||
try {
|
||
const res = await createWaffoPancakeSubscriptionProduct({
|
||
name: title,
|
||
amount: priceAmount.toFixed(2),
|
||
})
|
||
if (
|
||
res.message === 'success' &&
|
||
typeof res.data === 'object' &&
|
||
res.data
|
||
) {
|
||
const created = res.data as { product_id: string; product_name: string }
|
||
form.setValue('waffo_pancake_product_id', created.product_id, {
|
||
shouldDirty: true,
|
||
})
|
||
// Refetch from GraphQL so the dropdown reflects authoritative state.
|
||
try {
|
||
const refresh = await listWaffoPancakeSubscriptionProductOptions()
|
||
if (
|
||
refresh.message === 'success' &&
|
||
typeof refresh.data === 'object' &&
|
||
refresh.data &&
|
||
Array.isArray((refresh.data as { products?: unknown }).products)
|
||
) {
|
||
setPancakeProducts(
|
||
(refresh.data as { products: typeof pancakeProducts }).products
|
||
)
|
||
}
|
||
} catch {
|
||
// Best-effort — form value already points at the new product;
|
||
// raw-ID fallback covers the missing label.
|
||
}
|
||
toast.success(
|
||
`${t('Waffo Pancake product created')}: ${created.product_id}`
|
||
)
|
||
} else {
|
||
const reason = typeof res.data === 'string' ? res.data : undefined
|
||
toast.error(
|
||
reason
|
||
? `${t('Waffo Pancake product creation failed')}: ${reason}`
|
||
: t('Waffo Pancake product creation failed')
|
||
)
|
||
}
|
||
} catch (err) {
|
||
toast.error(
|
||
`${t('Waffo Pancake product creation failed')}: ${err instanceof Error ? err.message : String(err)}`
|
||
)
|
||
} finally {
|
||
setCreatingPancakeProduct(false)
|
||
}
|
||
}
|
||
|
||
const durationUnitOpts = getDurationUnitOptions(t)
|
||
const resetPeriodOpts = getResetPeriodOptions(t)
|
||
|
||
return (
|
||
<Sheet
|
||
open={open}
|
||
onOpenChange={(v) => {
|
||
onOpenChange(v)
|
||
if (!v) {
|
||
form.reset()
|
||
}
|
||
}}
|
||
>
|
||
<SheetContent className={sideDrawerContentClassName('sm:max-w-[600px]')}>
|
||
<SheetHeader className={sideDrawerHeaderClassName()}>
|
||
<SheetTitle>
|
||
{isEdit ? t('Update plan info') : t('Create new subscription plan')}
|
||
</SheetTitle>
|
||
<SheetDescription>
|
||
{isEdit
|
||
? t('Modify existing subscription plan configuration')
|
||
: t(
|
||
'Fill in the following info to create a new subscription plan'
|
||
)}
|
||
</SheetDescription>
|
||
</SheetHeader>
|
||
<Form {...form}>
|
||
<form
|
||
id='subscription-form'
|
||
onSubmit={form.handleSubmit(onSubmit)}
|
||
className={sideDrawerFormClassName()}
|
||
>
|
||
{/* Basic Info */}
|
||
<SideDrawerSection>
|
||
<h3 className='flex items-center gap-2 text-sm font-medium'>
|
||
<Settings2 className='h-4 w-4' />
|
||
{t('Basic Info')}
|
||
</h3>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='title'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Plan Title')}</FormLabel>
|
||
<FormControl>
|
||
<Input {...field} placeholder={t('e.g. Basic Plan')} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='subtitle'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Plan Subtitle')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
placeholder={t('e.g. Suitable for light usage')}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||
<FormField
|
||
control={form.control}
|
||
name='price_amount'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Actual Amount')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
step='0.01'
|
||
min={0}
|
||
onChange={(e) =>
|
||
field.onChange(parseFloat(e.target.value) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='total_amount'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Received amount')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
min={0}
|
||
onChange={(e) =>
|
||
field.onChange(parseFloat(e.target.value) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormDescription>
|
||
{t(
|
||
'0 means unlimited. The value is converted to quota units when saved.'
|
||
)}
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||
<FormField
|
||
control={form.control}
|
||
name='upgrade_group'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Upgrade Group')}</FormLabel>
|
||
<Select
|
||
items={[
|
||
{ value: '__none__', label: t('No Upgrade') },
|
||
...groupOptions.map((g) => ({ value: g, label: g })),
|
||
]}
|
||
onValueChange={(v) =>
|
||
field.onChange(v === '__none__' ? '' : v)
|
||
}
|
||
value={field.value || ''}
|
||
>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t('No Upgrade')} />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent alignItemWithTrigger={false}>
|
||
<SelectGroup>
|
||
<SelectItem value='__none__'>
|
||
{t('No Upgrade')}
|
||
</SelectItem>
|
||
{groupOptions.map((g) => (
|
||
<SelectItem key={g} value={g}>
|
||
{g}
|
||
</SelectItem>
|
||
))}
|
||
</SelectGroup>
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='max_purchase_per_user'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Purchase Limit')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
min={0}
|
||
onChange={(e) =>
|
||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormDescription>
|
||
{t('0 means unlimited')}
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||
<FormField
|
||
control={form.control}
|
||
name='sort_order'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Sort Order')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
onChange={(e) =>
|
||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='enabled'
|
||
render={({ field }) => (
|
||
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||
<FormLabel className='!mt-0'>
|
||
{t('Enabled Status')}
|
||
</FormLabel>
|
||
<FormControl>
|
||
<Switch
|
||
checked={field.value}
|
||
onCheckedChange={field.onChange}
|
||
/>
|
||
</FormControl>
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
</SideDrawerSection>
|
||
|
||
{/* Duration Settings */}
|
||
<SideDrawerSection>
|
||
<h3 className='flex items-center gap-2 text-sm font-medium'>
|
||
<CalendarClock className='h-4 w-4' />
|
||
{t('Duration Settings')}
|
||
</h3>
|
||
|
||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||
<FormField
|
||
control={form.control}
|
||
name='duration_unit'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Duration Unit')}</FormLabel>
|
||
<Select
|
||
items={[
|
||
...durationUnitOpts.map((o) => ({
|
||
value: o.value,
|
||
label: o.label,
|
||
})),
|
||
]}
|
||
onValueChange={field.onChange}
|
||
value={field.value}
|
||
>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent alignItemWithTrigger={false}>
|
||
<SelectGroup>
|
||
{durationUnitOpts.map((o) => (
|
||
<SelectItem key={o.value} value={o.value}>
|
||
{o.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectGroup>
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{durationUnit === 'custom' ? (
|
||
<FormField
|
||
control={form.control}
|
||
name='custom_seconds'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Custom Seconds')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
min={1}
|
||
onChange={(e) =>
|
||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
) : (
|
||
<FormField
|
||
control={form.control}
|
||
name='duration_value'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Duration Value')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
min={1}
|
||
onChange={(e) =>
|
||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
)}
|
||
</div>
|
||
</SideDrawerSection>
|
||
|
||
{/* Quota Reset */}
|
||
<SideDrawerSection>
|
||
<h3 className='flex items-center gap-2 text-sm font-medium'>
|
||
<RefreshCw className='h-4 w-4' />
|
||
{t('Quota Reset')}
|
||
</h3>
|
||
|
||
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
|
||
<FormField
|
||
control={form.control}
|
||
name='quota_reset_period'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Reset Cycle')}</FormLabel>
|
||
<Select
|
||
items={[
|
||
...resetPeriodOpts.map((o) => ({
|
||
value: o.value,
|
||
label: o.label,
|
||
})),
|
||
]}
|
||
onValueChange={field.onChange}
|
||
value={field.value}
|
||
>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent alignItemWithTrigger={false}>
|
||
<SelectGroup>
|
||
{resetPeriodOpts.map((o) => (
|
||
<SelectItem key={o.value} value={o.value}>
|
||
{o.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectGroup>
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='quota_reset_custom_seconds'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{t('Custom Seconds')}</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
type='number'
|
||
min={0}
|
||
disabled={resetPeriod !== 'custom'}
|
||
onChange={(e) =>
|
||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||
}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
</SideDrawerSection>
|
||
|
||
{/* Payment Config */}
|
||
<SideDrawerSection>
|
||
<h3 className='flex items-center gap-2 text-sm font-medium'>
|
||
<CreditCard className='h-4 w-4' />
|
||
{t('Third-party Payment Config')}
|
||
</h3>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='stripe_price_id'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Stripe Price ID</FormLabel>
|
||
<FormControl>
|
||
<Input {...field} placeholder='price_...' />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='creem_product_id'
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Creem Product ID</FormLabel>
|
||
<FormControl>
|
||
<Input {...field} placeholder='prod_...' />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name='waffo_pancake_product_id'
|
||
render={({ field }) => {
|
||
// Raw-ID fallback for IDs not yet in the catalog.
|
||
const items = pancakeProducts.map((p) => ({
|
||
value: p.id,
|
||
label: `${p.name} (${p.id})`,
|
||
}))
|
||
if (
|
||
field.value &&
|
||
!pancakeProducts.some((p) => p.id === field.value)
|
||
) {
|
||
items.push({ value: field.value, label: field.value })
|
||
}
|
||
return (
|
||
<FormItem>
|
||
<FormLabel>Waffo Pancake Product ID</FormLabel>
|
||
<div className='flex gap-2'>
|
||
<Select
|
||
items={items}
|
||
value={field.value || ''}
|
||
onValueChange={(v) => field.onChange(v)}
|
||
disabled={items.length === 0}
|
||
>
|
||
<SelectTrigger className='w-full flex-1'>
|
||
<SelectValue placeholder={t('Select a product')} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{items.map((item) => (
|
||
<SelectItem key={item.value} value={item.value}>
|
||
{item.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Button
|
||
type='button'
|
||
variant='outline'
|
||
onClick={handleCreatePancakeProduct}
|
||
disabled={
|
||
creatingPancakeProduct || !pancakeCreateReady
|
||
}
|
||
className='shrink-0'
|
||
>
|
||
{creatingPancakeProduct
|
||
? t('Creating...')
|
||
: `+ ${t('Create')}`}
|
||
</Button>
|
||
</div>
|
||
<FormDescription>
|
||
{t(
|
||
'Creates a Pancake product in the saved store using this plan’s title and price. Requires Waffo Pancake to be fully configured in Payment settings first.'
|
||
)}
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)
|
||
}}
|
||
/>
|
||
</SideDrawerSection>
|
||
</form>
|
||
</Form>
|
||
<SheetFooter className={sideDrawerFooterClassName()}>
|
||
<SheetClose render={<Button variant='outline' />}>
|
||
{t('Close')}
|
||
</SheetClose>
|
||
<Button
|
||
form='subscription-form'
|
||
type='submit'
|
||
disabled={isSubmitting}
|
||
>
|
||
{isSubmitting ? t('Saving...') : t('Save changes')}
|
||
</Button>
|
||
</SheetFooter>
|
||
</SheetContent>
|
||
</Sheet>
|
||
)
|
||
}
|