diff --git a/apps/remix-ide/src/app/files/dgitProvider.js b/apps/remix-ide/src/app/files/dgitProvider.js index 28f7fb2715..d3e60a954e 100644 --- a/apps/remix-ide/src/app/files/dgitProvider.js +++ b/apps/remix-ide/src/app/files/dgitProvider.js @@ -50,6 +50,8 @@ class DGitProvider extends Plugin { async getGitConfig () { const workspace = await this.call('filePanel', 'getCurrentWorkspace') + + if (!workspace) return return { fs: window.remixFileSystemCallback, dir: addSlash(workspace.absolutePath) @@ -106,14 +108,17 @@ class DGitProvider extends Plugin { }, 1000) } - async checkout (cmd) { + async checkout (cmd, refresh = true) { await git.checkout({ ...await this.getGitConfig(), ...cmd }) - setTimeout(async () => { - await this.call('fileManager', 'refresh') - }, 1000) + if (refresh) { + setTimeout(async () => { + await this.call('fileManager', 'refresh') + }, 1000) + } + this.emit('checkout') } async log (cmd) { @@ -124,39 +129,42 @@ class DGitProvider extends Plugin { return status } - async remotes () { + async remotes (config) { let remotes = [] try { - remotes = await git.listRemotes({ ...await this.getGitConfig() }) + remotes = await git.listRemotes({ ...config ? config : await this.getGitConfig() }) } catch (e) { // do nothing } return remotes } - async branch (cmd) { + async branch (cmd, refresh = true) { const status = await git.branch({ ...await this.getGitConfig(), ...cmd }) - setTimeout(async () => { - await this.call('fileManager', 'refresh') - }, 1000) + if (refresh) { + setTimeout(async () => { + await this.call('fileManager', 'refresh') + }, 1000) + } + this.emit('branch') return status } - async currentbranch () { - const name = await git.currentBranch({ - ...await this.getGitConfig() - }) + async currentbranch (config) { + const defaultConfig = await this.getGitConfig() + const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig + const name = await git.currentBranch(cmd) + return name } - async branches () { - const cmd = { - ...await this.getGitConfig() - } - const remotes = await this.remotes() + async branches (config) { + const defaultConfig = await this.getGitConfig() + const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig + const remotes = await this.remotes(config) let branches = [] branches = (await git.listBranches(cmd)).map((branch) => { return { remote: undefined, name: branch } }) for (const remote of remotes) { @@ -387,6 +395,8 @@ class DGitProvider extends Plugin { pinata_api_key: pinataApiKey, pinata_secret_api_key: pinataSecretApiKey } + }).catch((e) => { + console.log(e) }) // also commit to remix IPFS for availability after pinning to Pinata return await this.export(this.remixIPFS) || result.data.IpfsHash @@ -405,6 +415,8 @@ class DGitProvider extends Plugin { pinata_api_key: pinataApiKey, pinata_secret_api_key: pinataSecretApiKey } + }).catch((e) => { + console.log('Pinata unreachable') }) return result.data } catch (error) { diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index 847d6f3f02..d6f5fd9e4b 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -818,8 +818,8 @@ class FileManager extends Plugin { } } - async isGitRepo (directory: string): Promise { - const path = directory + '/.git' + async isGitRepo (): Promise { + const path = '.git' const exists = await this.exists(path) return exists diff --git a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts index d9fd9b03b9..02a688b9a6 100644 --- a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts +++ b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts @@ -123,3 +123,8 @@ export const shortenHexData = (data) => { const len = data.length return data.slice(0, 5) + '...' + data.slice(len - 5, len) } + +export const addSlash = (file: string) => { + if (!file.startsWith('/'))file = '/' + file + return file +} diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index aa06fb7840..8c48726c98 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -53,6 +53,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React. const params = queryParams.get() as UrlParametersType const workspaces = await getWorkspaces() || [] dispatch(setWorkspaces(workspaces)) + // console.log('workspaces: ', workspaces) if (params.gist) { await createWorkspaceTemplate('gist-sample', 'gist-template') plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false }) diff --git a/libs/remix-ui/workspace/src/lib/actions/payload.ts b/libs/remix-ui/workspace/src/lib/actions/payload.ts index 8c3fb8fc18..eeea274dc2 100644 --- a/libs/remix-ui/workspace/src/lib/actions/payload.ts +++ b/libs/remix-ui/workspace/src/lib/actions/payload.ts @@ -126,7 +126,7 @@ export const createWorkspaceRequest = (promise: Promise) => { } } -export const createWorkspaceSuccess = (workspaceName: { name: string; isGitRepo: boolean; }) => { +export const createWorkspaceSuccess = (workspaceName: { name: string; isGitRepo: boolean; branches?: { remote: any; name: string; }[], currentBranch?: string }) => { return { type: 'CREATE_WORKSPACE_SUCCESS', payload: workspaceName @@ -264,3 +264,17 @@ export const cloneRepositoryFailed = () => { type: 'CLONE_REPOSITORY_FAILED' } } + +export const setCurrentWorkspaceBranches = (branches?: { remote: any, name: string }[]) => { + return { + type: 'SET_CURRENT_WORKSPACE_BRANCHES', + payload: branches + } +} + +export const setCurrentWorkspaceCurrentBranch = (currentBranch?: string) => { + return { + type: 'SET_CURRENT_WORKSPACE_CURRENT_BRANCH', + payload: currentBranch + } +} diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 0495875a87..db6ff0804b 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,13 +1,20 @@ import React from 'react' import { bufferToHex, keccakFromString } from 'ethereumjs-util' import axios, { AxiosResponse } from 'axios' -import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' -import { checkSlash, checkSpecialChars } from '@remix-ui/helper' +import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setCurrentWorkspaceBranches, setCurrentWorkspaceCurrentBranch, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' +import { addSlash, checkSlash, checkSpecialChars } from '@remix-ui/helper' import { JSONStandardInput, WorkspaceTemplate } from '../types' import { QueryParams } from '@remix-project/remix-lib' import * as templateWithContent from '@remix-project/remix-ws-templates' import { ROOT_PATH } from '../utils/constants' +// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries +import { IndexedDBStorage } from '../../../../../../apps/remix-ide/src/app/files/filesystems/indexedDB' +import { getUncommittedFiles } from '../utils/gitStatusFilter' + +declare global { + interface Window { remixFileSystemCallback: IndexedDBStorage; } +} const LOCALHOST = ' - connect to localhost - ' @@ -19,6 +26,13 @@ let plugin, dispatch: React.Dispatch export const setPlugin = (filePanelPlugin, reducerDispatch) => { plugin = filePanelPlugin dispatch = reducerDispatch + plugin.on('dGitProvider', 'checkout', async () => { + const currentBranch = await plugin.call('dGitProvider', 'currentbranch') + dispatch(setCurrentWorkspaceCurrentBranch(currentBranch)) + }) + plugin.on('dGitProvider', 'branch', async () => { + await refreshBranches() + }) } export const addInputField = async (type: 'file' | 'folder', path: string, cb?: (err: Error, result?: string | number | boolean | Record) => void) => { @@ -54,13 +68,13 @@ export const createWorkspace = async (workspaceName: string, workspaceTemplateNa await plugin.workspaceCreated(workspaceName) if (isGitRepo) { - await plugin.call('dGitProvider', 'init') + await plugin.call('dGitProvider', 'init', { branch: 'main' }) + dispatch(setCurrentWorkspaceCurrentBranch('main')) const isActive = await plugin.call('manager', 'isActive', 'dgit') if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') } if (!isEmpty) await loadWorkspacePreset(workspaceTemplateName, opts) - cb && cb(null, workspaceName) }).catch((error) => { dispatch(createWorkspaceError({ error })) @@ -265,6 +279,11 @@ export const switchToWorkspace = async (name: string) => { await plugin.setWorkspace({ name, isLocalhost: false }) const isGitRepo = await plugin.fileManager.isGitRepo() + if (isGitRepo) { + const isActive = await plugin.call('manager', 'isActive', 'dgit') + + if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') + } dispatch(setMode('browser')) dispatch(setCurrentWorkspace({ name, isGitRepo })) dispatch(setReadOnlyMode(false)) @@ -313,9 +332,9 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error, }) } -export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean}[]> | undefined => { +export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }[]> | undefined => { try { - const workspaces: {name: string, isGitRepo: boolean}[] = await new Promise((resolve, reject) => { + const workspaces: {name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string}[] = await new Promise((resolve, reject) => { const workspacesPath = plugin.fileProviders.workspace.workspacesPath plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { @@ -326,9 +345,24 @@ export const getWorkspaces = async (): Promise<{name: string, isGitRepo: boolean .filter((item) => items[item].isDirectory) .map(async (folder) => { const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git') - return { - name: folder.replace(workspacesPath + '/', ''), - isGitRepo + + if (isGitRepo) { + let branches = [] + let currentBranch = null + + branches = await getGitRepoBranches(folder) + currentBranch = await getGitRepoCurrentBranch(folder) + return { + name: folder.replace(workspacesPath + '/', ''), + isGitRepo, + branches, + currentBranch + } + } else { + return { + name: folder.replace(workspacesPath + '/', ''), + isGitRepo + } } })).then(workspacesList => resolve(workspacesList)) }) @@ -355,6 +389,13 @@ export const cloneRepository = async (url: string) => { if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') await fetchWorkspaceDirectory(ROOT_PATH) + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + const branches = await getGitRepoBranches(workspacesPath + '/' + repoName) + + dispatch(setCurrentWorkspaceBranches(branches)) + const currentBranch = await getGitRepoCurrentBranch(workspacesPath + '/' + repoName) + + dispatch(setCurrentWorkspaceCurrentBranch(currentBranch)) dispatch(cloneRepositorySuccess()) }).catch(() => { const cloneModal = { @@ -397,3 +438,153 @@ export const getRepositoryTitle = async (url: string) => { return name + counter } + +export const getGitRepoBranches = async (workspacePath: string) => { + const gitConfig: { fs: IndexedDBStorage, dir: string } = { + fs: window.remixFileSystemCallback, + dir: addSlash(workspacePath) + } + const branches: { remote: any; name: string; }[] = await plugin.call('dGitProvider', 'branches', { ...gitConfig }) + + return branches +} + +export const getGitRepoCurrentBranch = async (workspaceName: string) => { + const gitConfig: { fs: IndexedDBStorage, dir: string } = { + fs: window.remixFileSystemCallback, + dir: addSlash(workspaceName) + } + const currentBranch: string = await plugin.call('dGitProvider', 'currentbranch', { ...gitConfig }) + + return currentBranch +} + +export const showAllBranches = async () => { + const isActive = await plugin.call('manager', 'isActive', 'dgit') + + if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') + plugin.call('menuicons', 'select', 'dgit') +} + +const refreshBranches = async () => { + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + const workspaceName = plugin.fileProviders.workspace.workspace + const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName) + + dispatch(setCurrentWorkspaceBranches(branches)) +} + +export const switchBranch = async (branch: string) => { + await plugin.call('fileManager', 'closeAllFiles') + const localChanges = await hasLocalChanges() + + if (Array.isArray(localChanges) && localChanges.length > 0) { + const cloneModal = { + id: 'switchBranch', + title: 'Switch Git Branch', + message: `Your local changes to the following files would be overwritten by checkout.\n + ${localChanges.join('\n')}\n + Do you want to continue?`, + modalType: 'modal', + okLabel: 'Force Checkout', + okFn: async () => { + dispatch(cloneRepositoryRequest()) + plugin.call('dGitProvider', 'checkout', { ref: branch, force: true }, false).then(async () => { + await fetchWorkspaceDirectory(ROOT_PATH) + dispatch(setCurrentWorkspaceCurrentBranch(branch)) + dispatch(cloneRepositorySuccess()) + }).catch(() => { + dispatch(cloneRepositoryFailed()) + }) + }, + cancelLabel: 'Cancel', + cancelFn: () => {}, + hideFn: () => {} + } + plugin.call('notification', 'modal', cloneModal) + } else { + dispatch(cloneRepositoryRequest()) + plugin.call('dGitProvider', 'checkout', { ref: branch, force: true }, false).then(async () => { + await fetchWorkspaceDirectory(ROOT_PATH) + dispatch(setCurrentWorkspaceCurrentBranch(branch)) + dispatch(cloneRepositorySuccess()) + }).catch(() => { + dispatch(cloneRepositoryFailed()) + }) + } +} + +export const createNewBranch = async (branch: string) => { + const promise = plugin.call('dGitProvider', 'branch', { ref: branch, checkout: true }, false) + + dispatch(cloneRepositoryRequest()) + promise.then(async () => { + await fetchWorkspaceDirectory(ROOT_PATH) + dispatch(setCurrentWorkspaceCurrentBranch(branch)) + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + const workspaceName = plugin.fileProviders.workspace.workspace + const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName) + + dispatch(setCurrentWorkspaceBranches(branches)) + dispatch(cloneRepositorySuccess()) + }).catch(() => { + dispatch(cloneRepositoryFailed()) + }) + return promise +} + +export const checkoutRemoteBranch = async (branch: string, remote: string) => { + const localChanges = await hasLocalChanges() + + if (Array.isArray(localChanges) && localChanges.length > 0) { + const cloneModal = { + id: 'checkoutRemoteBranch', + title: 'Checkout Remote Branch', + message: `Your local changes to the following files would be overwritten by checkout.\n + ${localChanges.join('\n')}\n + Do you want to continue?`, + modalType: 'modal', + okLabel: 'Force Checkout', + okFn: async () => { + dispatch(cloneRepositoryRequest()) + plugin.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false).then(async () => { + await fetchWorkspaceDirectory(ROOT_PATH) + dispatch(setCurrentWorkspaceCurrentBranch(branch)) + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + const workspaceName = plugin.fileProviders.workspace.workspace + const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName) + + dispatch(setCurrentWorkspaceBranches(branches)) + dispatch(cloneRepositorySuccess()) + }).catch(() => { + dispatch(cloneRepositoryFailed()) + }) + }, + cancelLabel: 'Cancel', + cancelFn: () => {}, + hideFn: () => {} + } + plugin.call('notification', 'modal', cloneModal) + } else { + dispatch(cloneRepositoryRequest()) + plugin.call('dGitProvider', 'checkout', { ref: branch, remote, force: true }, false).then(async () => { + await fetchWorkspaceDirectory(ROOT_PATH) + dispatch(setCurrentWorkspaceCurrentBranch(branch)) + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + const workspaceName = plugin.fileProviders.workspace.workspace + const branches = await getGitRepoBranches(workspacesPath + '/' + workspaceName) + + dispatch(setCurrentWorkspaceBranches(branches)) + dispatch(cloneRepositorySuccess()) + }).catch(() => { + dispatch(cloneRepositoryFailed()) + }) + } +} + +export const hasLocalChanges = async () => { + const filesStatus = await plugin.call('dGitProvider', 'status') + const uncommittedFiles = getUncommittedFiles(filesStatus) + + return uncommittedFiles +} diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 2fd6a71b46..2e199c2f60 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -32,6 +32,10 @@ export const FileSystemContext = createContext<{ dispatchHandleRestoreBackup: () => Promise dispatchCloneRepository: (url: string) => Promise, dispatchMoveFile: (src: string, dest: string) => Promise, - dispatchMoveFolder: (src: string, dest: string) => Promise + dispatchMoveFolder: (src: string, dest: string) => Promise, + dispatchShowAllBranches: () => Promise, + dispatchSwitchToBranch: (branch: string) => Promise, + dispatchCreateNewBranch: (branch: string) => Promise, + dispatchCheckoutRemoteBranch: (branch: string, remote: string) => Promise }>(null) \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css index 55357dacae..13756a9b94 100644 --- a/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css +++ b/libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css @@ -84,6 +84,7 @@ border-radius: .25rem; background: var(--custom-select); } + .custom-dropdown-items a { border-radius: .25rem; text-transform: none; @@ -133,3 +134,7 @@ color: var(--text) } + .checkout-input { + font-size: 10px !important; + } + diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 2b1409fe86..2b01004d50 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -7,7 +7,9 @@ import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, - fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository, moveFile, moveFolder } from '../actions' + fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository, moveFile, moveFolder, + showAllBranches, switchBranch, createNewBranch, checkoutRemoteBranch +} from '../actions' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -136,6 +138,22 @@ export const FileSystemProvider = (props: WorkspaceProps) => { const dispatchMoveFolder = async (src: string, dest: string) => { await moveFolder(src, dest) } + + const dispatchShowAllBranches = async () => { + await showAllBranches() + } + + const dispatchSwitchToBranch = async (branch: string) => { + await switchBranch(branch) + } + + const dispatchCreateNewBranch = async (branch: string) => { + await createNewBranch(branch) + } + + const dispatchCheckoutRemoteBranch = async (branch: string, remote: string) => { + await checkoutRemoteBranch(branch, remote) + } useEffect(() => { dispatchInitWorkspace() @@ -241,7 +259,11 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchHandleRestoreBackup, dispatchCloneRepository, dispatchMoveFile, - dispatchMoveFolder + dispatchMoveFolder, + dispatchShowAllBranches, + dispatchSwitchToBranch, + dispatchCreateNewBranch, + dispatchCheckoutRemoteBranch } return ( diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index af20d18fa2..ab982ea269 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -13,6 +13,11 @@ export interface BrowserState { workspaces: { name: string; isGitRepo: boolean; + branches?: { + remote: any; + name: string; + }[], + currentBranch?: string }[], files: { [x: string]: Record }, expandPath: string[] @@ -117,7 +122,7 @@ export const browserInitialState: BrowserState = { export const browserReducer = (state = browserInitialState, action: Action) => { switch (action.type) { case 'SET_CURRENT_WORKSPACE': { - const payload = action.payload as { name: string; isGitRepo: boolean; } + const payload = action.payload as { name: string; isGitRepo: boolean; branches?: { remote: any; name: string; }[], currentBranch?: string } const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] return { @@ -131,7 +136,8 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } case 'SET_WORKSPACES': { - const payload = action.payload as { name: string; isGitRepo: boolean; }[] + console.log('called SET_WORKSPACES') + const payload = action.payload as { name: string; isGitRepo: boolean; branches?: { remote: any; name: string; }[], currentBranch?: string }[] return { ...state, @@ -429,7 +435,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } case 'CREATE_WORKSPACE_SUCCESS': { - const payload = action.payload as { name: string; isGitRepo: boolean; } + const payload = action.payload as { name: string; isGitRepo: boolean; branches?: { remote: any; name: string; }[], currentBranch?: string } const workspaces = state.browser.workspaces.find(({ name }) => name === payload.name) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] return { @@ -460,13 +466,15 @@ export const browserReducer = (state = browserInitialState, action: Action) => { case 'RENAME_WORKSPACE': { const payload = action.payload as { oldName: string, workspaceName: string } let renamedWorkspace - const workspaces = state.browser.workspaces.filter(({ name, isGitRepo }) => { + const workspaces = state.browser.workspaces.filter(({ name, isGitRepo, branches, currentBranch }) => { if (name && (name !== payload.oldName)) { return true } else { renamedWorkspace = { name: payload.workspaceName, - isGitRepo + isGitRepo, + branches, + currentBranch } return false } @@ -666,6 +674,36 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } + case 'SET_CURRENT_WORKSPACE_BRANCHES': { + const payload: { remote: any, name: string }[] = action.payload + + return { + ...state, + browser: { + ...state.browser, + workspaces: state.browser.workspaces.map((workspace) => { + if (workspace.name === state.browser.currentWorkspace) workspace.branches = payload + return workspace + }) + } + } + } + + case 'SET_CURRENT_WORKSPACE_CURRENT_BRANCH': { + const payload: string = action.payload + + return { + ...state, + browser: { + ...state.browser, + workspaces: state.browser.workspaces.map((workspace) => { + if (workspace.name === state.browser.currentWorkspace) workspace.currentBranch = payload + return workspace + }) + } + } + } + default: throw new Error() } diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 25975f37b4..473460382e 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useContext, SyntheticEvent } from 'react' // eslint-disable-line +import React, { useState, useEffect, useRef, useContext, SyntheticEvent, ChangeEvent, KeyboardEvent } from 'react' // eslint-disable-line import { Dropdown, OverlayTrigger, Tooltip } from 'react-bootstrap' import { CustomIconsToggle, CustomMenu, CustomToggle } from '@remix-ui/helper' import { FileExplorer } from './components/file-explorer' // eslint-disable-line @@ -13,9 +13,11 @@ export function Workspace () { const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' const [currentWorkspace, setCurrentWorkspace] = useState(NO_WORKSPACE) - const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean}>(null) + const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }>(null) const [showDropdown, setShowDropdown] = useState(false) const [showIconsMenu, hideIconsMenu] = useState(false) + const [showBranches, setShowBranches] = useState(false) + const [branchFilter, setBranchFilter] = useState('') const displayOzCustomRef = useRef() const mintableCheckboxRef = useRef() const burnableCheckboxRef = useRef() @@ -28,6 +30,8 @@ export function Workspace () { const workspaceCreateTemplateInput = useRef() const cloneUrlRef = useRef() const initGitRepoRef = useRef() + const filteredBranches = selectedWorkspace ? (selectedWorkspace.branches || []).filter(branch => branch.name.includes(branchFilter) && branch.name !== 'HEAD').slice(0, 20) : [] + const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null useEffect(() => { let workspaceName = localStorage.getItem('currentWorkspace') @@ -198,6 +202,41 @@ export function Workspace () { // @ts-ignore workspaceCreateInput.current.value = `${workspaceCreateTemplateInput.current.value + '_upgradeable'}_${Date.now()}` } + + const toggleBranches = (isOpen: boolean) => { + setShowBranches(isOpen) + } + + const handleBranchFilterChange = (e: ChangeEvent) => { + const branchFilter = e.target.value + + setBranchFilter(branchFilter) + } + + const showAllBranches = () => { + global.dispatchShowAllBranches() + } + + const switchToBranch = async (branch: { remote: string, name: string }) => { + try { + if (branch.remote) { + await global.dispatchCheckoutRemoteBranch(branch.name, branch.remote) + } else { + await global.dispatchSwitchToBranch(branch.name) + } + } catch (e) { + console.error(e) + global.modal('Checkout Git Branch', e.message, 'OK', () => {}) + } + } + + const switchToNewBranch = async () => { + try { + await global.dispatchCreateNewBranch(branchFilter) + } catch (e) { + global.modal('Checkout Git Branch', e.message, 'OK', () => {}) + } + } const createModalMessage = () => { return ( @@ -495,188 +534,245 @@ export function Workspace () { ] return ( -
-
-
-
-
-
- - - - - - Create - - } - > -
- -
-
{ toggleDropdown(false) }}> -
- { (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
} - { !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && - (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && -
- { toggleDropdown(false) }}> +
+ { (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
} + { !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && + (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && +
+ +
+ } + { global.fs.localhost.isRequestingLocalhost &&
} + { (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) && +
+ +
+ }
- } - { global.fs.localhost.isRequestingLocalhost &&
} - { (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) && -
- -
- }
-
+
+ { + selectedWorkspace && +
+
+
GIT
+
+ + + { global.fs.browser.isRequestingCloning ? : currentBranch || '-none-' } + + + +
+ Switch branches +
{ toggleBranches(false) }}> +
+
+
+ +
+
+ { + filteredBranches.length > 0 ? filteredBranches.map((branch, index) => { + return ( + { switchToBranch(branch) }} title={branch.remote ? 'Checkout new branch from remote branch' : 'Checkout to local branch'}> + { + (currentBranch === branch.name) && !branch.remote ? + { branch.name } : + { branch.remote ? `${branch.remote}/${branch.name}` : branch.name } + } + + ) + }) : + +
+ Create branch: { branchFilter } from '{currentBranch}' +
+
+ } +
+ { + (selectedWorkspace.branches || []).length > 4 && + } +
+
+
+
+
+ } ) } diff --git a/libs/remix-ui/workspace/src/lib/utils/gitStatusFilter.ts b/libs/remix-ui/workspace/src/lib/utils/gitStatusFilter.ts new file mode 100644 index 0000000000..1b95e12538 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/utils/gitStatusFilter.ts @@ -0,0 +1,8 @@ +const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3 + +export const getUncommittedFiles = (statusMatrix: Array>) => { + statusMatrix = statusMatrix.filter(row => (row[HEAD] !== row[WORKDIR]) || (row[HEAD] !== row[STAGE])) + const uncommitedFiles = statusMatrix.map(row => row[FILE]) + + return uncommitedFiles +} \ No newline at end of file