From 50a1bbc4b18cc503ef8807dc99cc48c137527a17 Mon Sep 17 00:00:00 2001 From: Federico Luzzi Date: Mon, 30 Oct 2023 14:15:41 +0000 Subject: [PATCH] feat: first base implementation of a drafts feature (#472) * feat(ui): export number format values type from number input * chore: add changeset * feat(sdk): split full and partial kpi token specification types * feat(sdk): introduce serializable type and kpi token internal state types * feat(frontend): introduce kpi token creation form state management --------- Co-authored-by: luzzifoss --- .changeset/slimy-sloths-visit.md | 5 ++ .changeset/smooth-rice-double.md | 6 ++ .changeset/strange-beds-add.md | 5 ++ .changeset/thick-apes-double.md | 6 ++ .../pages/create-with-template-id/index.tsx | 32 ++++++++++- .../components/kpi-token-creation-form.tsx | 9 ++- .../src/components/oracle-creation-form.tsx | 4 +- packages/react/src/types/templates.ts | 56 +++++++++++++------ packages/sdk/src/entities/kpi-token.ts | 11 ++-- packages/ui/src/components/number-input.tsx | 2 + 10 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 .changeset/slimy-sloths-visit.md create mode 100644 .changeset/smooth-rice-double.md create mode 100644 .changeset/strange-beds-add.md create mode 100644 .changeset/thick-apes-double.md diff --git a/.changeset/slimy-sloths-visit.md b/.changeset/slimy-sloths-visit.md new file mode 100644 index 000000000..c3df93ea2 --- /dev/null +++ b/.changeset/slimy-sloths-visit.md @@ -0,0 +1,5 @@ +--- +"@carrot-kpi/host-frontend": minor +--- + +Introduce internal state management for KPI token creation forms. diff --git a/.changeset/smooth-rice-double.md b/.changeset/smooth-rice-double.md new file mode 100644 index 000000000..2ec6c298c --- /dev/null +++ b/.changeset/smooth-rice-double.md @@ -0,0 +1,6 @@ +--- +"@carrot-kpi/react": minor +--- + +Include state and state change handler in KPI token creation form types and +introduce Serializable type to represent internal template states diff --git a/.changeset/strange-beds-add.md b/.changeset/strange-beds-add.md new file mode 100644 index 000000000..f78d8e207 --- /dev/null +++ b/.changeset/strange-beds-add.md @@ -0,0 +1,5 @@ +--- +"@carrot-kpi/ui": patch +--- + +Export NumberFormatValues type from number input component diff --git a/.changeset/thick-apes-double.md b/.changeset/thick-apes-double.md new file mode 100644 index 000000000..13e55133b --- /dev/null +++ b/.changeset/thick-apes-double.md @@ -0,0 +1,6 @@ +--- +"@carrot-kpi/sdk": minor +--- + +Split KPITokenSpecification and FullKPITokenSpecification into 2 different +types, respectively without and with the ipfsHash property diff --git a/packages/frontend/src/pages/create-with-template-id/index.tsx b/packages/frontend/src/pages/create-with-template-id/index.tsx index 54b90678e..938966682 100644 --- a/packages/frontend/src/pages/create-with-template-id/index.tsx +++ b/packages/frontend/src/pages/create-with-template-id/index.tsx @@ -16,6 +16,7 @@ import { useInvalidateLatestKPITokens } from "../../hooks/useInvalidateLatestKPI import { Layout } from "../../components/layout"; import { Permissioned } from "../../components/permissioned"; import { useIsCreatorAllowed } from "../../hooks/useIsCreatorAllowed"; +import type { Serializable } from "@carrot-kpi/react"; export const CreateWithTemplateId = () => { const { i18n, t } = useTranslation(); @@ -29,13 +30,20 @@ export const CreateWithTemplateId = () => { const preferDecentralization = usePreferDecentralization(); const ipfsGatewayURL = useIPFSGatewayURL(); const invalidateLatestKPITokens = useInvalidateLatestKPITokens(); + const pinningProxyAuthenticated = useIsPinningProxyAuthenticated(); const [loading, setLoading] = useState(true); const [template, setTemplate] = useState( state && "specification" in state.template ? state.template : null, ); const [formKey, setFormKey] = useState(0); - const pinningProxyAuthenticated = useIsPinningProxyAuthenticated(); + const [draftState, setDraftState] = useState<{ + templateId?: number; + state: Serializable; + }>({ + templateId: undefined, + state: {}, + }); const { allowed: creatorAllowed, loading: loadingPermission } = useIsCreatorAllowed(address); @@ -54,6 +62,10 @@ export const CreateWithTemplateId = () => { useEffect(() => { if (!!state?.template) { + setDraftState((previousState) => ({ + templateId: state.template.id, + state: previousState.state, + })); setTemplate(state.template); return; } @@ -93,7 +105,12 @@ export const CreateWithTemplateId = () => { if (!cancelled) setTemplate(null); return; } - if (!cancelled) setTemplate(resolvedTemplates[0]); + const resolvedTemplate = resolvedTemplates[0]; + setDraftState((previousState) => ({ + templateId: resolvedTemplate.id, + state: previousState.state, + })); + if (!cancelled) setTemplate(resolvedTemplate); } catch (error) { console.error( `could not fetch template with id ${templateId}`, @@ -116,6 +133,13 @@ export const CreateWithTemplateId = () => { templateId, ]); + const handleChange = useCallback((state: Serializable) => { + setDraftState((prevState) => ({ + templateId: prevState.templateId, + state, + })); + }, []); + const handleCreate = useCallback(() => { invalidateLatestKPITokens(); }, [invalidateLatestKPITokens]); @@ -142,7 +166,7 @@ export const CreateWithTemplateId = () => { ) : template ? ( @@ -164,6 +188,8 @@ export const CreateWithTemplateId = () => { } i18n={i18n} className={{ root: "w-full h-full" }} + state={draftState.state} + onChange={handleChange} onCreate={handleCreate} navigate={navigate} onTx={addTransaction} diff --git a/packages/react/src/components/kpi-token-creation-form.tsx b/packages/react/src/components/kpi-token-creation-form.tsx index cea72800c..95eee9b8f 100644 --- a/packages/react/src/components/kpi-token-creation-form.tsx +++ b/packages/react/src/components/kpi-token-creation-form.tsx @@ -1,16 +1,19 @@ import React from "react"; import type { ReactElement } from "react"; -import type { KPITokenCreationFormProps } from "../types/templates"; +import type { + KPITokenCreationFormProps, + Serializable, +} from "../types/templates"; import { TemplateComponent } from "./template-component"; -export function KPITokenCreationForm({ +export function KPITokenCreationForm({ template, fallback, error, i18n, className, ...additionalProps -}: KPITokenCreationFormProps): ReactElement { +}: KPITokenCreationFormProps): ReactElement { return ( ({ +export function OracleCreationForm({ template, fallback, error, diff --git a/packages/react/src/types/templates.ts b/packages/react/src/types/templates.ts index bd703503e..2af87ef67 100644 --- a/packages/react/src/types/templates.ts +++ b/packages/react/src/types/templates.ts @@ -11,6 +11,14 @@ import { ResolvedTemplate } from "@carrot-kpi/sdk"; import type { ReactElement, ReactNode } from "react"; import type { Hex } from "viem"; +export type Serializable = + | null + | string + | number + | boolean + | { [key: string | symbol | number]: Serializable } + | Serializable[]; + export type TemplateEntity = "kpiToken" | "oracle"; export type TemplateType = "creationForm" | "page"; @@ -46,37 +54,50 @@ export interface OracleInitializationBundle { export type OracleInitializationBundleGetter = () => Promise; -export type OracleChangeCallback = ( - internalState: Partial, +export type OracleChangeCallback = ( + internalState: S, initializationBundleGetter?: OracleInitializationBundleGetter, ) => void; -export interface AdditionalRemoteOracleCreationFormProps { - state: Partial; +export interface AdditionalRemoteOracleCreationFormProps< + S extends Serializable = Serializable, +> { + state: S; kpiToken?: Partial; - onChange: OracleChangeCallback; + onChange: OracleChangeCallback; navigate: NavigateFunction; onTx: (tx: Tx) => void; } -export type OracleRemoteCreationFormProps = - BaseRemoteTemplateComponentProps & - AdditionalRemoteOracleCreationFormProps; - -export type OracleCreationFormProps = TemplateComponentProps & +export type OracleRemoteCreationFormProps< + S extends Serializable = Serializable, +> = BaseRemoteTemplateComponentProps & AdditionalRemoteOracleCreationFormProps; -export interface AdditionalRemoteKPITokenCreationFormProps { +export type KPITokenChangeCallback = ( + state: S, +) => void; + +export type OracleCreationFormProps = + TemplateComponentProps & AdditionalRemoteOracleCreationFormProps; + +export interface AdditionalRemoteKPITokenCreationFormProps< + S extends Serializable = Serializable, +> { + state: S; + onChange: KPITokenChangeCallback; onCreate: () => void; navigate: NavigateFunction; onTx: (tx: Tx) => void; } -export type KPITokenRemoteCreationFormProps = BaseRemoteTemplateComponentProps & - AdditionalRemoteKPITokenCreationFormProps; +export type KPITokenRemoteCreationFormProps< + S extends Serializable = Serializable, +> = BaseRemoteTemplateComponentProps & + AdditionalRemoteKPITokenCreationFormProps; -export type KPITokenCreationFormProps = TemplateComponentProps & - AdditionalRemoteKPITokenCreationFormProps; +export type KPITokenCreationFormProps = + TemplateComponentProps & AdditionalRemoteKPITokenCreationFormProps; export interface AdditionalRemoteOraclePageProps { oracle?: ResolvedOracleWithData | null; @@ -104,12 +125,13 @@ export type KPITokenPageProps = TemplateComponentProps & export type RemoteComponentProps< E extends TemplateEntity, T extends TemplateType, + S extends Serializable = Serializable, > = E extends "kpiToken" ? T extends "creationForm" - ? KPITokenRemoteCreationFormProps + ? KPITokenRemoteCreationFormProps : KPITokenRemotePageProps : E extends "oracle" ? T extends "creationForm" - ? OracleRemoteCreationFormProps + ? OracleRemoteCreationFormProps : OracleRemotePageProps : never; diff --git a/packages/sdk/src/entities/kpi-token.ts b/packages/sdk/src/entities/kpi-token.ts index 6a892e813..bd3086ea9 100644 --- a/packages/sdk/src/entities/kpi-token.ts +++ b/packages/sdk/src/entities/kpi-token.ts @@ -4,12 +4,15 @@ import { ChainId } from "../commons"; import { type Address, type Hex } from "viem"; export interface KPITokenSpecification { - ipfsHash: string; title: string; description: string; tags: string[]; } +export interface FullKPITokenSpecification extends KPITokenSpecification { + ipfsHash: string; +} + export class BaseKPIToken { constructor( public readonly chainId: ChainId, @@ -57,7 +60,7 @@ export class ResolvedKPIToken extends BaseKPIToken { owner: Address, public readonly template: ResolvedTemplate, public readonly oracles: ResolvedOracle[], - public readonly specification: KPITokenSpecification, + public readonly specification: FullKPITokenSpecification, expiration: number, creationTimestamp: number, finalized: boolean, @@ -76,7 +79,7 @@ export class ResolvedKPIToken extends BaseKPIToken { kpiToken: KPIToken, template: ResolvedTemplate, oracles: ResolvedOracle[], - specification: KPITokenSpecification, + specification: FullKPITokenSpecification, ) { return new ResolvedKPIToken( kpiToken.chainId, @@ -99,7 +102,7 @@ export class ResolvedKPITokenWithData extends BaseKPIToken { owner: Address, public readonly template: ResolvedTemplate, public readonly oracles: ResolvedOracleWithData[], - public readonly specification: KPITokenSpecification, + public readonly specification: FullKPITokenSpecification, expiration: number, creationTimestamp: number, finalized: boolean, diff --git a/packages/ui/src/components/number-input.tsx b/packages/ui/src/components/number-input.tsx index 983635eb1..966f02900 100644 --- a/packages/ui/src/components/number-input.tsx +++ b/packages/ui/src/components/number-input.tsx @@ -7,6 +7,8 @@ import { BaseInputWrapper, } from "./commons/input"; +export { type NumberFormatValues } from "react-number-format"; + export type NumberInputProps = Omit< NumericFormatProps & BaseInputProps, "size" | "id" | "className"