diff --git a/apps/remix-ide/src/app.ts b/apps/remix-ide/src/app.ts index 6f524ae840e..d42977493d0 100644 --- a/apps/remix-ide/src/app.ts +++ b/apps/remix-ide/src/app.ts @@ -111,6 +111,7 @@ import Terminal from './app/panels/terminal' import TabProxy from './app/panels/tab-proxy.js' import { Plugin } from '@remixproject/engine' import BottomBarPanel from './app/components/bottom-bar-panel' +import { TemplateExplorerModalPlugin } from './app/plugins/remix-template-explorer-modal' const _paq = (window._paq = window._paq || []) @@ -157,6 +158,7 @@ class AppComponent { popupPanel: PopupPanel statusBar: StatusBar topBar: Topbar + templateExplorerModal: TemplateExplorerModalPlugin settings: SettingsTab params: any desktopClientMode: boolean @@ -267,6 +269,7 @@ class AppComponent { this.gistHandler = new GistHandler() // ----------------- theme service --------------------------------- this.themeModule = new ThemeModule() + this.templateExplorerModal = new TemplateExplorerModalPlugin(this.themeModule) // ----------------- locale service --------------------------------- this.localeModule = new LocaleModule() Registry.getInstance().put({ api: this.themeModule, name: 'themeModule' }) @@ -401,6 +404,8 @@ class AppComponent { const templateSelection = new TemplatesSelectionPlugin() + const templateExplorerModal = this.templateExplorerModal + const walletConnect = new WalletConnect() this.engine.register([ @@ -459,6 +464,7 @@ class AppComponent { scriptRunnerUI, remixAI, remixAiAssistant, + templateExplorerModal, walletConnect ]) @@ -611,6 +617,7 @@ class AppComponent { await this.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs']) await this.appManager.activatePlugin(['topbar']) await this.appManager.activatePlugin(['statusBar']) + // await this.appManager.activatePlugin(['remix-template-explorer-modal']) await this.appManager.activatePlugin(['bottomBar']) await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately await this.appManager.activatePlugin(['pinnedPanel']) diff --git a/apps/remix-ide/src/app/plugins/notification.tsx b/apps/remix-ide/src/app/plugins/notification.tsx index 2d0455a31de..c37b74d0a61 100644 --- a/apps/remix-ide/src/app/plugins/notification.tsx +++ b/apps/remix-ide/src/app/plugins/notification.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @nrwl/nx/enforce-module-boundaries */ import { Plugin } from '@remixproject/engine' import { LibraryProfile, MethodApi, StatusEvents } from '@remixproject/plugin-utils' import { AppModal } from '@remix-ui/app' @@ -10,6 +11,7 @@ interface INotificationApi { modal: (args: AppModal) => void alert: (args: AlertModal) => void toast: (message: string) => void + } } diff --git a/apps/remix-ide/src/app/plugins/remix-template-explorer-modal.tsx b/apps/remix-ide/src/app/plugins/remix-template-explorer-modal.tsx new file mode 100644 index 00000000000..65015ac2200 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/remix-template-explorer-modal.tsx @@ -0,0 +1,70 @@ +/* eslint-disable @nrwl/nx/enforce-module-boundaries */ +import React from 'react' +import { AppAction, AppState } from '@remix-ui/app' +import { PluginViewWrapper } from '@remix-ui/helper' +import { Plugin } from '@remixproject/engine' +import { EventEmitter } from 'events' +import { ThemeModule } from '../tabs/theme-module' +import { TemplateExplorerProvider } from 'libs/remix-ui/template-explorer-modal/context/template-explorer-context' + +const pluginProfile = { + name: 'remix-template-explorer-modal', + displayName: 'Remix Generic Modal', + description: 'Remix Generic Modal for every type of content meant for a modal', + methods: ['openModal'] +} + +export class TemplateExplorerModalPlugin extends Plugin { + element: HTMLDivElement + dispatch: React.Dispatch = () => { } + event: any + appStateDispatch: any + theme: any = null + constructor(theme: ThemeModule) { + super(pluginProfile) + this.element = document.createElement('div') + this.element.setAttribute('id', 'template-explorer-modal') + this.dispatch = () => { } + this.event = new EventEmitter() + this.theme = theme + } + + async onActivation(): Promise { + this.on('theme', 'themeChanged', (theme: any) => { + this.theme = theme + }) + } + + onDeactivation(): void { + this.element.remove() + } + + setDispatch(dispatch: React.Dispatch) { + this.dispatch = dispatch + this.renderComponent() + } + + setAppStateDispatch(appStateDispatch: React.Dispatch) { + this.appStateDispatch = appStateDispatch + } + + render() { + return ( +
+ +
+ ) + } + + renderComponent(): void { + this.dispatch({ + plugins: this, + }) + } + + updateComponent() { + return ( + + ) + } +} diff --git a/apps/remix-ide/src/app/plugins/remixGuide.tsx b/apps/remix-ide/src/app/plugins/remixGuide.tsx index 39ae0cb59a8..46247594e4a 100644 --- a/apps/remix-ide/src/app/plugins/remixGuide.tsx +++ b/apps/remix-ide/src/app/plugins/remixGuide.tsx @@ -121,7 +121,7 @@ export class RemixGuidePlugin extends ViewPlugin { > { section.cells.map((cell, index) => { return = { [Key in keyof M]: M[Key] extends undefined @@ -18,6 +19,7 @@ export const enum appActionTypes { setCanUseGit = 'SET_CAN_USE_GIT', setShowPopupPanel = 'SET_SHOW_POPUP_PANEL', setConnectedToDesktop = 'SET_CONNECTED_TO_DESKTOP', + showGenericModal = 'SHOW_GENERIC_MODAL', } type AppPayload = { @@ -26,7 +28,8 @@ type AppPayload = { [appActionTypes.setNeedsGitInit]: boolean, [appActionTypes.setCanUseGit]: boolean, [appActionTypes.setShowPopupPanel]: boolean, - [appActionTypes.setConnectedToDesktop]: desktopConnection + [appActionTypes.setConnectedToDesktop]: desktopConnection, + [appActionTypes.showGenericModal]: boolean } export type AppAction = ActionMap[keyof ActionMap< diff --git a/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts b/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts index 4c2e70764d9..c4fed0d70e9 100644 --- a/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/actions/modals.ts @@ -1,4 +1,4 @@ -import { AppModal } from '../interface' +import { AppModal, GenericModal } from '../interface' type ActionMap = { [Key in keyof M]: M[Key] extends undefined @@ -16,7 +16,8 @@ export const enum modalActionTypes { setToast = 'SET_TOAST', processQueue = 'PROCESS_QUEUEU', handleHideModal = 'HANDLE_HIDE_MODAL', - handleToaster = 'HANDLE_HIDE_TOAST' + handleToaster = 'HANDLE_HIDE_TOAST', + setTemplateExplorer = 'SET_TEMPLATE_EXPLORER' } type ModalPayload = { @@ -24,7 +25,8 @@ type ModalPayload = { [modalActionTypes.handleHideModal]: any [modalActionTypes.setToast]: { message: string | JSX.Element, timestamp: number } [modalActionTypes.handleToaster]: any, - [modalActionTypes.processQueue]: any + [modalActionTypes.processQueue]: any, + [modalActionTypes.setTemplateExplorer]: GenericModal } export type ModalAction = ActionMap[keyof ActionMap< diff --git a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx index baa7931ce85..4d68896c30f 100644 --- a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx @@ -6,7 +6,6 @@ import { modalReducer } from '../reducer/modals' import { ModalInitialState } from '../state/modals' import { ModalTypes } from '../types' import { AppContext, dispatchModalContext, modalContext, platformContext, onLineContext } from './context' - declare global { interface Window { _intl: IntlShape @@ -14,7 +13,7 @@ declare global { } export const ModalProvider = ({ children = [], reducer = modalReducer, initialState = ModalInitialState } = {}) => { - const [{ modals, toasters, focusModal, focusToaster }, dispatch] = useReducer(reducer, initialState) + const [{ modals, toasters, focusModal, focusToaster, focusTemplateExplorer }, dispatch] = useReducer(reducer, initialState) const onNextFn = async () => { dispatch({ @@ -86,7 +85,9 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt return ( - {children} + + {children} + ) } @@ -109,3 +110,11 @@ export const useDialogs = () => { export const useDialogDispatchers = () => { return React.useContext(dispatchModalContext) } + +export const defaultFocusTemplateExplorer = () => { + return ( + <> +

Template Explorer

+ + ) +} diff --git a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts index 084795e3212..d4d574630b0 100644 --- a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts +++ b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts @@ -1,5 +1,7 @@ +/* eslint-disable @nrwl/nx/enforce-module-boundaries */ import { branch, desktopConnection, GitHubUser } from '@remix-api' import { AppModalCancelTypes, ModalTypes } from '../types' +import { Template, TemplateGroup, TemplateOption } from 'libs/remix-ui/workspace/src/lib/utils/constants' export type ValidationResult = { valid: boolean, @@ -40,7 +42,8 @@ export interface ModalState { modals: AppModal[], toasters: {message: (string | JSX.Element), timestamp: number }[], focusModal: AppModal, - focusToaster: {message: (string | JSX.Element), timestamp: number } + focusToaster: {message: (string | JSX.Element), timestamp: number }, + focusTemplateExplorer: GenericModal } export interface forceChoiceModal { @@ -49,6 +52,47 @@ export interface forceChoiceModal { message: string | JSX.Element, } +export interface TemplateExplorerGenericData { + workspaceName: string, + modifyWorkspaceName: boolean, + workspaceDescription: string, + workspaceTemplateOptions: TemplateOption, + workspaceTemplateGroup: TemplateGroup, + workspaceTemplate: Template, + workspaceTags: string[] + searchTerm?: string + modifyWorkspace?: boolean +} + +export interface GenericModal { + id: string + title?: JSX.Element, + message: JSX.Element, + footer?: JSX.Element, + genericData?: any, + timestamp?: number + hide?: boolean + showModal?: boolean + validationFn?: (value: string) => ValidationResult + // eslint-disable-next-line no-undef + okLabel: string | JSX.Element + okFn?: (value?:any) => void + cancelLabel?: string | JSX.Element + cancelFn?: (reason?: AppModalCancelTypes) => void, + modalType?: ModalTypes, + modalParentClass?: string + defaultValue?: string + hideFn?: () => void, + resolve?: (value?:any) => void, + next?: () => void, + data?: any, + showCancelIcon?: boolean, + preventBlur?: boolean + placeholderText?: string + width?: string + height?: string +} + export interface AppState { gitHubUser: GitHubUser currentBranch: branch @@ -57,5 +101,6 @@ export interface AppState { showPopupPanel: boolean connectedToDesktop: desktopConnection desktopClientConnected: desktopConnection + genericModalState?: GenericModal } diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/app.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/app.ts index 2110a638997..f047fdb2f47 100644 --- a/libs/remix-ui/app/src/lib/remix-app/reducer/app.ts +++ b/libs/remix-ui/app/src/lib/remix-app/reducer/app.ts @@ -42,5 +42,12 @@ export const appReducer = (state: AppState, action: AppAction): AppState => { connectedToDesktop: action.payload } } + + case appActionTypes.showGenericModal: { + return { + ...state, + genericModalState: { ...state.genericModalState, showModal: action.payload } + } + } } -} \ No newline at end of file +} diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts index 9be604ce80f..4e26a6073af 100644 --- a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts @@ -83,5 +83,9 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda return { ...state, toasters: []} } } + + case modalActionTypes.setTemplateExplorer: { + return { ...state, focusTemplateExplorer: action.payload } + } } } diff --git a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx index 58c87575a24..86c4cc22a41 100644 --- a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @nrwl/nx/enforce-module-boundaries */ import React, { useEffect, useReducer, useRef, useState } from 'react' import './style/remix-app.css' import { RemixUIMainPanel } from '@remix-ui/panel' @@ -14,6 +15,8 @@ import { appReducer } from './reducer/app' import { appInitialState } from './state/app' import isElectron from 'is-electron' import { desktopConnectionType } from '@remix-api' +import { RemixUiTemplateExplorerModal } from 'libs/remix-ui/template-explorer-modal/src/lib/remix-ui-template-explorer-modal' +import { TemplateExplorerProvider } from 'libs/remix-ui/template-explorer-modal/context/template-explorer-context' declare global { interface Window { @@ -47,7 +50,20 @@ const RemixApp = (props: IRemixAppUi) => { const [appState, appStateDispatch] = useReducer(appReducer, { ...appInitialState, showPopupPanel: !window.localStorage.getItem('did_show_popup_panel') && !isElectron(), - connectedToDesktop: props.app.desktopClientMode ? desktopConnectionType .disconnected : desktopConnectionType .disabled + connectedToDesktop: props.app.desktopClientMode ? desktopConnectionType .disconnected : desktopConnectionType .disabled, + genericModalState: { + id: '', + title:
Default Title
, + message:
Default Message
, + footer:
Default Footer
, + okLabel: 'Default Ok Label', + okFn: () => { }, + cancelLabel: 'Default Cancel Label', + cancelFn: () => { }, + width: '720px', + height: '720px', + showModal: true + } }) useEffect(() => { @@ -218,6 +234,9 @@ const RemixApp = (props: IRemixAppUi) => { + {appState.genericModalState.showModal && + + } diff --git a/libs/remix-ui/app/src/lib/remix-app/state/app.ts b/libs/remix-ui/app/src/lib/remix-app/state/app.ts index 4c0eae622ee..d7b7fc4085b 100644 --- a/libs/remix-ui/app/src/lib/remix-app/state/app.ts +++ b/libs/remix-ui/app/src/lib/remix-app/state/app.ts @@ -9,4 +9,4 @@ export const appInitialState: AppState = { showPopupPanel: false, connectedToDesktop: desktopConnectionType.disabled, desktopClientConnected: desktopConnectionType.disabled -} \ No newline at end of file +} diff --git a/libs/remix-ui/app/src/lib/remix-app/state/modals.ts b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts index 56993cc7ec9..b48186f3091 100644 --- a/libs/remix-ui/app/src/lib/remix-app/state/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/state/modals.ts @@ -1,4 +1,6 @@ +import { Template, TemplateGroup } from '@remix-ui/workspace' import { ModalState } from '../interface' +import { defaultFocusTemplateExplorer } from '../context/provider' export const ModalInitialState: ModalState = { modals: [], @@ -15,5 +17,21 @@ export const ModalInitialState: ModalState = { cancelFn: () => { }, showCancelIcon: false }, - focusToaster: { message: '', timestamp: 0 } + focusToaster: { message: '', timestamp: 0 }, + focusTemplateExplorer: { + id: '', + hide: true, + title: defaultFocusTemplateExplorer(), + message: defaultFocusTemplateExplorer(), + footer: defaultFocusTemplateExplorer(), + validationFn: () => { return { valid: true, message: '' } }, + okLabel: '', + okFn: () => { }, + cancelLabel: '', + cancelFn: () => { }, + showCancelIcon: false, + preventBlur: false, + placeholderText: '', + genericData: {} + } } diff --git a/libs/remix-ui/environment-explorer/src/lib/components/environment-explorer-ui.tsx b/libs/remix-ui/environment-explorer/src/lib/components/environment-explorer-ui.tsx index 138c09b718c..ce2d36a44f4 100644 --- a/libs/remix-ui/environment-explorer/src/lib/components/environment-explorer-ui.tsx +++ b/libs/remix-ui/environment-explorer/src/lib/components/environment-explorer-ui.tsx @@ -96,7 +96,7 @@ export const EnvironmentExplorerUI = (props: environmentExplorerUIProps) => { > {section.providers.map(provider => ( { _paq.push(['trackEvent', 'hometab', 'header', 'Create a new workspace']) } + function openGenericModal(event): void { + appContext.appStateDispatch({ + type: appActionTypes.showGenericModal, + payload: true + }) + } + // if (appContext.appState.connectedToDesktop != desktopConnectionType.disabled) { // return (<>) // } @@ -85,6 +92,7 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
+
diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts index cd3f9f80af7..d828a30a303 100644 --- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts +++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts @@ -31,4 +31,5 @@ export interface ModalDialogProps { cancelBtnClass?: string, preventBlur?: boolean placeholderText?: string + genericData?: any } diff --git a/libs/remix-ui/template-explorer-modal/context/template-explorer-context.tsx b/libs/remix-ui/template-explorer-modal/context/template-explorer-context.tsx new file mode 100644 index 00000000000..b2ba6c15ab9 --- /dev/null +++ b/libs/remix-ui/template-explorer-modal/context/template-explorer-context.tsx @@ -0,0 +1,173 @@ +/* eslint-disable @nrwl/nx/enforce-module-boundaries */ +import React, { createContext, useContext, useEffect, useMemo, useReducer, useState } from 'react' +import { TemplateCategory, TemplateExplorerContextType, TemplateExplorerWizardAction, TemplateItem } from '../types/template-explorer-types' +import { initialState, templateExplorerReducer } from '../reducers/template-explorer-reducer' +import { metadata, templatesRepository } from '../src/utils/helpers' +import { AppContext } from '@remix-ui/app' +import { TemplateExplorerModalPlugin } from 'apps/remix-ide/src/app/plugins/remix-template-explorer-modal' +import { RemixUiTemplateExplorerModal } from '@remix-ui/template-explorer-modal' +import { TemplateExplorerModalFacade } from '../src/utils/workspaceUtils' +import { TemplateCategoryStrategy } from '../stategies/templateCategoryStrategy' + +export const TemplateExplorerContext = createContext({} as any) + +export const TemplateExplorerProvider = (props: { plugin: TemplateExplorerModalPlugin }) => { + const [state, dispatch] = useReducer(templateExplorerReducer, initialState) + const appContext = useContext(AppContext) + const { plugin } = props + const makeWorkspace = new TemplateExplorerModalFacade(plugin) + const templateCategoryStrategy = new TemplateCategoryStrategy() + + useEffect(() => { + dispatch({ type: TemplateExplorerWizardAction.SET_TEMPLATE_REPOSITORY, payload: templatesRepository }) + }, []) + + useEffect(() => { + dispatch({ type: TemplateExplorerWizardAction.SET_METADATA, payload: metadata }) + }, []) + + const setSearchTerm = (term: string) => { + dispatch({ type: TemplateExplorerWizardAction.SET_SEARCH_TERM, payload: term }) + } + + const allTags = useMemo((): string[] => { + const tags: string[] = [] + + if (state.templateRepository && Array.isArray(state.templateRepository)) { + state.templateRepository.forEach((template: any) => { + if (template && template.items && Array.isArray(template.items)) { + template.items.forEach((item: any) => { + if (item && item.tagList && Array.isArray(item.tagList)) { + item.tagList.forEach((tag: string) => { + if (typeof tag === 'string' && !tags.includes(tag)) { + tags.push(tag) + } + }) + } + }) + } + }) + } + + return tags.sort() + }, []) + + const recentTemplates = useMemo((): TemplateItem[] => { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(RECENT_KEY) : null + const list: string[] = raw ? JSON.parse(raw) : [] + const items: TemplateItem[] = [] + if (Array.isArray(state.templateRepository)) { + list.forEach((val) => { + for (const group of state.templateRepository as any[]) { + if (group && Array.isArray(group.items)) { + const found = group.items.find((it: any) => it && it.value === val) + if (found) { + items.push(found) + break + } + } + } + }) + } + //tag filter + const filtered = state.selectedTag + ? items.filter((it: any) => it && Array.isArray(it.tagList) && it.tagList.includes(state.selectedTag)) + : items + return filtered + } catch (e) { + return [] + } + }, [state.selectedTag, state.recentBump]) + + const filteredTemplates = useMemo((): TemplateCategory[] => { + const repo = (state.templateRepository as TemplateCategory[]) || [] + if (!Array.isArray(repo)) return [] + + const searchTerm = (state.searchTerm || '').trim().toLowerCase() + const selectedTag = state.selectedTag + + return repo + .map((template: TemplateCategory) => ({ + ...template, + items: (template.items || []).filter((item: TemplateItem) => { + // Filter by search term + const matchesSearch = !searchTerm || + (item.displayName || item.value || '').toLowerCase().includes(searchTerm) + + // Filter by selected tag + const matchesTag = !selectedTag || + (item.tagList && item.tagList.includes(selectedTag)) + + return matchesSearch && matchesTag + }) + })) + .filter((template: TemplateCategory) => + template && template.items && template.items.length > 0 + ) + }, [state.selectedTag, state.searchTerm, state.templateRepository]) + + const dedupedTemplates = useMemo((): TemplateCategory[] => { + const recentSet = new Set((recentTemplates || []).map((t: any) => t && t.value)) + const seen = new Set() + const makeUniqueItems = (items: any[]) => { + const unique: any[] = [] + for (const it of items || []) { + const val = it && it.value + if (!val) continue + if (recentSet.has(val)) continue + if (seen.has(val)) continue + seen.add(val) + unique.push(it) + } + return unique + } + return (filteredTemplates || []).map((group: any) => ({ + ...group, + items: makeUniqueItems(group && group.items ? group.items : []) + })).filter((g: any) => { + // Keep categories that have items OR special functionality (like Cookbook) + return g && ( + (g.items && g.items.length > 0) || + (g.name === 'Cookbook' && g.onClick) || + (g.hasOptions && g.name !== 'Cookbook') + ) + }) + }, [filteredTemplates, recentTemplates]) + + const handleTagClick = (tag: string) => { + dispatch({ type: TemplateExplorerWizardAction.SET_SELECTED_TAG, payload: state.selectedTag === tag ? null : tag }) + } + + const clearFilter = () => { + dispatch({ type: TemplateExplorerWizardAction.SET_SELECTED_TAG, payload: null }) + } + + const RECENT_KEY = 'remix.recentTemplates' + + const addRecentTemplate = (template: TemplateItem) => { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(RECENT_KEY) : null + const list: string[] = raw ? JSON.parse(raw) : [] + const filtered = list.filter((v) => v !== template.value) + filtered.unshift(template.value) + const trimmed = filtered.slice(0, 4) + if (typeof window !== 'undefined') window.localStorage.setItem(RECENT_KEY, JSON.stringify(trimmed)) + dispatch({ type: TemplateExplorerWizardAction.SET_RECENT_BUMP, payload: state.recentBump + 1 }) + } catch (e) { + + } + } + + const contextValue = { templateRepository: state.templateRepository, metadata: state.metadata, selectedTag: state.selectedTag, recentTemplates, filteredTemplates, dedupedTemplates, handleTagClick, clearFilter, addRecentTemplate, RECENT_KEY, allTags, plugin, setSearchTerm, dispatch, state, theme: plugin.theme, makeWorkspace, templateCategoryStrategy } + + return ( + + + + ) +} diff --git a/libs/remix-ui/template-explorer-modal/reducers/template-explorer-reducer.tsx b/libs/remix-ui/template-explorer-modal/reducers/template-explorer-reducer.tsx new file mode 100644 index 00000000000..bc79f3e649d --- /dev/null +++ b/libs/remix-ui/template-explorer-modal/reducers/template-explorer-reducer.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { ContractWizardAction, MetadataType, TemplateExplorerWizardAction, TemplateExplorerWizardState, TemplateRepository, WizardStep } from '../types/template-explorer-types' +import { metadata, templatesRepository } from '../src/utils/helpers' +import * as erc20 from '../src/contractCode/erc20' +import { getErc20ContractCode } from '../src/utils/contractWizardUtils' + +export const initialState: TemplateExplorerWizardState = { + workspaceTemplateChosen: '', + workspaceTemplateGroupChosen: '', + workspaceName: 'workspace Name', + defaultWorkspaceName: '', + topLeftNagivationName: '', + initializeAsGitRepo: false, + workspaceGeneratedWithAi: false, + searchTerm: '', + metadata: metadata as MetadataType, + templateRepository: templatesRepository as TemplateRepository || [], + selectedTag: null, + setSearchTerm: (term: string) => {}, + wizardStep: 'template', + setWizardStep: (step: WizardStep) => {}, + recentBump: 0, + contractType: 'erc20', + contractOptions: { + mintable: false, + burnable: false, + pausable: false + }, + contractAccessControl: '', + contractUpgradability: { + uups: false, + transparent: false + }, + contractCode: erc20.erc20DefaultNoOptions('MyToken'), + contractImport: '', + tokenName: 'MyToken', + contractName: 'MyToken' +} + +export const templateExplorerReducer = (state: TemplateExplorerWizardState, action: any) => { + switch (action.type) { + case TemplateExplorerWizardAction.SET_TEMPLATE_REPOSITORY: + return { ...state, templateRepository: action.payload } + case TemplateExplorerWizardAction.SET_METADATA: + return { ...state, metadata: action.payload } + case TemplateExplorerWizardAction.SELECT_TEMPLATE:{ + return { ...state, workspaceTemplateChosen: action.payload } + } + case TemplateExplorerWizardAction.SET_WORKSPACE_TEMPLATE_GROUP:{ + return { ...state, workspaceTemplateGroupChosen: action.payload } + } + case TemplateExplorerWizardAction.SET_WORKSPACE_NAME:{ + return { ...state, workspaceName: action.payload } + } + case TemplateExplorerWizardAction.SET_DEFAULT_WORKSPACE_NAME: + return { ...state, defaultWorkspaceName: action.payload } + case TemplateExplorerWizardAction.SET_TOP_LEFT_NAVIGATION_NAME: + return { ...state, topLeftNagivationName: action.payload } + case TemplateExplorerWizardAction.SET_INITIALIZE_AS_GIT_REPO: + return { ...state, initializeAsGitRepo: action.payload } + case TemplateExplorerWizardAction.SET_WORKSPACE_GENERATED_WITH_AI: + return { ...state, workspaceGeneratedWithAi: action.payload } + case TemplateExplorerWizardAction.END_WORKSPACE_WIZARD: + return { ...state, wizardStep: 'finishSetup' } + case TemplateExplorerWizardAction.SET_SELECTED_TAG: { + return { ...state, selectedTag: action.payload } + } + case TemplateExplorerWizardAction.SET_RECENT_BUMP: { + return { ...state, recentBump: action.payload } + } + case TemplateExplorerWizardAction.CLEAR_SELECTED_TAG: { + return { ...state, selectedTag: null } + } + case TemplateExplorerWizardAction.SET_SEARCH_TERM: { + return { ...state, searchTerm: action.payload } + } + case TemplateExplorerWizardAction.SET_WIZARD_STEP: { + console.log('action.payload wizardStep', action.payload) + return { ...state, wizardStep: action.payload } + } + case ContractWizardAction.CONTRACT_TYPE_UPDATED: { + return { ...state, contractType: action.payload } + } + case ContractWizardAction.CONTRACT_UPGRADABILITY_UPDATE: { + return { ...state, contractUpgradability: action.payload } + } + case ContractWizardAction.CONTRACT_ACCESS_CONTROL_UPDATE: { + return { ...state, contractAccessControl: action.payload } + } + case ContractWizardAction.CONTRACT_OPTIONS_UPDATE: { + return { ...state, contractOptions: action.payload } + } + case ContractWizardAction.CONTRACT_CODE_UPDATE: { + return { ...state, contractCode: action.payload } + } + case ContractWizardAction.CONTRACT_IMPORT_UPDATE: { + return { ...state, contractImport: action.payload } + } + case ContractWizardAction.INITIALIZE_AS_GIT_REPO_UPDATE: { + return { ...state, initializeAsGitRepo: action.payload } + } + case ContractWizardAction.TOKEN_NAME_UPDATE: { + return { ...state, tokenName: action.payload } + } + case ContractWizardAction.CONTRACT_NAME_UPDATE: { + console.log('action.payload contractName', action.payload) + return { ...state, contractName: action.payload } + } + default: { + return { ...state, contractCode: getErc20ContractCode('erc20', state) } + } + } +} + +function doTemplateSearch (searchTerm: string, repo: TemplateRepository) { + if (!searchTerm) return repo + return repo.filter(template => template.name.toLowerCase().includes(searchTerm.toLowerCase())) + .map(template => ({ + ...template, + items: template.items.filter(item => item.displayName.toLowerCase().includes(searchTerm.toLowerCase())) + })) +} diff --git a/libs/remix-ui/template-explorer-modal/src/components/contract-wizard.tsx b/libs/remix-ui/template-explorer-modal/src/components/contract-wizard.tsx new file mode 100644 index 00000000000..389a90aacc8 --- /dev/null +++ b/libs/remix-ui/template-explorer-modal/src/components/contract-wizard.tsx @@ -0,0 +1,182 @@ +import React,{ useContext, useEffect, useMemo, useReducer, useState } from 'react' +import Editor from '@monaco-editor/react' +import * as erc20 from '../contractCode/erc20' +import * as erc721 from '../contractCode/erc721' +import * as erc1155 from '../contractCode/erc1155' +import { AccessControlType, ContractTypeStrategy, ContractWizardAction } from '../../types/template-explorer-types' +import { getErc1155ContractCode, getErc20ContractCode, getErc721ContractCode } from '../utils/contractWizardUtils' +import { TemplateExplorerContext } from '../../context/template-explorer-context' +import { CustomTooltip } from '@remix-ui/helper' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import { TemplateExplorerModalFacade } from '../utils/workspaceUtils' + +const defaultStrategy: ContractTypeStrategy = { + contractType: 'erc20', + contractOptions: { + mintable: false, + burnable: false, + pausable: false + }, + contractAccessControl: '', + contractUpgradability: { + uups: false, + transparent: false + }, + contractCode: erc20.erc20DefaultNoOptions('MyToken'), + contractImport: '', + initializeAsGitRepo: false +} + +export function ContractWizard () { + const [showEditModal, setShowEditModal] = useState(false) + const { state, dispatch, theme, makeWorkspace } = useContext(TemplateExplorerContext) + const strategy = state + + function toggleContractOption(key: keyof typeof strategy.contractOptions) { + if (key === 'mintable') { + dispatch({ type: ContractWizardAction.CONTRACT_OPTIONS_UPDATE, payload: { ...strategy.contractOptions, [key]: !strategy.contractOptions[key] } }) + switchAccessControl('ownable') + } else if (key === 'pausable') { + dispatch({ type: ContractWizardAction.CONTRACT_OPTIONS_UPDATE, payload: { ...strategy.contractOptions, [key]: !strategy.contractOptions[key] } }) + switchAccessControl('ownable') + } + dispatch({ type: ContractWizardAction.CONTRACT_OPTIONS_UPDATE, payload: { ...strategy.contractOptions, [key]: !strategy.contractOptions[key] } }) + } + + function switchAccessControl(accessControl: AccessControlType) { + console.log('switchAccessControl', accessControl) + dispatch({ type: ContractWizardAction.CONTRACT_ACCESS_CONTROL_UPDATE, payload: accessControl }) + } + function updateTokenName(tokenName: string) { + dispatch({ type: ContractWizardAction.TOKEN_NAME_UPDATE, payload: tokenName }) + } + function updateContractName(contractName: string) { + dispatch({ type: ContractWizardAction.CONTRACT_NAME_UPDATE, payload: contractName }) + dispatch({ type: ContractWizardAction.TOKEN_NAME_UPDATE, payload: contractName }) + } + + useEffect(() => { + if (strategy.contractType === 'erc20') { + dispatch({ type: ContractWizardAction.CONTRACT_CODE_UPDATE, payload: getErc20ContractCode(strategy.contractType, strategy) }) + } else if (strategy.contractType === 'erc721') { + dispatch({ type: ContractWizardAction.CONTRACT_CODE_UPDATE, payload: getErc721ContractCode(strategy.contractType, strategy) }) + } else if (strategy.contractType === 'erc1155') { + dispatch({ type: ContractWizardAction.CONTRACT_CODE_UPDATE, payload: getErc1155ContractCode(strategy.contractType, strategy) }) + } + }, [strategy.contractType, strategy.contractOptions, strategy.contractAccessControl, strategy.contractUpgradability, strategy.contractName]) + + return ( +
+
+
+
+ {showEditModal ? updateContractName(e.target.value)} /> : + {state.tokenName} + } + setShowEditModal(true)}> +
+
+ + +
+
+ +
+
+
+
Contract settings
+ + +
+ +
+
Features
+
+ { + toggleContractOption('mintable')} + } /> + +
+
+ toggleContractOption('burnable')} /> + +
+
+ toggleContractOption('pausable')} /> + +
+
+ +
+
Access control
+
+ switchAccessControl('ownable')} /> + +
+
+ switchAccessControl('roles')} /> + +
+
+ switchAccessControl('managed')} /> + +
+
+ +
+
Upgradability
+
+ dispatch({ type: ContractWizardAction.CONTRACT_UPGRADABILITY_UPDATE, payload: { ...strategy.contractUpgradability, uups: !strategy.contractUpgradability.uups } })} /> + +
+
+ dispatch({ type: ContractWizardAction.CONTRACT_UPGRADABILITY_UPDATE, payload: { ...strategy.contractUpgradability, transparent: !strategy.contractUpgradability.transparent } })} /> + +
+
+
+
+ +
+
+ +
+
+
+ <> + dispatch({ type: ContractWizardAction.INITIALIZE_AS_GIT_REPO_UPDATE, payload: e.target.checked })} /> + + +
+ + +
+
+
+
+ ) +} diff --git a/libs/remix-ui/template-explorer-modal/src/components/generateWorkspaceWithAi.tsx b/libs/remix-ui/template-explorer-modal/src/components/generateWorkspaceWithAi.tsx new file mode 100644 index 00000000000..dfbfb397e06 --- /dev/null +++ b/libs/remix-ui/template-explorer-modal/src/components/generateWorkspaceWithAi.tsx @@ -0,0 +1,31 @@ +import React, { useReducer } from 'react' +import { TemplateExplorerWizardAction } from '../../types/template-explorer-types' +import { initialState, templateExplorerReducer } from '../../reducers/template-explorer-reducer' + +export function GenerateWorkspaceWithAi() { + const [state, dispatch] = useReducer(templateExplorerReducer, initialState) + return ( +
+
+
+ + Beta +
+
+