Merge pull request #2879 from ethereum/git-branch

Switch and Checkout Branch
pull/2955/head
David Disu 2 years ago committed by GitHub
commit 69204ac078
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      apps/remix-ide/src/app/files/dgitProvider.js
  2. 4
      apps/remix-ide/src/app/files/fileManager.ts
  3. 5
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  4. 1
      libs/remix-ui/workspace/src/lib/actions/index.ts
  5. 16
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  6. 209
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  7. 6
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  8. 5
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  9. 26
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  10. 48
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  11. 440
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  12. 8
      libs/remix-ui/workspace/src/lib/utils/gitStatusFilter.ts

@ -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) {

@ -818,8 +818,8 @@ class FileManager extends Plugin {
}
}
async isGitRepo (directory: string): Promise<boolean> {
const path = directory + '/.git'
async isGitRepo (): Promise<boolean> {
const path = '.git'
const exists = await this.exists(path)
return exists

@ -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
}

@ -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 })

@ -126,7 +126,7 @@ export const createWorkspaceRequest = (promise: Promise<any>) => {
}
}
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
}
}

@ -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<any>
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<string, any>) => 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
}

@ -32,6 +32,10 @@ export const FileSystemContext = createContext<{
dispatchHandleRestoreBackup: () => Promise<void>
dispatchCloneRepository: (url: string) => Promise<void>,
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchMoveFolder: (src: string, dest: string) => Promise<void>
dispatchMoveFolder: (src: string, dest: string) => Promise<void>,
dispatchShowAllBranches: () => Promise<void>,
dispatchSwitchToBranch: (branch: string) => Promise<void>,
dispatchCreateNewBranch: (branch: string) => Promise<void>,
dispatchCheckoutRemoteBranch: (branch: string, remote: string) => Promise<void>
}>(null)

@ -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;
}

@ -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 (
<FileSystemContext.Provider value={value}>

@ -13,6 +13,11 @@ export interface BrowserState {
workspaces: {
name: string;
isGitRepo: boolean;
branches?: {
remote: any;
name: string;
}[],
currentBranch?: string
}[],
files: { [x: string]: Record<string, FileType> },
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()
}

@ -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<string>(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<boolean>(false)
const [showIconsMenu, hideIconsMenu] = useState<boolean>(false)
const [showBranches, setShowBranches] = useState<boolean>(false)
const [branchFilter, setBranchFilter] = useState<string>('')
const displayOzCustomRef = useRef<HTMLDivElement>()
const mintableCheckboxRef = useRef()
const burnableCheckboxRef = useRef()
@ -28,6 +30,8 @@ export function Workspace () {
const workspaceCreateTemplateInput = useRef()
const cloneUrlRef = useRef<HTMLInputElement>()
const initGitRepoRef = useRef<HTMLInputElement>()
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<HTMLInputElement>) => {
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 (
<div className='remixui_container'>
<div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mx-2 mb-2 d-flex flex-column">
<div className="d-flex justify-content-between">
<span className="d-flex align-items-end">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect">
WORKSPACES
</label>
</span>
<span className="remixui_menu remixui_topmenu d-flex justify-content-between align-items-end w-75">
<OverlayTrigger
placement="top-end"
overlay={
<Tooltip id="createWorkspaceTooltip" className="text-nowrap">
<span>Create</span>
</Tooltip>
}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='workspaceCreate'
data-id='workspaceCreate'
onClick={(e) => {
e.stopPropagation()
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
<div className='d-flex flex-column justify-content-between h-100'>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }}>
<div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mx-2 mb-2 d-flex flex-column">
<div className="d-flex justify-content-between">
<span className="d-flex align-items-end">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect">
WORKSPACES
</label>
</span>
<span className="remixui_menu remixui_topmenu d-flex justify-content-between align-items-end w-75">
<OverlayTrigger
placement="top-end"
overlay={
<Tooltip id="createWorkspaceTooltip" className="text-nowrap">
<span>Create</span>
</Tooltip>
}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='workspaceCreate'
data-id='workspaceCreate'
onClick={(e) => {
e.stopPropagation()
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
}}
style={{ fontSize: 'large' }}
className='far fa-plus remixui_menuicon d-flex align-self-end'
>
</span>
</OverlayTrigger>
<Dropdown id="workspacesMenuDropdown" data-id="workspacesMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}>
<Dropdown.Toggle
as={CustomIconsToggle}
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
style={{ fontSize: 'large' }}
className='far fa-plus remixui_menuicon d-flex align-self-end'
>
</span>
</OverlayTrigger>
<Dropdown id="workspacesMenuDropdown" data-id="workspacesMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}>
<Dropdown.Toggle
as={CustomIconsToggle}
icon={'fas fa-bars'}
></Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} data-id="wsdropdownMenu" className='custom-dropdown-items remixui_menuwidth' rootCloseEvent="click">
{
workspaceMenuIcons.map(m => {
return (
<Dropdown.Item>
{m}
</Dropdown.Item>
)
})
}
</Dropdown.Menu>
</Dropdown>
</span>
</div>
<Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}>
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1" icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}>
{ selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? 'localhost' : NO_WORKSPACE }
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items">
<Dropdown.Item
onClick={() => {
hideIconsMenu(!showIconsMenu)
createWorkspace()
}}
icon={'fas fa-bars'}
></Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} data-id="wsdropdownMenu" className='custom-dropdown-items remixui_menuwidth' rootCloseEvent="click">
>
{
workspaceMenuIcons.map(m => {
return (
<Dropdown.Item>
{m}
<span className="pl-3"> - create a new workspace - </span>
}
</Dropdown.Item>
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item>
{
global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
{ isGitRepo ?
<div className='d-flex justify-content-between'>
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
<i className='fas fa-code-branch pt-1'></i>
</div> :
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
}
</Dropdown.Item>
)
})
}
))
}
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item>
{ ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{ <span className="pl-3">NO_WORKSPACE</span> }</Dropdown.Item> }
</Dropdown.Menu>
</Dropdown>
</span>
</div>
<Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}>
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1" icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}>
{ selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? 'localhost' : NO_WORKSPACE }
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items">
<Dropdown.Item
onClick={() => {
createWorkspace()
}}
>
{
<span className="pl-3"> - create a new workspace - </span>
}
</Dropdown.Item>
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item>
{
global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
{ isGitRepo ?
<div className='d-flex justify-content-between'>
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
<i className='fas fa-code-branch pt-1'></i>
</div> :
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span>
}
</Dropdown.Item>
))
}
{ ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{ <span className="pl-3">NO_WORKSPACE</span> }</Dropdown.Item> }
</Dropdown.Menu>
</Dropdown>
</div>
</header>
</div>
</header>
</div>
<div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'>
{ (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{ !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
(global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
fileState={global.fs.browser.fileState}
expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
hideIconsMenu={hideIconsMenu}
showIconsMenu={showIconsMenu}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
<div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'>
{ (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{ !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
(global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
fileState={global.fs.browser.fileState}
expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
hideIconsMenu={hideIconsMenu}
showIconsMenu={showIconsMenu}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
/>
</div>
}
{ global.fs.localhost.isRequestingLocalhost && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) &&
<div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer
name='localhost'
menuItems={['createNewFile', 'createNewFolder']}
contextMenuItems={global.fs.localhost.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
fileState={[]}
expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
hideIconsMenu={hideIconsMenu}
showIconsMenu={showIconsMenu}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
/>
</div>
}
</div>
}
{ global.fs.localhost.isRequestingLocalhost && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) &&
<div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer
name='localhost'
menuItems={['createNewFile', 'createNewFolder']}
contextMenuItems={global.fs.localhost.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
fileState={[]}
expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
hideIconsMenu={hideIconsMenu}
showIconsMenu={showIconsMenu}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
/>
</div>
}
</div>
</div>
</div>
</div>
{
selectedWorkspace &&
<div className={`bg-light border-top ${selectedWorkspace.isGitRepo ? 'd-block' : 'd-none'}`}>
<div className='d-flex justify-space-between p-1'>
<div className="mr-auto text-uppercase text-dark pt-2 pl-2">GIT</div>
<div className="pt-1 mr-1">
<Dropdown style={{ height: 30, minWidth: 80 }} onToggle={toggleBranches} show={showBranches} drop={'up'}>
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control h-100 p-0 pl-2 pr-2 text-dark" icon={null}>
{ global.fs.browser.isRequestingCloning ? <i className="fad fa-spinner fa-spin"></i> : currentBranch || '-none-' }
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='custom-dropdown-items branches-dropdown' data-id="custom-dropdown-items">
<div className='d-flex text-dark' style={{ fontSize: 14, fontWeight: 'bold' }}>
<span className='mt-2 ml-2 mr-auto'>Switch branches</span>
<div className='pt-2 pr-2' onClick={() => { toggleBranches(false) }}><i className='fa fa-close'></i>
</div>
</div>
<div className='border-top py-2'>
<input
className='form-control border checkout-input bg-light'
placeholder='Find or create a branch.'
style={{ minWidth: 225 }}
onChange={handleBranchFilterChange}
/>
</div>
<div className='border-top' style={{ maxHeight: 120, overflowY: 'scroll' }}>
{
filteredBranches.length > 0 ? filteredBranches.map((branch, index) => {
return (
<Dropdown.Item key={index} onClick={() => { switchToBranch(branch) }} title={branch.remote ? 'Checkout new branch from remote branch' : 'Checkout to local branch'}>
{
(currentBranch === branch.name) && !branch.remote ?
<span>&#10003; <i className='far fa-code-branch'></i><span className='pl-1'>{ branch.name }</span></span> :
<span className='pl-3'><i className={`far ${ branch.remote ? 'fa-cloud' : 'fa-code-branch'}`}></i><span className='pl-1'>{ branch.remote ? `${branch.remote}/${branch.name}` : branch.name }</span></span>
}
</Dropdown.Item>
)
}) :
<Dropdown.Item onClick={switchToNewBranch}>
<div className="pl-1 pr-1">
<i className="fas fa-code-branch pr-2"></i><span>Create branch: { branchFilter } from '{currentBranch}'</span>
</div>
</Dropdown.Item>
}
</div>
{
(selectedWorkspace.branches || []).length > 4 && <div className='text-center border-top pt-2'><a href='#' style={{ fontSize: 12 }} onClick={showAllBranches}>view all branches</a></div>
}
</Dropdown.Menu>
</Dropdown>
</div>
</div>
</div>
}
</div>
)
}

@ -0,0 +1,8 @@
const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3
export const getUncommittedFiles = (statusMatrix: Array<Array<string | number>>) => {
statusMatrix = statusMatrix.filter(row => (row[HEAD] !== row[WORKDIR]) || (row[HEAD] !== row[STAGE]))
const uncommitedFiles = statusMatrix.map(row => row[FILE])
return uncommitedFiles
}
Loading…
Cancel
Save