diff --git a/package.json b/package.json index 7ba5be50..387130ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepsquare/app.deepsquare.io", - "version": "1.0.1", + "version": "1.1.0", "description": "Full-stack application of the DeepSquare Investment project base on Next.js.", "keywords": [ "deepsquare", @@ -90,6 +90,7 @@ "tailwindcss": "^3.2.7", "tsyringe": "^4.8.0", "type-graphql": "2.0.0-beta.2", + "uuid": "^9.0.0", "vanilla-jsoneditor": "^0.17.8", "viem": "^0.3.35", "wagmi": "^1.0.9", @@ -118,6 +119,7 @@ "@types/lodash": "^4.14.191", "@types/prop-types": "^15.7.5", "@types/react": "^18.2.13", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "@vue/compiler-sfc": "^3.2.47", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a063d74..925bed17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,9 @@ dependencies: type-graphql: specifier: 2.0.0-beta.2 version: 2.0.0-beta.2(class-validator@0.14.0)(graphql@16.6.0) + uuid: + specifier: ^9.0.0 + version: 9.0.0 vanilla-jsoneditor: specifier: ^0.17.8 version: 0.17.8 @@ -241,6 +244,9 @@ devDependencies: '@types/react': specifier: ^18.2.13 version: 18.2.13 + '@types/uuid': + specifier: ^9.0.2 + version: 9.0.2 '@typescript-eslint/eslint-plugin': specifier: ^5.60.0 version: 5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.34.0)(typescript@5.1.3) @@ -6045,6 +6051,10 @@ packages: resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==} dev: false + /@types/uuid@9.0.2: + resolution: {integrity: sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==} + dev: true + /@types/validator@13.7.17: resolution: {integrity: sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==} dev: false diff --git a/src/app/sandbox/page.tsx b/src/app/sandbox/page.tsx index ebd3bc44..6dfd72ef 100644 --- a/src/app/sandbox/page.tsx +++ b/src/app/sandbox/page.tsx @@ -7,6 +7,7 @@ // You should have received a copy of the GNU General Public License along with Nexus. If not, see . import type { NextPage } from 'next'; import dynamic from 'next/dynamic'; +import { useSearchParams } from 'next/navigation'; import randomWords from 'random-words'; import type { SubmitHandler } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -20,6 +21,7 @@ import CreditSubform from '@components/forms/CreditSubform'; import CustomLink from '@components/routing/Link'; import Card from '@components/ui/containers/Card/Card'; import type { Job } from '@graphql/external/sbatchServiceClient/generated/Types'; +import { useGetWorkflowQuery } from '@graphql/internal/client/generated/getWorkflow.generated'; import { yupResolver } from '@hookform/resolvers/yup'; import useBalances from '@hooks/useBalances'; import useGetMinimumAmount from '@hooks/useGetMinimumAmount'; @@ -28,6 +30,7 @@ import { authContext } from '@lib/contexts/AuthContext'; import { isWeb2 } from '@lib/types/AuthMethod'; import type { WorkloadFormData } from '@lib/types/WorkloadFormData'; import WorkloadType from '@lib/types/enums/WorkloadType'; +import LoadingButton from '@mui/lab/LoadingButton'; import formatCredit from '@utils/format/formatCredit'; import { formatWei } from '@utils/format/formatWei'; @@ -57,6 +60,8 @@ const schema = (maxAmount: bigint, minAmount: bigint, ignoreBalance: boolean) => type Content = { json: Job } | { text: string }; +type Store = { content: Content; initialized: boolean }; + function isJson(content: Content): content is { json: Job } { return (content as { json: Job }).json !== undefined; } @@ -83,16 +88,37 @@ const SandboxPage: NextPage = () => { const { balance_wCredit } = useBalances(); const { data: minAmount } = useGetMinimumAmount(); const { authMethod } = useContext(authContext); + const searchParams = useSearchParams(); + const workflowId = searchParams.get('workflowId'); + + const [store, setStore] = useState(() => { + if (typeof window === 'undefined') return { content: { text: JSON.stringify(defaultJob) }, initialized: false }; + const storedContent = localStorage.getItem(workflowId ? `sandbox-${workflowId}` : 'sandbox'); + return storedContent + ? (JSON.parse(storedContent) as Store) + : { + content: { text: JSON.stringify(defaultJob) }, + initialized: workflowId === null, + }; + }); - const [content, setContent] = useState(() => { - if (typeof window === 'undefined') return { text: JSON.stringify(defaultJob) }; - const storedContent = localStorage.getItem('userContent'); - return storedContent ? (JSON.parse(storedContent) as Content) : { text: JSON.stringify(defaultJob) }; + const { data, loading } = useGetWorkflowQuery({ + variables: { workflowId: workflowId! }, + skip: !workflowId, + onCompleted: (data) => { + if (data.getWorkflow && !store.initialized) { + setStore({ + content: { json: JSON.parse(data.getWorkflow) }, + initialized: true, + }); + } + }, }); + let json: any; try { - json = isJson(content) ? content.json : JSON.parse(content.text); + json = isJson(store.content) ? store.content.json : JSON.parse(store.content.text); } catch (e) { json = defaultJob; } @@ -100,8 +126,9 @@ const SandboxPage: NextPage = () => { const [jsonErrors, setJsonErrors] = useState({ validationErrors: [] }); useEffect(() => { - localStorage.setItem('userContent', JSON.stringify(content)); - }, [content]); + if (!store.initialized) return; + localStorage.setItem(workflowId ? `sandbox-${workflowId}` : 'sandbox', JSON.stringify(store)); + }, [store, workflowId]); const methods = useForm({ defaultValues: { @@ -149,20 +176,35 @@ const SandboxPage: NextPage = () => {
{ setJsonErrors(contentErrors); - setContent(newContent); + setStore((prev) => { + return { content: newContent, initialized: prev.initialized }; + }); }} /> + { + setStore({ + content: { text: data?.getWorkflow ? data?.getWorkflow : JSON.stringify(defaultJob) }, + initialized: true, + }); + }} + > + Reset +
) { return () => { void jsonEditorPromise.then((editor) => { void editor.destroy(); - editorRef.current = null; }); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/database/Workflow/Workflow.ts b/src/database/Workflow/Workflow.ts new file mode 100644 index 00000000..2bee746c --- /dev/null +++ b/src/database/Workflow/Workflow.ts @@ -0,0 +1,10 @@ +// Copyright 2023 Deepsquare Association +// This file is part of Foobar. +// Foobar is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// Foobar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with Foobar. If not, see . + +export default interface Workflow { + _id: string; + content: string; +} diff --git a/src/database/Workflow/WorkflowModel.ts b/src/database/Workflow/WorkflowModel.ts new file mode 100644 index 00000000..8be0827b --- /dev/null +++ b/src/database/Workflow/WorkflowModel.ts @@ -0,0 +1,18 @@ +// Copyright 2023 Deepsquare Association +// This file is part of Foobar. +// Foobar is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// Foobar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with Foobar. If not, see . +import type { Model } from 'mongoose'; +import { model, models, Schema } from 'mongoose'; +import { v4 } from 'uuid'; +import type Workflow from './Workflow'; + +export const WorkflowSchema = new Schema({ + _id: { type: String, default: v4 }, + content: { type: String, required: true }, +}); + +const WorkflowModel: Model = models.Workflow ?? model('Workflow', WorkflowSchema, 'workflows'); + +export default WorkflowModel; diff --git a/src/graphql/internal/client/generated/Types.ts b/src/graphql/internal/client/generated/Types.ts index b3d5c1b8..15f3af3d 100644 --- a/src/graphql/internal/client/generated/Types.ts +++ b/src/graphql/internal/client/generated/Types.ts @@ -170,6 +170,7 @@ export type ProviderPrices = { export type Query = { getJobHash: GetJobHashOutput; + getWorkflow?: Maybe; listJobs: Array; ping: Scalars['String']; }; @@ -178,6 +179,10 @@ export type QueryGetJobHashArgs = { jobId: Scalars['Hex']; }; +export type QueryGetWorkflowArgs = { + workflowId: Scalars['String']; +}; + export type QueryListJobsArgs = { userId: Scalars['String']; }; diff --git a/src/graphql/internal/client/generated/getWorkflow.generated.ts b/src/graphql/internal/client/generated/getWorkflow.generated.ts new file mode 100644 index 00000000..ccf950cd --- /dev/null +++ b/src/graphql/internal/client/generated/getWorkflow.generated.ts @@ -0,0 +1,49 @@ +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +import type * as Types from './Types'; + +const defaultOptions = {} as const; +export type GetWorkflowQueryVariables = Types.Exact<{ + workflowId: Types.Scalars['String']; +}>; + +export type GetWorkflowQuery = { getWorkflow?: string | null }; + +export const GetWorkflowDocument = /*#__PURE__*/ gql` + query GetWorkflow($workflowId: String!) { + getWorkflow(workflowId: $workflowId) + } +`; + +/** + * __useGetWorkflowQuery__ + * + * To run a query within a React component, call `useGetWorkflowQuery` and pass it any options that fit your needs. + * When your component renders, `useGetWorkflowQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetWorkflowQuery({ + * variables: { + * workflowId: // value for 'workflowId' + * }, + * }); + */ +export function useGetWorkflowQuery(baseOptions: Apollo.QueryHookOptions) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery(GetWorkflowDocument, options); +} +export function useGetWorkflowLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery(GetWorkflowDocument, options); +} +export type GetWorkflowQueryHookResult = ReturnType; +export type GetWorkflowLazyQueryHookResult = ReturnType; +export type GetWorkflowQueryResult = Apollo.QueryResult; +export function refetchGetWorkflowQuery(variables: GetWorkflowQueryVariables) { + return { query: GetWorkflowDocument, variables: variables }; +} diff --git a/src/graphql/internal/client/generated/introspection.json b/src/graphql/internal/client/generated/introspection.json index ede421ef..0d91c8c0 100644 --- a/src/graphql/internal/client/generated/introspection.json +++ b/src/graphql/internal/client/generated/introspection.json @@ -1228,6 +1228,27 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "getWorkflow", + "description": null, + "args": [ + { + "name": "workflowId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "listJobs", "description": null, diff --git a/src/graphql/internal/client/operations/getWorkflow.graphql b/src/graphql/internal/client/operations/getWorkflow.graphql new file mode 100644 index 00000000..e673177c --- /dev/null +++ b/src/graphql/internal/client/operations/getWorkflow.graphql @@ -0,0 +1,8 @@ +# Copyright 2023 Deepsquare Association +# This file is part of Nexus. +# Nexus is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# Nexus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with Nexus. If not, see . +query GetWorkflow($workflowId: String!) { + getWorkflow(workflowId: $workflowId) +} \ No newline at end of file diff --git a/src/graphql/internal/createSchema.ts b/src/graphql/internal/createSchema.ts index c5cec2b0..05c5d49f 100644 --- a/src/graphql/internal/createSchema.ts +++ b/src/graphql/internal/createSchema.ts @@ -9,6 +9,7 @@ import CancelJobMutation from '@graphql/internal/mutations/CancelJobMutation'; import CreateUserMutation from '@graphql/internal/mutations/CreateUserMutation'; import RequestJobMutation from '@graphql/internal/mutations/RequestJobMutation'; import GetJobHashQuery from '@graphql/internal/queries/GetJobHashQuery'; +import GetWorkflowQuery from '@graphql/internal/queries/GetWorkflowQuery'; import ListJobsQuery from '@graphql/internal/queries/ListJobsQuery'; import PingQuery from '@graphql/internal/queries/PingQuery'; import { BigIntScalar } from '@graphql/internal/scalars/BigIntScalar'; @@ -34,6 +35,7 @@ export default function createSchema() { resolvers: [ // Queries GetJobHashQuery, + GetWorkflowQuery, ListJobsQuery, PingQuery, diff --git a/src/graphql/internal/queries/GetWorkflowQuery.ts b/src/graphql/internal/queries/GetWorkflowQuery.ts new file mode 100644 index 00000000..537d7ce3 --- /dev/null +++ b/src/graphql/internal/queries/GetWorkflowQuery.ts @@ -0,0 +1,22 @@ +// Copyright 2023 Deepsquare Association +// This file is part of Foobar. +// Foobar is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// Foobar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with Foobar. If not, see . +import { injectable } from 'tsyringe'; +import { Arg, Query, Resolver } from 'type-graphql'; +import WorkflowModel from '../../../database/Workflow/WorkflowModel'; + +@injectable() +@Resolver() +export default class GetWorkflowQuery { + @Query(() => String, { nullable: true }) + async getWorkflow(@Arg('workflowId', () => String) workflowId: string) { + const workflow = await WorkflowModel.findById(workflowId).lean().exec(); + if (!workflow) { + return null; + } else { + return workflow.content; + } + } +}