Skip to content

Commit

Permalink
Merge pull request #13 from storyprotocol/don/modify-create-license-i…
Browse files Browse the repository at this point in the history
…nput

Modify create and configure license UI
  • Loading branch information
DonFungible authored Dec 18, 2023
2 parents 7bed682 + a7120a8 commit 2fc0b6b
Show file tree
Hide file tree
Showing 7 changed files with 791 additions and 45 deletions.
363 changes: 363 additions & 0 deletions app/admin/ConfigureLicenseWriteAccordionInputForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
'use client';

import { useEffect, useState } from 'react';
import { ConfigureLicenseRequest } from '@story-protocol/core-sdk';
import { useAccount, useNetwork } from 'wagmi';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import * as z from 'zod';
import { useForm } from 'react-hook-form';

import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Input } from '@/components/ui/input';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { ArrowPathIcon, CheckIcon, ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import EtherscanLink from '@/utils/EtherscanLink';
import { cn } from '@/utils';

type InputFormProps = {
formSchema: z.ZodObject<any, any>;
hook: any;
fcnName: string;
description?: string;
onSuccessDisplay?: any;
defaultValues?: Record<string, any>;
descriptions?: Record<string, any>;
placeholders?: Record<string, any>;
};

function getDefaultValuesFromSchema(
schema: z.ZodObject<any, any>,
defaultValues: Record<string, any>,
): Record<string, any> {
const shape = schema.shape;
const values: Record<string, any> = {};

for (const key in shape) {
values[key] = defaultValues[key] || '';
}

for (const key in defaultValues) {
if (!values[key]) {
values[key] = defaultValues[key];
}
}

return values;
}

function getZodTypeName(type: z.ZodType<any, any>): string {
if (type instanceof z.ZodString) return 'string';
if (type instanceof z.ZodNumber) return 'number';
if (type instanceof z.ZodBoolean) return 'boolean';
if (type instanceof z.ZodNativeEnum) return 'enum';
if (type instanceof z.ZodArray) return 'array';
return '';
}

type EnumDropdownProps = {
field: any;
enumType: Record<string, number | string>;
};

function EnumDropdown({ field, enumType }: EnumDropdownProps) {
const [selectedValue, setSelectedValue] = useState('');

const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = event.target.value;
setSelectedValue(selectedValue);
const isEnumOfNumbers = typeof enumType[selectedValue] === 'number';

const convertedValue = isEnumOfNumbers ? Number(enumType[selectedValue]) : selectedValue;
console.log(convertedValue);
field.onChange(convertedValue);
};

const enumKeys = Object.keys(enumType).filter((key) => isNaN(Number(key)));

return (
<div className="relative border-2 rounded-xl">
<select
{...field}
value={selectedValue}
onChange={handleChange}
className="block appearance-none w-full px-4 py-2 pr-8 rounded-xl bg-transparent focus:outline-none"
>
{enumKeys.map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-gray-700">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M5.293 9.293a1 1 0 0 1 1.414 0L10 12.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z" />
</svg>
</div>
</div>
);
}

type BooleanDropdownProps = {
field: any;
};

const BooleanDropdown: React.FC<BooleanDropdownProps> = ({ field }) => {
const [hasSelection, setHasSelection] = useState(false);

useEffect(() => {
// Ensure the initial field value is undefined
if (!hasSelection && field.value !== undefined) {
field.onChange(undefined);
}
}, [hasSelection, field]);

const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = event.target.value === 'true';
field.onChange(selectedValue);
setHasSelection(true);
};

return (
<div className="relative border-2 rounded-xl">
<select
{...field}
value={field.value === undefined ? '' : field.value.toString()} // Set initial value as empty string
onChange={handleSelectChange}
className="block appearance-none w-full px-4 py-2 pr-8 rounded-xl bg-transparent focus:outline-none"
>
{!hasSelection && <option value="">Select...</option>}
<option value="true">True</option>
<option value="false">False</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-gray-700">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M5.293 9.293a1 1 0 0 1 1.414 0L10 12.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z" />
</svg>
</div>
</div>
);
};

type NestedFieldProps = {
control: any;
name: string;
schema: z.ZodType<any, any>;
defaultValue?: Record<string, any>;
description?: string;
placeholder?: string;
};

function isZodOptional(type: z.ZodType<any, any>): boolean {
return type.isOptional();
}

function NestedField({ control, name, schema, defaultValue, description, placeholder }: NestedFieldProps) {
const typeName = getZodTypeName(schema);
const optionalText = isZodOptional(schema) ? ' (optional)' : '';

const isReadOnly = defaultValue && defaultValue[name] !== undefined; // Check if a default value exists for the current field

if (schema instanceof z.ZodObject) {
return (
<div style={{ border: '1px solid #e2e8f0', margin: '10px 0', padding: '10px' }} className="rounded-xl">
<h3 className="pb-4">{name.split('.').pop()}</h3> {/* Displaying the name of the nested object */}
{Object.keys(schema.shape).map((key) => (
<NestedField
key={key}
control={control}
name={`${name}.${key}`}
schema={schema.shape[key]}
defaultValue={defaultValue && defaultValue[key]} // Pass the default value for nested fields
placeholder={placeholder}
/>
))}
</div>
);
}

return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className="pb-4">
<FormLabel className="pb-4">
{name.split('.').pop()} {Boolean(optionalText.length) && optionalText}
{!(schema instanceof z.ZodOptional) && <span className="text-red-500">*</span>}
</FormLabel>
<FormControl className="">
{typeName === 'enum' ? (
<EnumDropdown field={field} enumType={(schema as z.ZodNativeEnum<any>)._def.values} />
) : typeName === 'boolean' ? ( // Check if the type is boolean
<BooleanDropdown field={field} />
) : (
<Input placeholder={placeholder || `Enter ${name}`} {...field} readOnly={isReadOnly} />
)}
</FormControl>
<FormMessage />
{description && <FormDescription>{description}</FormDescription>}
</FormItem>
)}
/>
);
}

export default function ConfigureLicenseWriteAccordionInputForm({
formSchema,
hook,
fcnName,
description = '',
defaultValues = {},
onSuccessDisplay = <></>,
descriptions = {},
placeholders = {},
}: InputFormProps) {
const { chain } = useNetwork();
const { isConnected } = useAccount();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: getDefaultValuesFromSchema(formSchema, defaultValues),
});

async function onSubmit(values: z.infer<typeof formSchema>, event: any) {
event.preventDefault();
await execute();
}

const configureLicenseReq: ConfigureLicenseRequest = {
ipOrg: form.getValues().ipOrg,
frameworkId: form.getValues().frameworkId,
licensor: form.getValues().licensor,
params: [
{
tag: 'Attribution',
value: {
interface: 'bool',
data: [form.getValues().attribution],
},
},
],
txOptions: {
waitForTransaction: true,
},
};
const { execute, isIdle, isLoading, isSuccess, data, reset, errorMsg } = hook(configureLicenseReq);

const IdleComponent = () => (
<>
<ArrowPathIcon className="animate-spin w-full mx-auto mb-4 text-yellow-500" width={40} height={40} />
<p>Please confirm the transaction in your wallet.</p>
</>
);
const LoadingComponent = () => (
<>
<ArrowPathIcon className="animate-spin w-full mx-auto mb-4 text-blue-500" width={40} height={40} />
<p>
Transaction in progress... <EtherscanLink txHash={data?.txHash} chainId={chain?.id} />
</p>
</>
);

const SuccessComponent = () => (
<>
<CheckIcon className=" w-full mx-auto text-green-500 mb-4" width={40} height={40} />
<p>Transaction successful!</p>
{onSuccessDisplay}
</>
);

const FailedComponent = () => {
console.log('Failed:', data);
return (
<>
<XCircleIcon className=" w-full mx-auto text-red-500 mb-4" width={40} height={40} />
<div className="flex flex-col">
<p>Transaction failed.</p>
{errorMsg && <span className="max-w-2xl break-all">{errorMsg}</span>}
<EtherscanLink txHash={data?.txHash} chainId={chain?.id} />
</div>
</>
);
};

const DialogComponent = isIdle
? IdleComponent
: isLoading
? LoadingComponent
: isSuccess
? SuccessComponent
: FailedComponent;

return (
<>
<Dialog onOpenChange={() => reset()}>
<DialogContent>
<DialogHeader>
<DialogTitle className="w-full text-center pb-4">Transaction Status</DialogTitle>
<DialogDescription className="w-full text-center flex flex-col">
<DialogComponent />
</DialogDescription>
</DialogHeader>
</DialogContent>
<Accordion
type="single"
collapsible
className="rounded-t-xl rounded-l-xl rounded-r-xl border-t-2 border-l-2 border-r-2 border-b-2 bg-white"
>
<AccordionItem value="item-1 border-b-2" className="">
<AccordionTrigger className="px-4">{fcnName}</AccordionTrigger>
<AccordionContent className="px-4 pt-4 bg-white rounded-b-xl">
{description && (
<section className="flex flex-row gap-2 pb-2">
<ExclamationCircleIcon className="w-5 h-5 text-yellow-500" />
<p className="pb-4 flex my-auto">{description}</p>
</section>
)}
{isConnected ? (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{Object.keys(formSchema.shape).map((key) => (
<NestedField
key={key}
control={form.control}
name={key}
schema={formSchema.shape[key]}
description={descriptions[key]}
placeholder={placeholders[key]}
/>
))}
<DialogTrigger
type="submit"
disabled={!form.formState.isValid}
className={cn(
'px-4 py-2 border-2 rounded-xl ',
form.formState.isValid ? 'bg-sp-purple/80 text-white hover:bg-sp-purple' : 'bg-black/10',
)}
>
Submit
</DialogTrigger>
</form>
</Form>
) : (
<section className="flex flex-col gap-4">
<p>Connect your wallet first to perform a transaction</p>
{/* <ConnectWalletButton /> */}
<ConnectButton />
</section>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</Dialog>
</>
);
}
Loading

0 comments on commit 2fc0b6b

Please sign in to comment.