From ac618f9a328df586920e58e87e6f461d27a2e17e Mon Sep 17 00:00:00 2001 From: Camila Sosa Morales Date: Wed, 29 Mar 2023 18:13:22 -0300 Subject: [PATCH] feat: form field validations (#190) * feat: add form field validation * chore: add form on repo configuration * wip: setting default branch * chore: set default branch * chore: form field validation workig with some fix needed * chore: fix change first github step * feat: set default branch * feat: validation for textarea. fix styles on select repository * chore: PR comments changes * chore: remove constant * chore: added comments * chore: change combobox input props * chore: remove ens validation since we dont allow custom ens * chore: remove isEns * refactor: fetch ens list from ens graph --- subgraph/src/access-point.ts | 213 +++++++------ subgraph/src/metadata-update.ts | 240 +++++++------- subgraph/src/mint.ts | 145 +++++---- subgraph/src/transfer.ts | 113 +++---- subgraph/tests/matchstick/.latest.json | 2 +- ui/.graphclientrc.yml | 4 + ui/graphql/queries.graphql | 9 + ui/src/components/card/card.tsx | 6 +- .../core/color-picker}/color-picker.tsx | 24 +- ui/src/components/core/color-picker/index.ts | 1 + ui/src/components/core/combobox/combobox.tsx | 291 +++++++++-------- ui/src/components/core/index.ts | 1 + .../core/input/input-file.styles.ts | 32 ++ ui/src/components/core/input/input-file.tsx | 39 +-- ui/src/components/form/form-field.context.ts | 15 + ui/src/components/form/form.context.tsx | 79 +++++ ui/src/components/form/form.styles.ts | 9 + ui/src/components/form/form.tsx | 297 ++++++++++++++++-- ui/src/components/form/form.utils.ts | 19 ++ ui/src/components/form/index.ts | 1 + ui/src/declarations/index.d.ts | 5 + ui/src/mocks/list.ts | 15 +- .../ens/async-thunk/fetch-ens-names.ts | 1 + .../github/async-thunk/fetch-repositories.ts | 3 +- ui/src/utils/index.ts | 1 + ui/src/utils/string-validators.ts | 73 +++++ .../views/components-test/combobox-test.tsx | 15 +- .../repo-branch-commit-fields.tsx | 88 ------ .../repo-configuration-body/index.ts | 1 + .../repo-branch-commit-fields.tsx | 95 ++++++ .../repo-configuration-body.tsx | 15 +- .../repo-configuration-header.tsx | 15 +- .../github-repository-selection.tsx | 34 +- .../repository.tsx | 6 +- .../users-orgs-combobox.tsx | 11 +- ui/src/views/mint/mint-stepper.tsx | 54 ++-- ui/src/views/mint/mint.context.tsx | 51 +-- ui/src/views/mint/mint.tsx | 33 +- .../fields/app-description-field.tsx | 25 +- .../form-step/fields/app-name-field.tsx | 25 +- .../fields/ens-domain-field/domain-field.tsx | 20 +- .../fields/ens-domain-field/ens-field.tsx | 69 ++-- .../fields/logo-field/logo-field.tsx | 41 +-- .../mint/nfa-step/form-step/form.utils.ts | 13 - .../views/mint/nfa-step/form-step/index.tsx | 1 + .../nfa-step/form-step/mint-form.context.ts | 55 ++++ .../mint/nfa-step/form-step/mint-form.tsx | 58 ++-- ui/src/views/mint/nft-card/nft-card.tsx | 24 +- ui/tailwind.config.js | 1 + 49 files changed, 1478 insertions(+), 910 deletions(-) rename ui/src/{views/mint/nfa-step/form-step/fields/logo-field => components/core/color-picker}/color-picker.tsx (76%) create mode 100644 ui/src/components/core/color-picker/index.ts create mode 100644 ui/src/components/core/input/input-file.styles.ts create mode 100644 ui/src/components/form/form-field.context.ts create mode 100644 ui/src/components/form/form.context.tsx create mode 100644 ui/src/components/form/form.utils.ts create mode 100644 ui/src/declarations/index.d.ts create mode 100644 ui/src/utils/string-validators.ts delete mode 100644 ui/src/views/mint/github-step/steps/github-repo-configuration/repo-branch-commit-fields.tsx create mode 100644 ui/src/views/mint/github-step/steps/github-repo-configuration/repo-configuration-body/index.ts create mode 100644 ui/src/views/mint/github-step/steps/github-repo-configuration/repo-configuration-body/repo-branch-commit-fields.tsx rename ui/src/views/mint/github-step/steps/github-repo-configuration/{ => repo-configuration-body}/repo-configuration-body.tsx (71%) create mode 100644 ui/src/views/mint/nfa-step/form-step/mint-form.context.ts diff --git a/subgraph/src/access-point.ts b/subgraph/src/access-point.ts index 7d598cc..bda9ba1 100644 --- a/subgraph/src/access-point.ts +++ b/subgraph/src/access-point.ts @@ -1,26 +1,23 @@ import { - Address, - Bytes, - log, - store, - ethereum, - BigInt, + Address, + Bytes, + log, + store, + ethereum, + BigInt, } from '@graphprotocol/graph-ts'; // Event Imports [based on the yaml config] import { - ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent, - ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent, - NewAccessPoint as NewAccessPointEvent, - ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent, - ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent, + ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent, + ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent, + NewAccessPoint as NewAccessPointEvent, + ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent, + ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent, } from '../generated/FleekNFA/FleekNFA'; // Entity Imports [based on the schema] -import { - AccessPoint, - Owner, -} from '../generated/schema'; +import { AccessPoint, Owner } from '../generated/schema'; /** * This handler will create and load entities in the following order: @@ -29,133 +26,133 @@ import { * Note to discuss later: Should a `NewAccessPoint` entity be also created and defined? */ export function handleNewAccessPoint(event: NewAccessPointEvent): void { - // Create an AccessPoint entity - let accessPointEntity = new AccessPoint(event.params.apName); - accessPointEntity.score = BigInt.fromU32(0); - accessPointEntity.contentVerified = false; - accessPointEntity.nameVerified = false; - accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler. - accessPointEntity.owner = event.params.owner; - accessPointEntity.token = Bytes.fromByteArray( - Bytes.fromBigInt(event.params.tokenId) - ); + // Create an AccessPoint entity + let accessPointEntity = new AccessPoint(event.params.apName); + accessPointEntity.score = BigInt.fromU32(0); + accessPointEntity.contentVerified = false; + accessPointEntity.nameVerified = false; + accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler. + accessPointEntity.owner = event.params.owner; + accessPointEntity.token = Bytes.fromByteArray( + Bytes.fromBigInt(event.params.tokenId) + ); - // Load / Create an Owner entity - let ownerEntity = Owner.load(event.params.owner); + // Load / Create an Owner entity + let ownerEntity = Owner.load(event.params.owner); - if (!ownerEntity) { - // Create a new owner entity - ownerEntity = new Owner(event.params.owner); - // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. - ownerEntity.collection = false; - } + if (!ownerEntity) { + // Create a new owner entity + ownerEntity = new Owner(event.params.owner); + // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. + ownerEntity.collection = false; + } - // Save entities. - accessPointEntity.save(); - ownerEntity.save(); + // Save entities. + accessPointEntity.save(); + ownerEntity.save(); } /** * This handler will update the status of an access point entity. */ export function handleChangeAccessPointCreationStatus( - event: ChangeAccessPointCreationStatusEvent + event: ChangeAccessPointCreationStatusEvent ): void { - // Load the AccessPoint entity - let accessPointEntity = AccessPoint.load(event.params.apName); - let status = event.params.status; + // Load the AccessPoint entity + let accessPointEntity = AccessPoint.load(event.params.apName); + let status = event.params.status; - if (accessPointEntity) { - switch (status) { - case 0: - accessPointEntity.creationStatus = 'DRAFT'; - break; - case 1: - accessPointEntity.creationStatus = 'APPROVED'; - break; - case 2: - accessPointEntity.creationStatus = 'REJECTED'; - break; - case 3: - accessPointEntity.creationStatus = 'REMOVED'; - break; - default: - // Unknown status - log.error( - 'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}', - [status.toString(), event.params.apName] - ); - } + if (accessPointEntity) { + switch (status) { + case 0: + accessPointEntity.creationStatus = 'DRAFT'; + break; + case 1: + accessPointEntity.creationStatus = 'APPROVED'; + break; + case 2: + accessPointEntity.creationStatus = 'REJECTED'; + break; + case 3: + accessPointEntity.creationStatus = 'REMOVED'; + break; + default: + // Unknown status + log.error( + 'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}', + [status.toString(), event.params.apName] + ); + } - accessPointEntity.save(); - } else { - // Unknown access point - log.error( - 'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}', - [status.toString(), event.params.apName] - ); - } + accessPointEntity.save(); + } else { + // Unknown access point + log.error( + 'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}', + [status.toString(), event.params.apName] + ); + } } /** * This handler will update the score of an access point entity. */ export function handleChangeAccessPointScore( - event: ChangeAccessPointCreationScoreEvent + event: ChangeAccessPointCreationScoreEvent ): void { - // Load the AccessPoint entity - let accessPointEntity = AccessPoint.load(event.params.apName); + // Load the AccessPoint entity + let accessPointEntity = AccessPoint.load(event.params.apName); - if (accessPointEntity) { - accessPointEntity.score = event.params.score; - accessPointEntity.save(); - } else { - // Unknown access point - log.error( - 'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}', - [event.params.score.toString(), event.params.apName] - ); - } + if (accessPointEntity) { + accessPointEntity.score = event.params.score; + accessPointEntity.save(); + } else { + // Unknown access point + log.error( + 'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}', + [event.params.score.toString(), event.params.apName] + ); + } } /** * This handler will update the nameVerified field of an access point entity. */ export function handleChangeAccessPointNameVerify( - event: ChangeAccessPointNameVerifyEvent + event: ChangeAccessPointNameVerifyEvent ): void { - // Load the AccessPoint entity - let accessPointEntity = AccessPoint.load(event.params.apName); + // Load the AccessPoint entity + let accessPointEntity = AccessPoint.load(event.params.apName); - if (accessPointEntity) { - accessPointEntity.nameVerified = event.params.verified; - accessPointEntity.save(); - } else { - // Unknown access point - log.error( - 'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}', - [event.params.verified.toString(), event.params.apName] - ); - } + if (accessPointEntity) { + accessPointEntity.nameVerified = event.params.verified; + accessPointEntity.save(); + } else { + // Unknown access point + log.error( + 'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}', + [event.params.verified.toString(), event.params.apName] + ); + } } /** * This handler will update the contentVerified field of an access point entity. */ export function handleChangeAccessPointContentVerify( - event: ChangeAccessPointContentVerifyEvent + event: ChangeAccessPointContentVerifyEvent ): void { - // Load the AccessPoint entity - let accessPointEntity = AccessPoint.load(event.params.apName); + // Load the AccessPoint entity + let accessPointEntity = AccessPoint.load(event.params.apName); - if (accessPointEntity) { - accessPointEntity.contentVerified = event.params.verified; - accessPointEntity.save(); - } else { - // Unknown access point - log.error( - 'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}', - [event.params.verified.toString(), event.params.apName] - ); - } + if (accessPointEntity) { + accessPointEntity.contentVerified = event.params.verified; + accessPointEntity.save(); + } else { + // Unknown access point + log.error( + 'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}', + [event.params.verified.toString(), event.params.apName] + ); + } } diff --git a/subgraph/src/metadata-update.ts b/subgraph/src/metadata-update.ts index 91f6ebf..5ab2b5b 100644 --- a/subgraph/src/metadata-update.ts +++ b/subgraph/src/metadata-update.ts @@ -2,163 +2,163 @@ import { Bytes } from '@graphprotocol/graph-ts'; // Event Imports [based on the yaml config] import { - MetadataUpdate as MetadataUpdateEvent, - MetadataUpdate1 as MetadataUpdateEvent1, - MetadataUpdate2 as MetadataUpdateEvent2, - MetadataUpdate3 as MetadataUpdateEvent3, - MetadataUpdate4 as MetadataUpdateEvent4, + MetadataUpdate as MetadataUpdateEvent, + MetadataUpdate1 as MetadataUpdateEvent1, + MetadataUpdate2 as MetadataUpdateEvent2, + MetadataUpdate3 as MetadataUpdateEvent3, + MetadataUpdate4 as MetadataUpdateEvent4, } from '../generated/FleekNFA/FleekNFA'; // Entity Imports [based on the schema] import { - GitRepository as GitRepositoryEntity, - MetadataUpdate, - Token, + GitRepository as GitRepositoryEntity, + MetadataUpdate, + Token, } from '../generated/schema'; export function handleMetadataUpdateWithStringValue( - event: MetadataUpdateEvent1 + event: MetadataUpdateEvent1 ): void { - /** - * Metadata handled here: - * setTokenExternalURL - * setTokenENS - * setTokenName - * setTokenDescription - * setTokenLogo - * */ - let entity = new MetadataUpdate( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); + /** + * Metadata handled here: + * setTokenExternalURL + * setTokenENS + * setTokenName + * setTokenDescription + * setTokenLogo + * */ + let entity = new MetadataUpdate( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); - entity.tokenId = event.params._tokenId; - entity.key = event.params.key; - entity.stringValue = event.params.value; - entity.blockNumber = event.block.number; - entity.blockTimestamp = event.block.timestamp; - entity.transactionHash = event.transaction.hash; + entity.tokenId = event.params._tokenId; + entity.key = event.params.key; + entity.stringValue = event.params.value; + entity.blockNumber = event.block.number; + entity.blockTimestamp = event.block.timestamp; + entity.transactionHash = event.transaction.hash; - entity.save(); + entity.save(); - // UPDATE TOKEN - let token = Token.load( - Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) - ); + // UPDATE TOKEN + let token = Token.load( + Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) + ); - if (token) { - if (event.params.key == 'externalURL') { - token.externalURL = event.params.value; - } else if (event.params.key == 'ENS') { - token.ENS = event.params.value; - } else if (event.params.key == 'name') { - token.name = event.params.value; - } else if (event.params.key == 'description') { - token.description = event.params.value; - } else { - // logo - token.logo = event.params.value; - } - token.save(); + if (token) { + if (event.params.key == 'externalURL') { + token.externalURL = event.params.value; + } else if (event.params.key == 'ENS') { + token.ENS = event.params.value; + } else if (event.params.key == 'name') { + token.name = event.params.value; + } else if (event.params.key == 'description') { + token.description = event.params.value; + } else { + // logo + token.logo = event.params.value; } + token.save(); + } } export function handleMetadataUpdateWithDoubleStringValue( - event: MetadataUpdateEvent3 + event: MetadataUpdateEvent3 ): void { - /** - * setTokenBuild - */ - let entity = new MetadataUpdate( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); + /** + * setTokenBuild + */ + let entity = new MetadataUpdate( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); - entity.key = event.params.key; - entity.tokenId = event.params._tokenId; - entity.doubleStringValue = event.params.value; - entity.blockNumber = event.block.number; - entity.blockTimestamp = event.block.timestamp; - entity.transactionHash = event.transaction.hash; + entity.key = event.params.key; + entity.tokenId = event.params._tokenId; + entity.doubleStringValue = event.params.value; + entity.blockNumber = event.block.number; + entity.blockTimestamp = event.block.timestamp; + entity.transactionHash = event.transaction.hash; - entity.save(); + entity.save(); - // UPDATE TOKEN - let token = Token.load( - Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) - ); + // UPDATE TOKEN + let token = Token.load( + Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) + ); - if (token) { - if (event.params.key == 'build') { - let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]); - if (!gitRepositoryEntity) { - // Create a new gitRepository entity - gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]); - } - token.commitHash = event.params.value[0]; - token.gitRepository = event.params.value[1]; - token.save(); - gitRepositoryEntity.save(); - } + if (token) { + if (event.params.key == 'build') { + let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]); + if (!gitRepositoryEntity) { + // Create a new gitRepository entity + gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]); + } + token.commitHash = event.params.value[0]; + token.gitRepository = event.params.value[1]; + token.save(); + gitRepositoryEntity.save(); } + } } export function handleMetadataUpdateWithIntValue( - event: MetadataUpdateEvent2 + event: MetadataUpdateEvent2 ): void { - /** - * setTokenColor - */ - let entity = new MetadataUpdate( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); + /** + * setTokenColor + */ + let entity = new MetadataUpdate( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); - entity.key = event.params.key; - entity.tokenId = event.params._tokenId; - entity.uint24Value = event.params.value; - entity.blockNumber = event.block.number; - entity.blockTimestamp = event.block.timestamp; - entity.transactionHash = event.transaction.hash; + entity.key = event.params.key; + entity.tokenId = event.params._tokenId; + entity.uint24Value = event.params.value; + entity.blockNumber = event.block.number; + entity.blockTimestamp = event.block.timestamp; + entity.transactionHash = event.transaction.hash; - entity.save(); + entity.save(); - let token = Token.load( - Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) - ); + let token = Token.load( + Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) + ); - if (token) { - if (event.params.key == 'color') { - token.color = event.params.value; - } - token.save(); + if (token) { + if (event.params.key == 'color') { + token.color = event.params.value; } + token.save(); + } } export function handleMetadataUpdateWithBooleanValue( - event: MetadataUpdateEvent4 + event: MetadataUpdateEvent4 ): void { - /** - * accessPointAutoApproval - */ - let entity = new MetadataUpdate( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); + /** + * accessPointAutoApproval + */ + let entity = new MetadataUpdate( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); - entity.key = event.params.key; - entity.tokenId = event.params._tokenId; - entity.booleanValue = event.params.value; - entity.blockNumber = event.block.number; - entity.blockTimestamp = event.block.timestamp; - entity.transactionHash = event.transaction.hash; + entity.key = event.params.key; + entity.tokenId = event.params._tokenId; + entity.booleanValue = event.params.value; + entity.blockNumber = event.block.number; + entity.blockTimestamp = event.block.timestamp; + entity.transactionHash = event.transaction.hash; - entity.save(); + entity.save(); - let token = Token.load( - Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) - ); + let token = Token.load( + Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) + ); - if (token) { - if (event.params.key == 'accessPointAutoApproval') { - token.accessPointAutoApproval = event.params.value; - } - token.save(); + if (token) { + if (event.params.key == 'accessPointAutoApproval') { + token.accessPointAutoApproval = event.params.value; } -} \ No newline at end of file + token.save(); + } +} diff --git a/subgraph/src/mint.ts b/subgraph/src/mint.ts index e3e01d4..df4d20c 100644 --- a/subgraph/src/mint.ts +++ b/subgraph/src/mint.ts @@ -1,90 +1,85 @@ -import { - Bytes, - log, -} from '@graphprotocol/graph-ts'; +import { Bytes, log } from '@graphprotocol/graph-ts'; // Event Imports [based on the yaml config] -import { - NewMint as NewMintEvent, -} from '../generated/FleekNFA/FleekNFA'; +import { NewMint as NewMintEvent } from '../generated/FleekNFA/FleekNFA'; // Entity Imports [based on the schema] import { - Owner, - GitRepository as GitRepositoryEntity, - NewMint, - Token, + Owner, + GitRepository as GitRepositoryEntity, + NewMint, + Token, } from '../generated/schema'; export function handleNewMint(event: NewMintEvent): void { - let newMintEntity = new NewMint( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); + let newMintEntity = new NewMint( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); - let name = event.params.name; - let description = event.params.description; - let externalURL = event.params.externalURL; - let ENS = event.params.ENS; - let gitRepository = event.params.gitRepository; - let commitHash = event.params.commitHash; - let logo = event.params.logo; - let color = event.params.color; - let accessPointAutoApproval = event.params.accessPointAutoApproval; - let tokenId = event.params.tokenId; - let ownerAddress = event.params.owner; - let verifierAddress = event.params.verifier; + let name = event.params.name; + let description = event.params.description; + let externalURL = event.params.externalURL; + let ENS = event.params.ENS; + let gitRepository = event.params.gitRepository; + let commitHash = event.params.commitHash; + let logo = event.params.logo; + let color = event.params.color; + let accessPointAutoApproval = event.params.accessPointAutoApproval; + let tokenId = event.params.tokenId; + let ownerAddress = event.params.owner; + let verifierAddress = event.params.verifier; - newMintEntity.tokenId = tokenId; - newMintEntity.name = name; - newMintEntity.description = description; - newMintEntity.externalURL = externalURL; - newMintEntity.ENS = ENS; - newMintEntity.commitHash = commitHash; - newMintEntity.gitRepository = gitRepository; - newMintEntity.logo = logo; - newMintEntity.color = color; - newMintEntity.accessPointAutoApproval = accessPointAutoApproval; - newMintEntity.triggeredBy = event.params.minter; - newMintEntity.owner = ownerAddress; - newMintEntity.verifier = verifierAddress; - newMintEntity.blockNumber = event.block.number; - newMintEntity.blockTimestamp = event.block.timestamp; - newMintEntity.transactionHash = event.transaction.hash; - newMintEntity.save(); - log.error('{}', [tokenId.toString()]); + newMintEntity.tokenId = tokenId; + newMintEntity.name = name; + newMintEntity.description = description; + newMintEntity.externalURL = externalURL; + newMintEntity.ENS = ENS; + newMintEntity.commitHash = commitHash; + newMintEntity.gitRepository = gitRepository; + newMintEntity.logo = logo; + newMintEntity.color = color; + newMintEntity.accessPointAutoApproval = accessPointAutoApproval; + newMintEntity.triggeredBy = event.params.minter; + newMintEntity.owner = ownerAddress; + newMintEntity.verifier = verifierAddress; + newMintEntity.blockNumber = event.block.number; + newMintEntity.blockTimestamp = event.block.timestamp; + newMintEntity.transactionHash = event.transaction.hash; + newMintEntity.save(); + log.error('{}', [tokenId.toString()]); - // Create Token, Owner, and Controller entities + // Create Token, Owner, and Controller entities - let owner = Owner.load(ownerAddress); - let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId))); + let owner = Owner.load(ownerAddress); + let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId))); - if (!owner) { - // Create a new owner entity - owner = new Owner(ownerAddress); - // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. - owner.collection = false; - } + if (!owner) { + // Create a new owner entity + owner = new Owner(ownerAddress); + // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. + owner.collection = false; + } - // Populate Token with data from the event - token.tokenId = tokenId; - token.name = name; - token.description = description; - token.externalURL = externalURL; - token.ENS = ENS; - token.gitRepository = gitRepository; - token.commitHash = commitHash; - token.logo = logo; - token.color = color; - token.accessPointAutoApproval = accessPointAutoApproval; - token.owner = ownerAddress; - token.verifier = verifierAddress; - token.mintTransaction = event.transaction.hash.concatI32( - event.logIndex.toI32() - ); - token.mintedBy = event.params.minter; - token.controllers = [ownerAddress]; + // Populate Token with data from the event + token.tokenId = tokenId; + token.name = name; + token.description = description; + token.externalURL = externalURL; + token.ENS = ENS; + token.gitRepository = gitRepository; + token.commitHash = commitHash; + token.logo = logo; + token.color = color; + token.accessPointAutoApproval = accessPointAutoApproval; + token.owner = ownerAddress; + token.verifier = verifierAddress; + token.mintTransaction = event.transaction.hash.concatI32( + event.logIndex.toI32() + ); + token.mintedBy = event.params.minter; + token.controllers = [ownerAddress]; - // Save entities - owner.save(); - token.save(); -} \ No newline at end of file + // Save entities + owner.save(); + token.save(); +} diff --git a/subgraph/src/transfer.ts b/subgraph/src/transfer.ts index faa64a4..dcd1869 100644 --- a/subgraph/src/transfer.ts +++ b/subgraph/src/transfer.ts @@ -1,73 +1,60 @@ -import { - Bytes, - log, - store -} from '@graphprotocol/graph-ts'; +import { Bytes, log, store } from '@graphprotocol/graph-ts'; // Event Imports [based on the yaml config] -import { - Transfer as TransferEvent, -} from '../generated/FleekNFA/FleekNFA'; +import { Transfer as TransferEvent } from '../generated/FleekNFA/FleekNFA'; // Entity Imports [based on the schema] -import { - Owner, - Token, - Transfer, -} from '../generated/schema'; +import { Owner, Token, Transfer } from '../generated/schema'; export function handleTransfer(event: TransferEvent): void { - let transfer = new Transfer( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ); - - const TokenId = event.params.tokenId; - - transfer.from = event.params.from; - transfer.to = event.params.to; - transfer.tokenId = TokenId; - - transfer.blockNumber = event.block.number; - transfer.blockTimestamp = event.block.timestamp; - transfer.transactionHash = event.transaction.hash; - - transfer.save(); - - let token: Token | null; - - let owner_address = event.params.to; - let owner = Owner.load(owner_address); - - if (!owner) { - // Create a new owner entity - owner = new Owner(owner_address); - } - - if (parseInt(event.params.from.toHexString()) !== 0) { - if (parseInt(event.params.to.toHexString()) === 0) { - // Burn - // Remove the entity from storage - // Its controllers and owner will be affected. - store.remove('Token', TokenId.toString()); + let transfer = new Transfer( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ); + + const TokenId = event.params.tokenId; + + transfer.from = event.params.from; + transfer.to = event.params.to; + transfer.tokenId = TokenId; + + transfer.blockNumber = event.block.number; + transfer.blockTimestamp = event.block.timestamp; + transfer.transactionHash = event.transaction.hash; + + transfer.save(); + + let token: Token | null; + + let owner_address = event.params.to; + let owner = Owner.load(owner_address); + + if (!owner) { + // Create a new owner entity + owner = new Owner(owner_address); + } + + if (parseInt(event.params.from.toHexString()) !== 0) { + if (parseInt(event.params.to.toHexString()) === 0) { + // Burn + // Remove the entity from storage + // Its controllers and owner will be affected. + store.remove('Token', TokenId.toString()); + } else { + // Transfer + // Load the Token by using its TokenId + token = Token.load(Bytes.fromByteArray(Bytes.fromBigInt(TokenId))); + + if (token) { + // Entity exists + token.owner = owner_address; + + // Save both entities + owner.save(); + token.save(); } else { - // Transfer - // Load the Token by using its TokenId - token = Token.load( - Bytes.fromByteArray(Bytes.fromBigInt(TokenId)) - ); - - if (token) { - // Entity exists - token.owner = owner_address; - - // Save both entities - owner.save(); - token.save(); - } else { - // Entity does not exist - log.error('Unknown token was transferred.', []); - } + // Entity does not exist + log.error('Unknown token was transferred.', []); } } } - +} diff --git a/subgraph/tests/matchstick/.latest.json b/subgraph/tests/matchstick/.latest.json index 870c04b..521caf3 100644 --- a/subgraph/tests/matchstick/.latest.json +++ b/subgraph/tests/matchstick/.latest.json @@ -1,4 +1,4 @@ { "version": "0.5.4", "timestamp": 1679061942846 -} \ No newline at end of file +} diff --git a/ui/.graphclientrc.yml b/ui/.graphclientrc.yml index 1638e01..bb39726 100644 --- a/ui/.graphclientrc.yml +++ b/ui/.graphclientrc.yml @@ -4,6 +4,10 @@ sources: handler: graphql: endpoint: https://api.thegraph.com/subgraphs/name/emperororokusaki/flk-test-subgraph #replace for nfa subgraph + - name: ENS + handler: + graphql: + endpoint: https://api.thegraph.com/subgraphs/name/ensdomains/ens documents: - ./graphql/*.graphql diff --git a/ui/graphql/queries.graphql b/ui/graphql/queries.graphql index e4d1bb2..1f4aef2 100644 --- a/ui/graphql/queries.graphql +++ b/ui/graphql/queries.graphql @@ -18,3 +18,12 @@ query totalTokens { } } +# query to get the ens name of an address +query getENSNames($address: ID!) { + account(id: $address) { + domains { + name + } + } +} + diff --git a/ui/src/components/card/card.tsx b/ui/src/components/card/card.tsx index 87769f7..adb0817 100644 --- a/ui/src/components/card/card.tsx +++ b/ui/src/components/card/card.tsx @@ -1,4 +1,3 @@ -import { Octokit } from 'octokit'; import React, { forwardRef } from 'react'; import { Flex } from '../layout'; import { CardStyles } from './card.styles'; @@ -15,9 +14,9 @@ export abstract class Card { ); static readonly Heading = forwardRef( - ({ title, leftIcon, rightIcon, ...props }, ref) => { + ({ title, leftIcon, rightIcon, css, ...props }, ref) => { return ( - + {leftIcon} @@ -58,6 +57,7 @@ export namespace Card { export type HeadingProps = { title: string; + css?: React.CSSProperties; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; } & React.ComponentProps; diff --git a/ui/src/views/mint/nfa-step/form-step/fields/logo-field/color-picker.tsx b/ui/src/components/core/color-picker/color-picker.tsx similarity index 76% rename from ui/src/views/mint/nfa-step/form-step/fields/logo-field/color-picker.tsx rename to ui/src/components/core/color-picker/color-picker.tsx index bf50915..ba37db1 100644 --- a/ui/src/views/mint/nfa-step/form-step/fields/logo-field/color-picker.tsx +++ b/ui/src/components/core/color-picker/color-picker.tsx @@ -2,10 +2,19 @@ import { Button, Card, Flex, Icon } from '@/components'; import { useRef } from 'react'; // @ts-ignore import ColorThief from 'colorthief'; -import { Mint } from '../../../../mint.context'; -export const ColorPicker = () => { - const { appLogo, logoColor, setLogoColor } = Mint.useContext(); +export type ColorPickerProps = { + logoColor: string; + setLogoColor: (color: string) => void; + logo: string; +} & React.HTMLAttributes; + +export const ColorPicker: React.FC = ({ + logoColor, + logo, + setLogoColor, + onBlur, +}) => { const inputColorRef = useRef(null); const imageRef = useRef(null); @@ -17,6 +26,10 @@ export const ColorPicker = () => { setLogoColor(hexColor); }; + const handleColorChange = (e: React.ChangeEvent) => { + setLogoColor(e.target.value); + }; + const handleColorPickerClick = () => { inputColorRef.current?.click(); }; @@ -43,6 +56,7 @@ export const ColorPicker = () => { borderRadius: '$md', color: '$slate12', zIndex: '$dropdown', + minWidth: '$28', }} onClick={handleColorPickerClick} > @@ -53,14 +67,14 @@ export const ColorPicker = () => { className="absolute right-16" type="color" value={logoColor} - onChange={(e) => setLogoColor(e.target.value)} + onChange={handleColorChange} /> ) => void; - /** - * Function to handle the input click. When the user clicks on the input, the list of options will be displayed - */ - handleInputClick: () => void; -}; + error?: boolean; +} & ComboboxLibInputProps<'input', ComboboxItem>; const ComboboxInput = ({ open, leftIcon, - handleInputChange, - handleInputClick, + error, + ...props }: ComboboxInputProps) => (
selectedValue.label} - onChange={handleInputChange} - onClick={handleInputClick} + {...props} /> @@ -134,131 +141,151 @@ export type ComboboxProps = { /** * Callback when the selected value changes. */ - onChange(option: ComboboxItem): void; + onChange: (option: ComboboxItem) => void; + /** + * Function to handle the input blur + */ + onBlur?: () => void; + /** + * Value to indicate it's invalid + */ + error?: boolean; }; -export const Combobox: React.FC = ({ - items, - selectedValue = { value: '', label: '' }, - withAutocomplete = false, - leftIcon = 'search', - onChange, -}) => { - const [filteredItems, setFilteredItems] = useState([]); - const [autocompleteItems, setAutocompleteItems] = useState( - [] - ); +export const Combobox = forwardRef( + ( + { + items, + selectedValue = { value: '', label: '' }, + withAutocomplete = false, + leftIcon = 'search', + onChange, + onBlur, + error = false, + }, + ref + ) => { + const [filteredItems, setFilteredItems] = useState([]); + const [autocompleteItems, setAutocompleteItems] = useState( + [] + ); - useEffect(() => { - // If the selected value doesn't exist in the list of items, we add it - if ( - items.filter((item) => item === selectedValue).length === 0 && - selectedValue.value !== undefined && - autocompleteItems.length === 0 && - withAutocomplete - ) { - setAutocompleteItems([selectedValue]); - } - }, [selectedValue]); + useEffect(() => { + // If the selected value doesn't exist in the list of items, we add it + if ( + items.filter((item) => item === selectedValue).length === 0 && + selectedValue.value !== undefined && + autocompleteItems.length === 0 && + withAutocomplete + ) { + setAutocompleteItems([selectedValue]); + } + }, [selectedValue]); - useEffect(() => { - setFilteredItems(items); - }, [items]); - - const buttonRef = useRef(null); - - const handleSearch = useDebounce((searchValue: string) => { - if (searchValue === '') { + useEffect(() => { setFilteredItems(items); + }, [items]); - if (withAutocomplete) { + const buttonRef = useRef(null); + + const handleSearch = useDebounce((searchValue: string) => { + if (searchValue === '') { + setFilteredItems(items); + + if (withAutocomplete) { + setAutocompleteItems([]); + handleComboboxChange({} as ComboboxItem); + } + } else { + const filteredValues = items.filter((item) => + cleanString(item.label).startsWith(cleanString(searchValue)) + ); + + if (withAutocomplete && filteredValues.length === 0) { + // If the search value doesn't exist in the list of items, we add it + setAutocompleteItems([{ value: searchValue, label: searchValue }]); + } + setFilteredItems(filteredValues); + } + }, 200); + + const handleInputChange = (event: React.ChangeEvent) => { + event.stopPropagation(); + handleSearch(event.target.value); + }; + + const handleInputClick = () => { + buttonRef.current?.click(); + }; + + const handleComboboxChange = (optionSelected: ComboboxItem) => { + onChange(optionSelected); + }; + + const handleLeaveTransition = () => { + setFilteredItems(items); + if (selectedValue.value === undefined && withAutocomplete) { setAutocompleteItems([]); handleComboboxChange({} as ComboboxItem); } - } else { - const filteredValues = items.filter((item) => - cleanString(item.label).startsWith(cleanString(searchValue)) - ); + }; - if (withAutocomplete && filteredValues.length === 0) { - // If the search value doesn't exist in the list of items, we add it - setAutocompleteItems([{ value: searchValue, label: searchValue }]); - } - setFilteredItems(filteredValues); - } - }, 200); + return ( + + {({ open }) => ( +
+ + - const handleInputChange = (event: React.ChangeEvent) => { - event.stopPropagation(); - handleSearch(event.target.value); - }; - - const handleInputClick = () => { - buttonRef.current?.click(); - }; - - const handleComboboxChange = (option: ComboboxItem) => { - onChange(option); - }; - - const handleLeaveTransition = () => { - setFilteredItems(items); - if (selectedValue.value === undefined && withAutocomplete) { - setAutocompleteItems([]); - handleComboboxChange({} as ComboboxItem); - } - }; - - return ( - - {({ open }) => ( -
- - - - - - {[...autocompleteItems, ...filteredItems].length === 0 || - filteredItems === undefined ? ( - - ) : ( - <> - {autocompleteItems.length > 0 && Create new} - {autocompleteItems.map((autocompleteOption: ComboboxItem) => ( - - ))} - {autocompleteItems.length > 0 && filteredItems.length > 0 && ( - - )} - {filteredItems.map((option: ComboboxItem) => ( - - ))} - - )} - - -
- )} -
- ); -}; + + + {[...autocompleteItems, ...filteredItems].length === 0 || + filteredItems === undefined ? ( + + ) : ( + <> + {autocompleteItems.length > 0 && Create new} + {autocompleteItems.map( + (autocompleteOption: ComboboxItem) => ( + + ) + )} + {autocompleteItems.length > 0 && + filteredItems.length > 0 && ( + + )} + {filteredItems.map((option: ComboboxItem) => ( + + ))} + + )} + + +
+ )} +
+ ); + } +); diff --git a/ui/src/components/core/index.ts b/ui/src/components/core/index.ts index 19da8f6..947be14 100644 --- a/ui/src/components/core/index.ts +++ b/ui/src/components/core/index.ts @@ -6,3 +6,4 @@ export * from './avatar'; export * from './separator.styles'; export * from './text'; export * from './switch'; +export * from './color-picker'; diff --git a/ui/src/components/core/input/input-file.styles.ts b/ui/src/components/core/input/input-file.styles.ts new file mode 100644 index 0000000..ecf65ef --- /dev/null +++ b/ui/src/components/core/input/input-file.styles.ts @@ -0,0 +1,32 @@ +import { Flex } from '@/components/layout'; +import { dripStitches } from '@/theme'; + +const { styled } = dripStitches; + +export abstract class InputFileStyles { + static readonly Container = styled(Flex, { + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + }); + + static readonly Border = styled('div', { + borderStyle: 'solid', + borderColor: '$gray7', + width: '$22', + height: '$22', + transition: 'border-color 0.2s ease-in-out', + borderWidth: '$default', + borderRadius: '$lg', + zIndex: '$docked', + + '&:hover': { + borderColor: '$gray8', + }, + + '&[aria-invalid=true], &[data-invalid]': { + borderColor: '$red9', + }, + //TODO add error state + }); +} diff --git a/ui/src/components/core/input/input-file.tsx b/ui/src/components/core/input/input-file.tsx index 7baf2ba..d321bee 100644 --- a/ui/src/components/core/input/input-file.tsx +++ b/ui/src/components/core/input/input-file.tsx @@ -1,29 +1,11 @@ -import { Flex } from '../../layout'; -import { dripStitches } from '../../../theme'; import { forwardRef, useRef } from 'react'; import { Icon } from '../icon'; - -const { styled } = dripStitches; - -const BorderInput = styled('div', { - borderStyle: 'solid', - borderColor: '$gray7', - width: '$22', - height: '$22', - transition: 'border-color 0.2s ease-in-out', - borderWidth: '$default', - borderRadius: '$lg', - zIndex: '$docked', - - '&:hover': { - borderColor: '$gray8', - }, -}); +import { InputFileStyles as S } from './input-file.styles'; type InputFileProps = { value: string; onChange: (e: React.ChangeEvent) => void; -} & React.ComponentProps; +} & React.ComponentProps; export const StyledInputFile = forwardRef( ({ value: file, onChange, css, ...props }, ref) => { @@ -40,24 +22,13 @@ export const StyledInputFile = forwardRef( return ( <> - + {file !== '' ? ( logo ) : ( )} - - + ( ref={inputFileRef} onChange={handleFileChange} /> - + ); } diff --git a/ui/src/components/form/form-field.context.ts b/ui/src/components/form/form-field.context.ts new file mode 100644 index 0000000..aa8e671 --- /dev/null +++ b/ui/src/components/form/form-field.context.ts @@ -0,0 +1,15 @@ +import { createContext, StringValidator } from '@/utils'; + +export type FormFieldContext = { + id: string; + validators: StringValidator[]; + value: ReactState; + validationEnabled: ReactState; +}; + +export const [FormFieldProvider, useFormFieldContext] = + createContext({ + name: 'FormFieldContext', + hookName: 'useFormFieldContext', + providerName: 'FormFieldProvider', + }); diff --git a/ui/src/components/form/form.context.tsx b/ui/src/components/form/form.context.tsx new file mode 100644 index 0000000..523b5d7 --- /dev/null +++ b/ui/src/components/form/form.context.tsx @@ -0,0 +1,79 @@ +import { createContext, StringValidator } from '@/utils'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +export type FormValidations = { [key: string]: StringValidator[] }; + +export type FormContext = { + onValidationChange: (isValid: boolean) => void; + validations: ReactState; +}; + +const [FormProviderCore, useFormContext] = createContext({ + name: 'FormContext', + hookName: 'useFormContext', + providerName: 'FormProvider', +}); + +export { useFormContext }; + +export const FormProvider = ({ + children, + onValidationChange, +}: React.PropsWithChildren< + Pick +>): JSX.Element => { + const validations = useState({}); + + useEffect(() => { + onValidationChange(Object.values(validations[0]).length === 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [validations]); + + return ( + + {children} + + ); +}; + +export const useFormFieldValidator = ( + id: string, + validators: StringValidator[] +): ((value: string) => boolean) => { + const { + validations: [, setValidations], + } = useFormContext(); + + return useCallback( + (value: string) => { + const fieldValidations = validators.reduce( + (acc, validator) => + validator.validate(value) ? acc : [...acc, validator], + [] + ); + + if (fieldValidations.length > 0) { + setValidations((prev) => ({ ...prev, [id]: fieldValidations })); + return false; + } + + setValidations((prev) => { + const { [id]: toBeRemoved, ...rest } = prev; + return rest; + }); + return true; + }, + [id, validators, setValidations] + ); +}; + +export const useFormFieldValidatorValue = ( + id: string, + validators: StringValidator[], + value: string +): boolean => { + const validatorHandler = useFormFieldValidator(id, validators); + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => validatorHandler(value), [value]); +}; diff --git a/ui/src/components/form/form.styles.ts b/ui/src/components/form/form.styles.ts index 74e491e..5440e54 100644 --- a/ui/src/components/form/form.styles.ts +++ b/ui/src/components/form/form.styles.ts @@ -42,6 +42,15 @@ export abstract class FormStyles { mt: '$1h', }); + static readonly Overline = styled('div', { + display: 'flex', + justifyContent: 'space-between', + }); + + static readonly OverlineErrors = styled(Flex, { + flexDirection: 'column', + }); + static readonly ErrorMessage = styled('span', { color: '$red11', fontSize: '0.625rem', diff --git a/ui/src/components/form/form.tsx b/ui/src/components/form/form.tsx index 29376a8..1e2242a 100644 --- a/ui/src/components/form/form.tsx +++ b/ui/src/components/form/form.tsx @@ -1,80 +1,313 @@ -import React, { forwardRef } from 'react'; +import { hasValidator } from '@/utils'; +import { fileToBase64 } from '@/views/mint/nfa-step/form-step/form.utils'; +import React, { forwardRef, useMemo, useState } from 'react'; +import { ColorPicker, Combobox, ComboboxItem } from '../core'; import { Input, LogoFileInput, Textarea } from '../core/input'; +import { + FormFieldContext, + FormFieldProvider, + useFormFieldContext, +} from './form-field.context'; +import { + FormProvider, + useFormContext, + useFormFieldValidatorValue, +} from './form.context'; import { FormStyles } from './form.styles'; export abstract class Form { + static readonly Root = FormProvider; + static readonly Field = forwardRef( - ({ children, ...props }, ref) => { + ({ children, context, ...props }, ref) => { + const { + value: [value], + } = context; + const validationEnabled = useState(Boolean(value)); + return ( - - {children} - + + + {children} + + ); } ); static readonly Label = forwardRef( - ({ children, isRequired, ...props }, ref) => ( - - {children}{' '} - {isRequired && *} - - ) - ); - - static readonly MaxLength = forwardRef( ({ children, ...props }, ref) => { + const { validators } = useFormFieldContext(); + + const isRequired = useMemo( + () => hasValidator(validators, 'required'), + [validators] + ); + return ( - + {children} - + {isRequired && *} + ); } ); - static readonly Error = forwardRef( - ({ children, ...props }, ref) => ( - - {children} - - ) - ); + static readonly Overline = forwardRef((props, ref) => { + const { + validations: [validations], + } = useFormContext(); + const { + id, + value: [value], + validationEnabled: [validationEnabled], + validators, + } = useFormFieldContext(); + + const errors = useMemo(() => { + if (!validationEnabled) return []; + if (!validations[id]) return []; + return validations[id].map((validator) => validator.message); + }, [validations, id, validationEnabled]); + + const counter = useMemo( + () => hasValidator(validators, 'maxLength')?.args || 0, + [validators] + ); + + return ( + + + {errors.map((error) => ( + + {error} + + ))} + + + {Boolean(counter) && ( + + {`${value.length}/${counter}`} + + )} + + ); + }); static readonly Input = forwardRef( (props, ref) => { - return ; + const { + id, + validators, + value: [value, setValue], + validationEnabled: [validationEnabled, setValidationEnabled], + } = useFormFieldContext(); + const isValid = useFormFieldValidatorValue(id, validators, value); + + const handleInputChange = (e: React.ChangeEvent) => { + if (props.onChange) props.onChange(e); + setValue(e.target.value); + }; + + const handleInputBlur = ( + e: React.FocusEvent + ) => { + if (props.onBlur) props.onBlur(e); + setValidationEnabled(true); + }; + + return ( + + ); } ); + static readonly Combobox = forwardRef( + (props, ref) => { + const { + id, + validators, + value: [value, setValue], + validationEnabled: [validationEnabled, setValidationEnabled], + } = useFormFieldContext(); + + const comboboxValue = useMemo(() => { + // if it's with autocomplete maybe won't be on the items list + const item = props.items.find((item) => item.label === value); + if (props.withAutocomplete && !item && value !== '') { + //return the selected value if the item doesn't exist + return { label: value, value: value }; + } + return item; + }, [value]); + + const isValid = useFormFieldValidatorValue(id, validators, value); + + const handleComboboxChange = (option: ComboboxItem) => { + if (props.onChange) props.onChange(option); + setValue(option.label); + }; + + const handleComboboxBlur = () => { + setValidationEnabled(true); + }; + + return ( + + ); + } + ); + + static readonly ColorPicker = ({ + logo, + setLogoColor, + }: Form.ColorPickerProps) => { + const { + value: [value, setValue], + validationEnabled: [, setValidationEnabled], + } = useFormFieldContext(); + + const handleColorChange = (color: string) => { + if (setLogoColor) setLogoColor(color); + setValue(color); + }; + + const handleInputBlur = () => { + setValidationEnabled(true); + }; + + return ( + + ); + }; + static readonly Textarea = forwardRef< HTMLTextAreaElement, Form.TextareaProps >((props, ref) => { - return