diff --git a/packages/api/src/resources.ts b/packages/api/src/resources.ts index 3aa936e0..2ded912b 100644 --- a/packages/api/src/resources.ts +++ b/packages/api/src/resources.ts @@ -328,13 +328,11 @@ export interface FlowEmulatorConfig { snapshot: boolean; } -export interface InteractionTemplate extends TimestampedResource { +export interface WorkspaceTemplate extends TimestampedResource { id: string; name: string; code: string; - source: { - filePath?: string; - }; + filePath: string; } export interface FlowCliInfo { diff --git a/packages/nodejs/src/flow-interactions.service.ts b/packages/nodejs/src/flow-interactions.service.ts index 44e4101d..8d52f443 100644 --- a/packages/nodejs/src/flow-interactions.service.ts +++ b/packages/nodejs/src/flow-interactions.service.ts @@ -2,22 +2,9 @@ import * as fs from "fs/promises"; import * as path from "path"; import { GoBindingsService } from "./go-bindings.service"; import { isDefined } from "@onflowser/core"; -import { InteractionKind, ParsedInteractionOrError } from "@onflowser/api"; +import { InteractionKind, ParsedInteractionOrError, WorkspaceTemplate } from "@onflowser/api"; import { IFlowInteractions } from "@onflowser/core"; -export interface InteractionTemplate { - id: string; - name: string; - code: string; - source: InteractionTemplate_Source | undefined; - createdDate: string; - updatedDate: string; -} - -interface InteractionTemplate_Source { - filePath: string; -} - type GetInteractionTemplatesOptions = { workspacePath: string; }; @@ -31,7 +18,7 @@ export class FlowInteractionsService implements IFlowInteractions { public async getTemplates( options: GetInteractionTemplatesOptions, - ): Promise { + ): Promise { const potentialCadenceFilePaths = await this.findAllCadenceFiles( options.workspacePath, ); @@ -45,7 +32,7 @@ export class FlowInteractionsService implements IFlowInteractions { private async buildMaybeTemplate( filePath: string, - ): Promise { + ): Promise { const [fileContent, fileStats] = await Promise.all([ fs.readFile(filePath), fs.stat(filePath), @@ -67,11 +54,9 @@ export class FlowInteractionsService implements IFlowInteractions { id: filePath, name: path.basename(filePath), code, - updatedDate: fileStats.mtime.toISOString(), - createdDate: fileStats.ctime.toISOString(), - source: { - filePath, - }, + updatedAt: fileStats.mtime, + createdAt: fileStats.ctime, + filePath }; } else { return undefined; diff --git a/packages/ui/src/api.ts b/packages/ui/src/api.ts index 020348ca..1503a0bb 100644 --- a/packages/ui/src/api.ts +++ b/packages/ui/src/api.ts @@ -10,7 +10,7 @@ import { FlowserWorkspace, FlowStateSnapshot, FlowTransaction, - InteractionTemplate, + WorkspaceTemplate, ManagedProcessOutput, ParsedInteractionOrError, ManagedKeyPair, @@ -249,12 +249,12 @@ export function useGetParsedInteraction( return state; } -export function useGetInteractionTemplates(): SWRResponse< - InteractionTemplate[] +export function useGetWorkspaceInteractionTemplates(): SWRResponse< + WorkspaceTemplate[] > { const { interactionsService } = useServiceRegistry(); - return useSWR(`interaction-templates`, () => + return useSWR(`workspace-interaction-templates`, () => interactionsService.getTemplates(), ); } diff --git a/packages/ui/src/common/icons/FlowserIcon.tsx b/packages/ui/src/common/icons/FlowserIcon.tsx index 98810022..e88a6ce8 100644 --- a/packages/ui/src/common/icons/FlowserIcon.tsx +++ b/packages/ui/src/common/icons/FlowserIcon.tsx @@ -32,6 +32,7 @@ import Shrink from "./assets/shrink.svg"; import Logs from "./assets/logs.svg"; import { TbBrandVscode } from "react-icons/tb"; import { SiWebstorm, SiIntellijidea } from "react-icons/si"; +import { RiVerifiedBadgeFill } from "react-icons/ri"; export const FlowserIcon = { Logs: Logs, @@ -69,4 +70,5 @@ export const FlowserIcon = { WebStorm: SiWebstorm, VsCode: TbBrandVscode, IntellijIdea: SiIntellijidea, + VerifiedCheck: RiVerifiedBadgeFill }; diff --git a/packages/ui/src/common/links/ExternalLink/ExternalLink.tsx b/packages/ui/src/common/links/ExternalLink/ExternalLink.tsx index 4927ede1..94e8f136 100644 --- a/packages/ui/src/common/links/ExternalLink/ExternalLink.tsx +++ b/packages/ui/src/common/links/ExternalLink/ExternalLink.tsx @@ -1,12 +1,14 @@ import React, { ReactNode, ReactElement, CSSProperties } from "react"; import { FlowserIcon } from "../../icons/FlowserIcon"; import classes from "./ExternalLink.module.scss"; +import classNames from "classnames"; export type ExternalLinkProps = { children?: ReactNode; href: string; inline?: boolean; style?: CSSProperties; + className?: string; }; export function ExternalLink({ @@ -14,13 +16,14 @@ export function ExternalLink({ children, inline, style, + className }: ExternalLinkProps): ReactElement { return ( {!inline && } @@ -30,5 +33,8 @@ export function ExternalLink({ } function prettifyUrl(url: string) { - return url.replace(/https?:\/\//, "").replace(/www\./, "").replace(/\/$/, "") + return url + .replace(/https?:\/\//, "") + .replace(/www\./, "") + .replace(/\/$/, ""); } diff --git a/packages/ui/src/common/misc/LineSeparator/LineSeparator.module.scss b/packages/ui/src/common/misc/LineSeparator/LineSeparator.module.scss index 60e78e2d..5dbed149 100644 --- a/packages/ui/src/common/misc/LineSeparator/LineSeparator.module.scss +++ b/packages/ui/src/common/misc/LineSeparator/LineSeparator.module.scss @@ -9,11 +9,11 @@ .horizontal { width: 100%; height: 1px; - margin: $spacing-l 0; + margin: $spacing-base 0; } .vertical { height: 100%; width: 1px; - margin: 0 $spacing-l; + margin: 0 $spacing-base; } diff --git a/packages/ui/src/contexts/service-registry.context.tsx b/packages/ui/src/contexts/service-registry.context.tsx index fccaf4c4..70ceb43a 100644 --- a/packages/ui/src/contexts/service-registry.context.tsx +++ b/packages/ui/src/contexts/service-registry.context.tsx @@ -11,7 +11,7 @@ import { FlowserWorkspace, FlowStateSnapshot, FlowTransaction, - InteractionTemplate, + WorkspaceTemplate, IResourceIndexReader, ManagedProcessOutput, ParsedInteractionOrError, @@ -38,7 +38,7 @@ export interface IWorkspaceService { export interface IInteractionService { parse(sourceCode: string): Promise; - getTemplates(): Promise; + getTemplates(): Promise; } export type SendTransactionRequest = { diff --git a/packages/ui/src/hooks/flix.ts b/packages/ui/src/hooks/flix.ts new file mode 100644 index 00000000..2323dde9 --- /dev/null +++ b/packages/ui/src/hooks/flix.ts @@ -0,0 +1,79 @@ +import useSWR, { SWRResponse } from "swr"; + +// https://github.com/onflow/flips/blob/main/application/20220503-interaction-templates.md#interaction-interfaces +export type FlixTemplate = { + id: string; + f_type: "InteractionTemplate"; + f_version: string; + data: { + messages: FlixMessages; + dependencies: Record; + cadence: string; + arguments: Record; + }; +}; + +export type FlixArgument = { + index: number; + type: string; + messages: FlixMessages; +} + +type FlixDependency = Record< + string, + { + mainnet: FlixDependencyOnNetwork; + testnet: FlixDependencyOnNetwork; + } +>; + +type FlixDependencyOnNetwork = { + address: string; + fq_address: string; + pin: string; + pin_block_height: number; +}; + +type FlixMessages = { + title?: FlixI18nMessage; + description?: FlixI18nMessage; +}; + +type FlixI18nMessage = { + i18n: { + "en-US"?: string; + }; +}; + +export const FLOW_FLIX_URL = "https://flix.flow.com"; +export const FLOWSER_FLIX_URL = "https://flowser-flix-368a32c94da2.herokuapp.com" + +export function useListFlixTemplates(): SWRResponse { + return useSWR(`flix/templates`, () => + fetch(`${FLOWSER_FLIX_URL}/v1/templates`).then((res) => res.json()), + ); +} + +export function useFlixSearch(options: { + sourceCode: string; + // Supports "any" network as of: + // https://github.com/onflowser/flow-interaction-template-service/pull/4 + network: "any" | "testnet" | "mainnet"; +}): SWRResponse { + return useSWR(`flix/templates/${options.sourceCode}`, () => + fetch(`${FLOWSER_FLIX_URL}/v1/templates/search`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + cadence_base64: btoa(options.sourceCode), + network: options.network + }) + }).then((res) => res.json()), + { + refreshInterval: 0, + shouldRetryOnError: false + } + ); +} diff --git a/packages/ui/src/interactions/components/ExecutionSettings/ExecutionSettings.tsx b/packages/ui/src/interactions/components/ExecutionSettings/ExecutionSettings.tsx index 0ce614db..7dba3290 100644 --- a/packages/ui/src/interactions/components/ExecutionSettings/ExecutionSettings.tsx +++ b/packages/ui/src/interactions/components/ExecutionSettings/ExecutionSettings.tsx @@ -44,7 +44,7 @@ function ExecuteButton() { } function TopContent() { - const { setFclValue, fclValuesByIdentifier, definition, parsedInteraction } = + const { flixTemplate, setFclValue, fclValuesByIdentifier, definition, parsedInteraction } = useInteractionDefinitionManager(); if (definition.code === "") { @@ -70,6 +70,7 @@ function TopContent() { parameters={parsedInteraction?.parameters ?? []} setFclValue={setFclValue} fclValuesByIdentifier={fclValuesByIdentifier} + flixTemplate={flixTemplate} /> {parsedInteraction?.kind === InteractionKind.INTERACTION_TRANSACTION && ( diff --git a/packages/ui/src/interactions/components/InteractionLabel/InteractionLabel.module.scss b/packages/ui/src/interactions/components/InteractionLabel/InteractionLabel.module.scss index 87aa9f6d..61b7c8cc 100644 --- a/packages/ui/src/interactions/components/InteractionLabel/InteractionLabel.module.scss +++ b/packages/ui/src/interactions/components/InteractionLabel/InteractionLabel.module.scss @@ -11,7 +11,6 @@ justify-content: center; } .label { - white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } diff --git a/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.module.scss b/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.module.scss index 8f8ce5cc..b9925003 100644 --- a/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.module.scss +++ b/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.module.scss @@ -3,6 +3,7 @@ @import "../../../styles/typography"; @import "../../../styles/animations"; @import "../../../styles/scrollbars"; +@import "../../../styles/rules"; .root { height: 100%; @@ -11,7 +12,7 @@ justify-content: space-between; position: relative; - .header { + & > .header { background: $gray-100; position: sticky; top: 0; @@ -29,6 +30,7 @@ .item { cursor: pointer; display: flex; + align-items: center; justify-content: space-between; font-size: $font-size-normal; color: $gray-20; @@ -36,6 +38,7 @@ .trash { @include scaleOnHover(); + min-width: 20px; * { fill: $gray-50; } @@ -59,6 +62,14 @@ padding-top: $spacing-base; background: $gray-100; bottom: 0; + } + + .workspaceInfo { + border-radius: $border-radius-input; + background: $gray-80; + padding: $spacing-base; + display: flex; + column-gap: $spacing-base; .actionButtons { display: flex; @@ -66,3 +77,36 @@ } } } + +.flixInfo { + border-radius: $border-radius-input; + background: $gray-80; + padding: $spacing-base; + .header { + font-weight: bold; + display: flex; + align-items: center; + column-gap: 3px; + .title { + color: $strong-green; + display: inline-flex !important; + } + @mixin icon { + height: 15px; + width: 15px; + } + .verifiedIcon { + color: $strong-green; + @include icon; + } + .unverifiedIcon { + color: $color-red; + @include icon; + } + } + .body { + display: flex; + flex-direction: column; + row-gap: $spacing-base; + } +} diff --git a/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.tsx b/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.tsx index 8316b96e..8a372681 100644 --- a/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.tsx +++ b/packages/ui/src/interactions/components/InteractionTemplates/InteractionTemplates.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useMemo, useState } from "react"; +import React, { Fragment, ReactElement, useMemo, useState } from "react"; import { useInteractionRegistry } from "../../contexts/interaction-registry.context"; import classes from "./InteractionTemplates.module.scss"; import { FlowserIcon } from "../../../common/icons/FlowserIcon"; @@ -10,12 +10,17 @@ import classNames from "classnames"; import { InteractionLabel } from "../InteractionLabel/InteractionLabel"; import { useTemplatesRegistry } from "../../contexts/templates.context"; import { IdeLink } from "../../../common/links/IdeLink"; +import { WorkspaceTemplate } from "@onflowser/api"; +import { FLOW_FLIX_URL, useFlixSearch } from "../../../hooks/flix"; +import { ExternalLink } from "../../../common/links/ExternalLink/ExternalLink"; +import { LineSeparator } from "../../../common/misc/LineSeparator/LineSeparator"; +import { Shimmer } from "../../../common/loaders/Shimmer/Shimmer"; export function InteractionTemplates(): ReactElement { return (
- +
); } @@ -29,14 +34,14 @@ function StoredTemplates() { if (searchTerm === "") { return templates; } - return templates.filter((template) => template.name.includes(searchTerm)); + return templates.filter((template) => template.name.toLowerCase().includes(searchTerm.toLowerCase())); }, [searchTerm, templates]); const filteredAndSortedTemplates = useMemo( () => filteredTemplates.sort( - (a, b) => b.updatedDate.getTime() - a.updatedDate.getTime(), + (a, b) => b.updatedDate.getTime() - a.updatedDate.getTime() ), - [filteredTemplates], + [filteredTemplates] ); return ( @@ -57,11 +62,11 @@ function StoredTemplates() { setFocused(createdInteraction.id); }} className={classNames(classes.item, { - [classes.focusedItem]: focusedDefinition?.id === template.id, + [classes.focusedItem]: focusedDefinition?.id === template.id })} > - {!template.filePath && ( + {template.source === "session" && ( { @@ -76,7 +81,7 @@ function StoredTemplates() { ), confirmButtonLabel: "REMOVE", cancelButtonLabel: "CANCEL", - onConfirm: () => removeTemplate(template), + onConfirm: () => removeTemplate(template) }); }} /> @@ -88,33 +93,48 @@ function StoredTemplates() { ); } -function FocusedTemplateSettings() { - const { focusedDefinition, update } = useInteractionRegistry(); - const { templates, saveTemplate } = useTemplatesRegistry(); - - if (!focusedDefinition) { - return null; - } +function FocusedInteraction() { + const { focusedDefinition } = useInteractionRegistry(); + const { templates } = useTemplatesRegistry(); const correspondingTemplate = templates.find( - (template) => template.id === focusedDefinition.id, + (template) => template.id === focusedDefinition?.id ); - if (correspondingTemplate && correspondingTemplate.filePath) { - return ( -
- Open in: -
- - - -
-
- ); + function renderContent() { + if (!correspondingTemplate) { + return ; + } + + switch (correspondingTemplate.source) { + case "workspace": + return ; + case "flix": + // Already shown at the top. + return null; + case "session": + return ; + } } return (
+ {focusedDefinition && } + {renderContent()} +
+ ); +} + +function SessionTemplateSettings() { + const { focusedDefinition, update } = useInteractionRegistry(); + const { saveTemplate } = useTemplatesRegistry(); + + if (!focusedDefinition) { + return null; + } + + return ( + <> saveTemplate(focusedDefinition)}> Save + + ); +} + +function WorkspaceTemplateInfo(props: { workspaceTemplate: WorkspaceTemplate }) { + const { workspaceTemplate } = props; + + return ( +
+ Open in: +
+ + + +
+
+ ); +} + +function FlixInfo(props: { sourceCode: string }) { + const { data, isLoading } = useFlixSearch({ + sourceCode: props.sourceCode, + network: "any" + }); + + if (isLoading) { + return ; + } + + const isVerified = data !== undefined; + + return ( +
+
+ + FLIX: + + {isVerified ? ( + + verified + + + ) : ( + + unverified + + + )} +
+ +
+ {isVerified ? ( + +

{data.data.messages.description?.i18n["en-US"]}

+ +
+ ) : ( + +

+ This interaction is not yet verified by FLIX. +

+ + Submit for verification + +
+ )} +
); } diff --git a/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.module.scss b/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.module.scss index f1fc408c..f718e527 100644 --- a/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.module.scss +++ b/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.module.scss @@ -9,7 +9,6 @@ .paramRoot { .label { - margin-bottom: $spacing-base; .identifier { margin-right: $spacing-s; } @@ -17,4 +16,10 @@ color: $gray-10; } } + .description { + color: $gray-20; + } + .valueBuilder { + margin-top: $spacing-base; + } } diff --git a/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.tsx b/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.tsx index 96ce3293..4d401ceb 100644 --- a/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.tsx +++ b/packages/ui/src/interactions/components/ParamBuilder/ParamBuilder.tsx @@ -5,15 +5,18 @@ import { InteractionParameterBuilder } from "../../contexts/definition.context"; import classes from "./ParamBuilder.module.scss"; import { CadenceValueBuilder } from "../ValueBuilder/interface"; import { CadenceParameter } from "@onflowser/api"; +import { FlixArgument, FlixTemplate } from "../../../hooks/flix"; +import { SizedBox } from "../../../common/misc/SizedBox/SizedBox"; export type ParameterListBuilderProps = InteractionParameterBuilder & { parameters: CadenceParameter[]; + flixTemplate: FlixTemplate | undefined; }; export function ParamListBuilder( props: ParameterListBuilderProps, ): ReactElement { - const { parameters, fclValuesByIdentifier, setFclValue } = props; + const { flixTemplate, parameters, fclValuesByIdentifier, setFclValue } = props; return (
{parameters.map((parameter) => ( @@ -22,6 +25,7 @@ export function ParamListBuilder( parameter={parameter} value={fclValuesByIdentifier.get(parameter.identifier)} setValue={(value) => setFclValue(parameter.identifier, value)} + flixArgument={flixTemplate?.data?.arguments?.[parameter.identifier]} /> ))}
@@ -30,14 +34,17 @@ export function ParamListBuilder( export type ParameterBuilderProps = Omit & { parameter: CadenceParameter; + flixArgument: FlixArgument | undefined; }; export function ParamBuilder(props: ParameterBuilderProps): ReactElement { - const { parameter, ...valueBuilderProps } = props; + const { parameter, flixArgument, ...valueBuilderProps } = props; if (!parameter.type) { throw new Error("Expected parameter.type"); } + const description = flixArgument?.messages?.title?.i18n?.["en-US"]; + return (
@@ -45,6 +52,10 @@ export function ParamBuilder(props: ParameterBuilderProps): ReactElement { {" "} {parameter.type.rawType}
+ {description && ( + {description} + )} +
); diff --git a/packages/ui/src/interactions/contexts/definition.context.tsx b/packages/ui/src/interactions/contexts/definition.context.tsx index b4aaa5b7..72c87024 100644 --- a/packages/ui/src/interactions/contexts/definition.context.tsx +++ b/packages/ui/src/interactions/contexts/definition.context.tsx @@ -12,10 +12,12 @@ import { import { ParsedInteraction } from "@onflowser/api"; import { FclValue } from "@onflowser/core"; import { useGetParsedInteraction } from "../../api"; +import { FlixTemplate, useFlixSearch } from "../../hooks/flix"; type InteractionDefinitionManager = InteractionParameterBuilder & { isParsing: boolean; parseError: string | undefined; + flixTemplate: FlixTemplate | undefined; parsedInteraction: ParsedInteraction | undefined; definition: InteractionDefinition; partialUpdate: (definition: Partial) => void; @@ -36,6 +38,10 @@ export function InteractionDefinitionManagerProvider(props: { const { update } = useInteractionRegistry(); const { data, isLoading } = useGetParsedInteraction(definition); const fclValuesByIdentifier = definition.fclValuesByIdentifier; + const { data: flixTemplate } = useFlixSearch({ + sourceCode: definition.code, + network: "any" + }) function partialUpdate(newDefinition: Partial) { update({ ...definition, ...newDefinition }); @@ -55,6 +61,7 @@ export function InteractionDefinitionManagerProvider(props: { ; @@ -39,16 +43,63 @@ const Context = createContext(undefined as never); export function TemplatesRegistryProvider(props: { children: React.ReactNode; }): ReactElement { - const { data: projectTemplatesData } = useGetInteractionTemplates(); - const [customTemplates, setRawTemplates] = useLocalStorage< - RawInteractionDefinitionTemplate[] + const { data: workspaceTemplates } = useGetWorkspaceInteractionTemplates(); + const { data: flixTemplates } = useListFlixTemplates(); + const { data: contracts } = useGetContracts(); + const [sessionTemplates, setSessionTemplates] = useLocalStorage< + SerializedSessionTemplate[] >("interactions", []); const randomId = () => String(Math.random() * 1000000); + const flowCoreContractNames = new Set([ + "FlowStorageFees", + "MetadataViews", + "NonFungibleToken", + "ViewResolver", + "FungibleToken", + "FungibleTokenMetadataViews", + "FlowToken", + "FlowClusterQC", + "FlowDKG", + "FlowEpoch", + "FlowIDTableStaking", + "FlowServiceAccount", + "FlowStakingCollection", + "LockedTokens", + "NFTStorefront", + "NFTStorefrontV2", + "StakingProxy", + "FlowFees", + ]); + + function isFlixTemplateUseful(template: FlixTemplate) { + const importedContractNames = Object.values(template.data.dependencies) + .map((dependency) => Object.keys(dependency)) + .flat(); + const nonCoreImportedContractNames = importedContractNames.filter( + (name) => !flowCoreContractNames.has(name), + ); + const nonCoreDeployedContractNamesLookup = new Set( + contracts + ?.filter((contract) => !flowCoreContractNames.has(contract.name)) + .map((contract) => contract.name), + ); + + // Interactions that only import core contracts are most likely generic ones, + // not tailored specifically to some third party contract/project. + const onlyImportsCoreContracts = nonCoreImportedContractNames.length === 0; + const importsSomeNonCoreDeployedContract = + nonCoreImportedContractNames.some((contractName) => + nonCoreDeployedContractNamesLookup.has(contractName), + ); + + return onlyImportsCoreContracts || importsSomeNonCoreDeployedContract; + } + const templates = useMemo( () => [ - ...customTemplates.map( + ...sessionTemplates.map( (template): InteractionDefinitionTemplate => ({ id: randomId(), name: template.name, @@ -60,10 +111,12 @@ export function TemplatesRegistryProvider(props: { fclValuesByIdentifier: new Map( Object.entries(template.fclValuesByIdentifier), ), - filePath: undefined, + workspace: undefined, + flix: undefined, + source: "session" }), ), - ...(projectTemplatesData?.map( + ...(workspaceTemplates?.map( (template): InteractionDefinitionTemplate => ({ id: randomId(), name: template.name, @@ -73,15 +126,32 @@ export function TemplatesRegistryProvider(props: { fclValuesByIdentifier: new Map(), createdDate: new Date(template.createdAt), updatedDate: new Date(template.updatedAt), - filePath: template.source?.filePath, + workspace: template, + flix: undefined, + source: "workspace" + }), + ) ?? []), + ...(flixTemplates?.filter(isFlixTemplateUseful)?.map( + (template): InteractionDefinitionTemplate => ({ + id: template.id, + name: getFlixTemplateName(template), + code: getCadenceWithNewImportSyntax(template), + transactionOptions: undefined, + initialOutcome: undefined, + fclValuesByIdentifier: new Map(), + createdDate: new Date(), + updatedDate: new Date(), + workspace: undefined, + flix: template, + source: "flix" }), ) ?? []), ], - [customTemplates, projectTemplatesData], + [sessionTemplates, workspaceTemplates], ); function saveTemplate(interaction: InteractionDefinition) { - const newTemplate: RawInteractionDefinitionTemplate = { + const newTemplate: SerializedSessionTemplate = { name: interaction.name, code: interaction.code, fclValuesByIdentifier: Object.fromEntries( @@ -92,8 +162,8 @@ export function TemplatesRegistryProvider(props: { updatedDate: new Date().toISOString(), }; - setRawTemplates([ - ...customTemplates.filter( + setSessionTemplates([ + ...sessionTemplates.filter( (template) => template.name !== newTemplate.name, ), newTemplate, @@ -101,7 +171,7 @@ export function TemplatesRegistryProvider(props: { } function removeTemplate(template: InteractionDefinitionTemplate) { - setRawTemplates((rawTemplates) => + setSessionTemplates((rawTemplates) => rawTemplates.filter( (existingTemplate) => existingTemplate.name !== template.name, ), @@ -130,3 +200,31 @@ export function useTemplatesRegistry(): InteractionTemplatesRegistry { return context; } + +// Transform imports with replacement patterns to the new import syntax, +// since FLIX v1.0 doesn't support new import syntax yet. +// https://github.com/onflow/flow-interaction-template-tools/issues/12 +function getCadenceWithNewImportSyntax(template: FlixTemplate) { + const replacementPatterns = Object.keys(template.data.dependencies); + return replacementPatterns.reduce( + (cadence, pattern) => { + const contractName = Object.keys(template.data.dependencies[pattern])[0]; + + return cadence + .replace(new RegExp(`from\\s+${pattern}`), "") + .replace(new RegExp(`import\\s+${contractName}`), `import "${contractName}"`) + }, + template.data.cadence, + ); +} + +function getFlixTemplateName(template: FlixTemplate) { + const englishTitle = template.data.messages?.title?.i18n?.["en-US"]; + if (englishTitle) { + // Transactions generated with NFT catalog have this necessary prefix in titles. + // https://github.com/onflow/nft-catalog + return englishTitle.replace("This transaction ", ""); + } else { + return "Unknown"; + } +}