diff --git a/apps/remix-ide-e2e/src/tests/file_decorator.test.ts b/apps/remix-ide-e2e/src/tests/file_decorator.test.ts new file mode 100644 index 0000000000..3483853cad --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/file_decorator.test.ts @@ -0,0 +1,125 @@ + +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +module.exports = { + + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + + 'Test decorators with script': function (browser: NightwatchBrowser) { + browser + .openFile('contracts') + .openFile('contracts/2_Owner.sol') + .openFile('contracts/1_Storage.sol') + .openFile('contracts/3_Ballot.sol') + .addFile('scripts/decorators.ts', { content: testScriptSet }) + .pause(2000) + .executeScriptInTerminal('remix.exeCurrent()') + .pause(4000) + .useXpath() + .waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', '2') + .waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', '2') + .waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-custom-contracts/2_Owner.sol"]', 'U') + .waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-custom-contracts/2_Owner.sol"]', 'U') + .waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-warning-contracts/1_Storage.sol"]', '2') + .waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-warning-contracts/1_Storage.sol"]', '2') + .waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') + .waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') + .moveToElement('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', 0, 0) + .waitForElementVisible('//*[@id="error-tooltip-contracts/2_Owner.sol"]') + .waitForElementContainsText('//*[@id="error-tooltip-contracts/2_Owner.sol"]', 'error on owner') + }, + + 'clear ballot decorator': function (browser: NightwatchBrowser) { + browser + .useCss() + .addFile('scripts/clearballot.ts', { content: testScriptClearBallot }) + .pause(2000) + .executeScriptInTerminal('remix.exeCurrent()') + .pause(4000) + .waitForElementNotPresent('[data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 10000) + }, + 'clear all decorators': function (browser: NightwatchBrowser) { + browser + .addFile('scripts/clearall.ts', { content: testScriptClear }) + .pause(2000) + .executeScriptInTerminal('remix.exeCurrent()') + .pause(4000) + .waitForElementNotPresent('[data-id="file-decoration-error-contracts/2_Owner.sol"]', 10000) + .waitForElementNotPresent('[data-id="file-decoration-warning-contracts/1_Storage.sol"]', 10000) + } + + +} +const testScriptSet = ` +(async () => { + remix.call('fileDecorator' as any, 'clearFileDecorators') + let decorator: any = { + path: 'contracts/2_Owner.sol', + isDirectory: false, + fileStateType: 'ERROR', + fileStateLabelClass: 'text-danger', + fileStateIconClass: '', + fileStateIcon: '', + text: '2', + bubble: true, + comment: 'error on owner', + } + let decorator2: any = { + path: 'contracts/2_Owner.sol', + isDirectory: false, + fileStateType: 'CUSTOM', + fileStateLabelClass: 'text-success', + fileStateIconClass: 'text-success', + fileStateIcon: 'U', + text: '', + bubble: true, + comment: 'modified', + } + await remix.call('fileDecorator' as any, 'setFileDecorators', [decorator, decorator2]) + + decorator = { + path: 'contracts/1_Storage.sol', + isDirectory: false, + fileStateType: 'WARNING', + fileStateLabelClass: 'text-warning', + fileStateIconClass: '', + fileStateIcon: '', + text: '2', + bubble: true, + comment: 'warning on storage', + } + await remix.call('fileDecorator' as any, 'setFileDecorators', decorator) + + decorator = { + path: 'contracts/3_Ballot.sol', + isDirectory: false, + fileStateType: 'CUSTOM', + fileStateLabelClass: '', + fileStateIconClass: '', + fileStateIcon: 'customtext', + text: 'with text', + bubble: true, + comment: 'custom comment', + } + await remix.call('fileDecorator' as any, 'setFileDecorators', decorator) + + })()` + + +const testScriptClearBallot = ` + (async () => { + + await remix.call('fileDecorator' as any, 'clearFileDecorators', 'contracts/3_Ballot.sol') + + })()` + +const testScriptClear = ` + (async () => { + await remix.call('fileDecorator' as any, 'clearAllFileDecorators') + + + })()` \ No newline at end of file diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 9fecce5b50..de6d2ba470 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -31,6 +31,7 @@ import { FoundryProvider } from './app/tabs/foundry-provider' import { ExternalHttpProvider } from './app/tabs/external-http-provider' import { Injected0ptimismProvider } from './app/tabs/injected-optimism-provider' import { InjectedArbitrumOneProvider } from './app/tabs/injected-arbitrum-one-provider' +import { FileDecorator } from './app/plugins/file-decorator' const isElectron = require('is-electron') @@ -156,6 +157,9 @@ class AppComponent { // ----------------- Storage plugin --------------------------------- const storagePlugin = new StoragePlugin() + // ------- FILE DECORATOR PLUGIN ------------------ + const fileDecorator = new FileDecorator() + //----- search const search = new SearchPlugin() @@ -239,6 +243,7 @@ class AppComponent { fetchAndCompile, dGitProvider, storagePlugin, + fileDecorator, hardhatProvider, ganacheProvider, foundryProvider, @@ -363,7 +368,7 @@ class AppComponent { await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately await this.appManager.activatePlugin(['home']) await this.appManager.activatePlugin(['settings', 'config']) - await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) + await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'fileDecorator', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['walkthrough','storage', 'search','compileAndRun', 'recorder']) diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index 82758c169f..3821788e18 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -169,6 +169,10 @@ export class TabProxy extends Plugin { this.on('manager', 'pluginDeactivated', (profile) => { this.removeTab(profile.name) }) + + this.on('fileDecorator', 'fileDecoratorsChanged', async (items) => { + this.tabsApi.setFileDecorations(items) + }) try { this.themeQuality = (await this.call('theme', 'currentTheme') ).quality @@ -306,6 +310,7 @@ export class TabProxy extends Plugin { updateComponent(state) { return { this.tabsApi = api } this.dispatch({ + plugin: this, loadedTabs: this.loadedTabs, onSelect, onClose, diff --git a/apps/remix-ide/src/app/plugins/file-decorator.ts b/apps/remix-ide/src/app/plugins/file-decorator.ts new file mode 100644 index 0000000000..c81c1894ff --- /dev/null +++ b/apps/remix-ide/src/app/plugins/file-decorator.ts @@ -0,0 +1,86 @@ +'use strict' + +import { default as deepequal } from 'deep-equal' // eslint-disable-line + +import { Plugin } from '@remixproject/engine' +import { fileDecoration } from '@remix-ui/file-decorators' + +const profile = { + name: 'fileDecorator', + desciption: 'Keeps decorators of the files', + methods: ['setFileDecorators', 'clearFileDecorators', 'clearAllFileDecorators'], + events: ['fileDecoratorsChanged'], + version: '0.0.1' +} + +export class FileDecorator extends Plugin { + private _fileStates: fileDecoration[] = [] + constructor() { + super(profile) + } + + onActivation(): void { + this.on('filePanel', 'setWorkspace', async () => { + await this.clearAllFileDecorators() + }) + } + + /** + * + * @param fileStates Array of file states + */ + async setFileDecorators(fileStates: fileDecoration[] | fileDecoration) { + const { from } = this.currentRequest + const workspace = await this.call('filePanel', 'getCurrentWorkspace') + const fileStatesPayload = Array.isArray(fileStates) ? fileStates : [fileStates] + // clear all file states in the previous state of this owner on the files called + fileStatesPayload.forEach((state) => { + state.workspace = workspace + state.owner = from + }) + const filteredState = this._fileStates.filter((state) => { + const index = fileStatesPayload.findIndex((payloadFileState: fileDecoration) => { + return from == state.owner && payloadFileState.path == state.path + }) + return index == -1 + }) + const newState = [...filteredState, ...fileStatesPayload].sort(sortByPath) + + if (!deepequal(newState, this._fileStates)) { + this._fileStates = newState + this.emit('fileDecoratorsChanged', this._fileStates) + } + } + + async clearFileDecorators(path?: string) { + const { from } = this.currentRequest + if (!from) return + + const filteredState = this._fileStates.filter((state) => { + if(state.owner != from) return true + if(path && state.path != path) return true + }) + const newState = [...filteredState].sort(sortByPath) + + if (!deepequal(newState, this._fileStates)) { + this._fileStates = newState + this.emit('fileDecoratorsChanged', this._fileStates) + } + + } + + async clearAllFileDecorators() { + this._fileStates = [] + this.emit('fileDecoratorsChanged', []) + } +} + +const sortByPath = (a: fileDecoration, b: fileDecoration) => { + if (a.path < b.path) { + return -1; + } + if (a.path > b.path) { + return 1; + } + return 0; +} \ No newline at end of file diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 1bebbc7083..fcf96659ec 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -11,7 +11,7 @@ const requiredModules = [ // services + layout views + system views 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries', 'openzeppelin-proxy', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected-optimism-provider', 'injected-arbitrum-one-provider', - 'compileAndRun', 'search', 'recorder'] + 'compileAndRun', 'search', 'recorder', 'fileDecorator'] // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) const dependentModules = ['hardhat', 'truffle', 'slither'] diff --git a/libs/remix-ui/file-decorators/.babelrc b/libs/remix-ui/file-decorators/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/file-decorators/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/file-decorators/.eslintrc b/libs/remix-ui/file-decorators/.eslintrc new file mode 100644 index 0000000000..0d43d424e3 --- /dev/null +++ b/libs/remix-ui/file-decorators/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc.json", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/file-decorators/src/index.ts b/libs/remix-ui/file-decorators/src/index.ts new file mode 100644 index 0000000000..a116c66cd3 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/index.ts @@ -0,0 +1,3 @@ +export { fileDecoration, fileDecorationType, FileType } from './lib/types/index' +export { FileDecorationIcons } from './lib/components/file-decoration-icon' + diff --git a/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx b/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx new file mode 100644 index 0000000000..f05cea9f65 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx @@ -0,0 +1,51 @@ +// eslint-disable-next-line no-use-before-define +import React, { useEffect, useState } from 'react' + +import { fileDecoration, fileDecorationType, FileType } from '../types' +import FileDecorationCustomIcon from './filedecorationicons/file-decoration-custom-icon' +import FileDecorationErrorIcon from './filedecorationicons/file-decoration-error-icon' +import FileDecorationTooltip from './filedecorationicons/file-decoration-tooltip' +import FileDecorationWarningIcon from './filedecorationicons/file-decoration-warning-icon' + +export type fileDecorationProps = { + file: FileType, + fileDecorations: fileDecoration[] +} + +export const FileDecorationIcons = (props: fileDecorationProps) => { + const [states, setStates] = useState([]) + useEffect(() => { + //console.log(props.file) + //console.log(props.fileState) + setStates(props.fileDecorations.filter((fileDecoration) => fileDecoration.path === props.file.path || `${fileDecoration.workspace.name}/${fileDecoration.path}` === props.file.path)) + }, [props.fileDecorations]) + + + const getTags = function () { + if (states && states.length) { + const elements: JSX.Element[] = [] + + for (const [index, state] of states.entries()) { + switch (state.fileStateType) { + case fileDecorationType.Error: + elements.push(}/>) + break + case fileDecorationType.Warning: + elements.push(}/>) + break + case fileDecorationType.Custom: + elements.push(}/>) + break + } + } + + return elements + } + } + + return <> + {getTags()} + +} + +export default FileDecorationIcons \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-custom-icon.tsx b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-custom-icon.tsx new file mode 100644 index 0000000000..2cfec2ac97 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-custom-icon.tsx @@ -0,0 +1,13 @@ +// eslint-disable-next-line no-use-before-define +import React from 'react' +import { fileDecoration } from '../../types' + +const FileDecorationCustomIcon = (props: { + fileDecoration: fileDecoration +}) => { + return <> + {props.fileDecoration.fileStateIcon} + +} + +export default FileDecorationCustomIcon \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-error-icon.tsx b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-error-icon.tsx new file mode 100644 index 0000000000..5a9c48b555 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-error-icon.tsx @@ -0,0 +1,14 @@ +// eslint-disable-next-line no-use-before-define +import React from 'react' + +import { fileDecoration } from '../../types' + +const FileDecorationErrorIcon = (props: { + fileDecoration: fileDecoration +}) => { + return <> + {props.fileDecoration.text} + +} + +export default FileDecorationErrorIcon \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx new file mode 100644 index 0000000000..1d87846192 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { fileDecoration } from "../../types"; + +const FileDecorationTooltip = (props: { + fileDecoration: fileDecoration, + icon: JSX.Element + index: number +}, +) => { + const getComments = function (fileDecoration: fileDecoration) { + if (fileDecoration.comment) { + const comments = Array.isArray(fileDecoration.comment) ? fileDecoration.comment : [fileDecoration.comment] + return comments.map((comment, index) => { + return
{comment}

+ }) + } + } + + return + <>{getComments(props.fileDecoration)} + + } + >
{props.icon}
+ +} + + +export default FileDecorationTooltip; \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-warning-icon.tsx b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-warning-icon.tsx new file mode 100644 index 0000000000..9bfd368506 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-warning-icon.tsx @@ -0,0 +1,11 @@ +// eslint-disable-next-line no-use-before-define +import React from 'react' +import { fileDecoration } from '../../types' + +const FileDecorationWarningIcon = (props: { + fileDecoration: fileDecoration +}) => { + return <>{props.fileDecoration.text} +} + +export default FileDecorationWarningIcon \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/helper/index.tsx b/libs/remix-ui/file-decorators/src/lib/helper/index.tsx new file mode 100644 index 0000000000..10bad7d3fc --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/helper/index.tsx @@ -0,0 +1,11 @@ +import React from "react" +import { fileDecoration } from "../types" + +export const getComments = function (fileDecoration: fileDecoration) { + if(fileDecoration.comment){ + const comments = Array.isArray(fileDecoration.comment) ? fileDecoration.comment : [fileDecoration.comment] + return comments.map((comment, index) => { + return
{comment}

+ }) + } +} \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/types/index.ts b/libs/remix-ui/file-decorators/src/lib/types/index.ts new file mode 100644 index 0000000000..fa783ec6e5 --- /dev/null +++ b/libs/remix-ui/file-decorators/src/lib/types/index.ts @@ -0,0 +1,29 @@ +export enum fileDecorationType { + Error = 'ERROR', + Warning = 'WARNING', + Custom = 'CUSTOM', + None = 'NONE' + } + + export type fileDecoration = { + path: string, + isDirectory: boolean, + fileStateType: fileDecorationType, + fileStateLabelClass: string, + fileStateIconClass: string, + fileStateIcon: string | HTMLDivElement | JSX.Element, + bubble: boolean, + text?: string, + owner?: string, + workspace?: any + tooltip?: string + comment?: string[] | string + } + + export interface FileType { + path: string, + name?: string, + isDirectory?: boolean, + type?: 'folder' | 'file' | 'gist', + child?: File[] + } \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/tsconfig.json b/libs/remix-ui/file-decorators/tsconfig.json new file mode 100644 index 0000000000..d52e31ad74 --- /dev/null +++ b/libs/remix-ui/file-decorators/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/file-decorators/tsconfig.lib.json b/libs/remix-ui/file-decorators/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/file-decorators/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 499d0d1eb1..67c7224c96 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -1,25 +1,63 @@ + +import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators' +import { Plugin } from '@remixproject/engine' + import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' import './remix-ui-tabs.css' /* eslint-disable-next-line */ export interface TabsUIProps { - tabs: Array - onSelect: (index: number) => void - onClose: (index: number) => void - onZoomOut: () => void - onZoomIn: () => void - onReady: (api: any) => void - themeQuality: string + tabs: Array + plugin: Plugin, + onSelect: (index: number) => void + onClose: (index: number) => void + onZoomOut: () => void + onZoomIn: () => void + onReady: (api: any) => void + themeQuality: string } export interface TabsUIApi { - activateTab: (namee: string) => void - active: () => string + activateTab: (namee: string) => void + active: () => string +} + +interface ITabsState { + selectedIndex: number, + fileDecorations: fileDecoration[], +} + +interface ITabsAction { + type: string, + payload: any, +} + + +const initialTabsState: ITabsState = { + selectedIndex: -1, + fileDecorations: [], +} + +const tabsReducer = (state: ITabsState, action: ITabsAction) => { + switch (action.type) { + case 'SELECT_INDEX': + return { + ...state, + selectedIndex: action.payload, + } + case 'SET_FILE_DECORATIONS': + return { + ...state, + fileDecorations: action.payload as fileDecoration[], + } + default: + return state + } } export const TabsUI = (props: TabsUIProps) => { - const [selectedIndex, setSelectedIndex] = useState(-1) + const [tabsState, dispatch] = useReducer(tabsReducer, initialTabsState); const currentIndexRef = useRef(-1) const tabsRef = useRef({}) const tabsElement = useRef(null) @@ -28,10 +66,24 @@ export const TabsUI = (props: TabsUIProps) => { tabs.current = props.tabs // we do this to pass the tabs list to the onReady callbacks useEffect(() => { - if (props.tabs[selectedIndex]) { - tabsRef.current[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }) + if (props.tabs[tabsState.selectedIndex]) { + tabsRef.current[tabsState.selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }) } - }, [selectedIndex]) + }, [tabsState.selectedIndex]) + + + + const getFileDecorationClasses = (tab: any) => { + const fileDecoration = tabsState.fileDecorations.find((fileDecoration: fileDecoration) => { + if(`${fileDecoration.workspace.name}/${fileDecoration.path}` === tab.name) return true + }) + return fileDecoration && fileDecoration.fileStateLabelClass + } + + const getFileDecorationIcons = (tab: any) => { + return + } + const renderTab = (tab, index) => { const classNameImg = 'my-1 mr-1 text-dark ' + tab.iconClass @@ -40,8 +92,9 @@ export const TabsUI = (props: TabsUIProps) => { return (
{ tabsRef.current[index] = el }} className={classNameTab} data-id={index === currentIndexRef.current ? 'tab-active' : ''} title={tab.tooltip}> - {tab.icon ? () : ()} - {tab.title} + {tab.icon ? () : ()} + {tab.title} + {getFileDecorationIcons(tab)} { props.onClose(index); event.stopPropagation() }}> @@ -56,7 +109,11 @@ export const TabsUI = (props: TabsUIProps) => { const activateTab = (name: string) => { const index = tabs.current.findIndex((tab) => tab.name === name) currentIndexRef.current = index - setSelectedIndex(index) + dispatch({ type: 'SELECT_INDEX', payload: index }) + } + + const setFileDecorations = (fileStates: fileDecoration[]) => { + dispatch({ type: 'SET_FILE_DECORATIONS', payload: fileStates }) } const transformScroll = (event) => { @@ -71,21 +128,23 @@ export const TabsUI = (props: TabsUIProps) => { useEffect(() => { props.onReady({ activateTab, - active + active, + setFileDecorations }) + return () => { tabsElement.current.removeEventListener('wheel', transformScroll) } }, []) return (
-
+
props.onZoomOut()}> props.onZoomIn()}>
{ if (tabsElement.current) return tabsElement.current = domEl @@ -94,7 +153,7 @@ export const TabsUI = (props: TabsUIProps) => { onSelect={(index) => { props.onSelect(index) currentIndexRef.current = index - setSelectedIndex(index) + dispatch({ type: 'SELECT_INDEX', payload: index }) }} > diff --git a/libs/remix-ui/workspace/src/index.ts b/libs/remix-ui/workspace/src/index.ts index 166467d115..c9e5b5355b 100644 --- a/libs/remix-ui/workspace/src/index.ts +++ b/libs/remix-ui/workspace/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/providers/FileSystemProvider' export * from './lib/contexts' +export { FileType } from './lib/types/index' \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/actions/events.ts b/libs/remix-ui/workspace/src/lib/actions/events.ts index a8bde65fd5..62092df30f 100644 --- a/libs/remix-ui/workspace/src/lib/actions/events.ts +++ b/libs/remix-ui/workspace/src/lib/actions/events.ts @@ -1,7 +1,8 @@ +import { fileDecoration } from '@remix-ui/file-decorators' import { extractParentFromKey } from '@remix-ui/helper' import React from 'react' import { action, WorkspaceTemplate } from '../types' -import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, removeFocus, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode } from './payload' +import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, removeFocus, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode, setFileDecorationSuccess } from './payload' import { addInputField, createWorkspace, deleteWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace' const LOCALHOST = ' - connect to localhost - ' @@ -38,6 +39,10 @@ export const listenOnPluginEvents = (filePanelPlugin) => { uploadFile(target, dir, cb) }) + plugin.on('fileDecorator', 'fileDecoratorsChanged', async (items: fileDecoration[]) => { + setFileDecorators(items) + }) + plugin.on('remixd', 'rootFolderChanged', async (path: string) => { rootFolderChanged(path) }) @@ -202,3 +207,8 @@ const fileRenamed = async (oldPath: string) => { const rootFolderChanged = async (path) => { await dispatch(rootFolderChangedSuccess(path)) } + +const setFileDecorators = async (items: fileDecoration[], cb?: (err: Error, result?: string | number | boolean | Record) => void) => { + await dispatch(setFileDecorationSuccess(items)) + cb && cb(null, true) +} diff --git a/libs/remix-ui/workspace/src/lib/actions/payload.ts b/libs/remix-ui/workspace/src/lib/actions/payload.ts index b663f60cfe..8c3fb8fc18 100644 --- a/libs/remix-ui/workspace/src/lib/actions/payload.ts +++ b/libs/remix-ui/workspace/src/lib/actions/payload.ts @@ -1,3 +1,4 @@ +import { fileDecoration } from '@remix-ui/file-decorators' import { action } from '../types' export const setCurrentWorkspace = (workspace: { name: string; isGitRepo: boolean; }) => { @@ -240,6 +241,12 @@ export const fsInitializationCompleted = () => { } } +export const setFileDecorationSuccess = (items: fileDecoration[]) => { + return { + type: 'SET_FILE_DECORATION_SUCCESS', + payload: items + } +} export const cloneRepositoryRequest = () => { return { type: 'CLONE_REPOSITORY_REQUEST' diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx index a0351ea3a1..3ccefdb2db 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -12,7 +12,7 @@ import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } import { FileRender } from './file-render' export const FileExplorer = (props: FileExplorerProps) => { - const { name, contextMenuItems, removedContextMenuItems, files } = props + const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props const [state, setState] = useState({ ctrlKey: false, newFileName: '', @@ -432,6 +432,7 @@ export const FileExplorer = (props: FileExplorerProps) => { { files[props.name] && Object.keys(files[props.name]).map((key, index) => void } export const FileLabel = (props: FileLabelProps) => { - const { file, focusEdit, editModeOff } = props + const { file, focusEdit, editModeOff, fileDecorations } = props const [isEditable, setIsEditable] = useState(false) + const [fileStateClasses, setFileStateClasses] = useState('') const labelRef = useRef(null) useEffect(() => { @@ -24,6 +27,18 @@ export const FileLabel = (props: FileLabelProps) => { } }, [file.path, focusEdit]) + useEffect(() => { + const state = props.fileDecorations.find((state: fileDecoration) => { + if(state.path === props.file.path) return true + if(state.bubble && props.file.isDirectory && state.path.startsWith(props.file.path)) return true + }) + if (state && state.fileStateLabelClass) { + setFileStateClasses(state.fileStateLabelClass) + } else{ + setFileStateClasses('') + } + }, [fileDecorations]) + useEffect(() => { if (labelRef.current) { setTimeout(() => { @@ -57,10 +72,10 @@ export const FileLabel = (props: FileLabelProps) => { > - { file.name } + {file.name}
) diff --git a/libs/remix-ui/workspace/src/lib/components/file-render.tsx b/libs/remix-ui/workspace/src/lib/components/file-render.tsx index ac0054e96b..164689899b 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-render.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-render.tsx @@ -6,6 +6,11 @@ import { TreeView, TreeViewItem } from '@remix-ui/tree-view' import { getPathIcon } from '@remix-ui/helper' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileLabel } from './file-label' +import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators' + + + + export interface RenderFileProps { file: FileType, @@ -19,6 +24,7 @@ export interface RenderFileProps { handleClickFolder: (path: string, type: string) => void, handleClickFile: (path: string, type: string) => void, handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void + fileDecorations: fileDecoration[] } export const FileRender = (props: RenderFileProps) => { @@ -76,7 +82,7 @@ export const FileRender = (props: RenderFileProps) => { iconX='pr-3 fa fa-folder' iconY='pr-3 fa fa-folder-open' key={`${file.path + props.index}`} - label={} + label={} onClick={handleFolderClick} onContextMenu={handleContextMenu} labelClass={labelClass} @@ -89,6 +95,7 @@ export const FileRender = (props: RenderFileProps) => { file.child ? { Object.keys(file.child).map((key, index) => { } + label={ + <> +
+ + +
+ + } onClick={handleFileClick} onContextMenu={handleContextMenu} icon={icon} diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index 39f56a202f..0a774e6b03 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -1,9 +1,10 @@ import { extractNameFromKey } from '@remix-ui/helper' import { action, FileType } from '../types' import * as _ from 'lodash' +import { fileDecoration } from '@remix-ui/file-decorators' interface Action { - type: string - payload: any + type: string + payload: any } export interface BrowserState { browser: { @@ -25,7 +26,8 @@ export interface BrowserState { registeredMenuItems: action[], removedMenuItems: action[], error: string - } + }, + fileState: fileDecoration[] }, localhost: { sharedFolder: string, @@ -40,7 +42,8 @@ export interface BrowserState { registeredMenuItems: action[], removedMenuItems: action[], error: string - } + }, + fileState: [] }, mode: 'browser' | 'localhost', notification: { @@ -75,7 +78,8 @@ export const browserInitialState: BrowserState = { registeredMenuItems: [], removedMenuItems: [], error: null - } + }, + fileState: [] }, localhost: { sharedFolder: '', @@ -90,14 +94,15 @@ export const browserInitialState: BrowserState = { registeredMenuItems: [], removedMenuItems: [], error: null - } + }, + fileState: [] }, mode: 'browser', notification: { title: '', message: '', - actionOk: () => {}, - actionCancel: () => {}, + actionOk: () => { }, + actionCancel: () => { }, labelOk: '', labelCancel: '' }, @@ -650,6 +655,16 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } + case 'SET_FILE_DECORATION_SUCCESS': { + return { + ...state, + browser: { + ...state.browser, + fileState: action.payload + } + } + } + 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 e25811df68..353e4ccf91 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -307,6 +307,7 @@ export function Workspace () { 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} @@ -343,6 +344,7 @@ export function Workspace () { 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} diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index fb169db5e8..93b6d9f8c9 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -1,5 +1,6 @@ import React from 'react' import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' +import { fileDecoration } from '@remix-ui/file-decorators'; export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean } export interface JSONStandardInput { @@ -73,6 +74,7 @@ export interface FileExplorerProps { contextMenuItems: MenuItems, removedContextMenuItems: MenuItems, files: { [x: string]: Record }, + fileState: fileDecoration[], expandPath: string[], focusEdit: string, focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[], diff --git a/nx.json b/nx.json index e5fa51dd58..aeaf1074ad 100644 --- a/nx.json +++ b/nx.json @@ -193,6 +193,9 @@ "remix-ui-permission-handler": { "tags": [] }, + "remix-ui-file-decorators": { + "tags": [] + }, "remix-ui-tooltip-popup": { "tags": [] }, diff --git a/tsconfig.base.json b/tsconfig.base.json index e97aaf9aff..9174b23427 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -88,6 +88,7 @@ "@remix-ui/permission-handler": [ "libs/remix-ui/permission-handler/src/index.ts" ], + "@remix-ui/file-decorators": ["libs/remix-ui/file-decorators/src/index.ts"], "@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"] } }, diff --git a/workspace.json b/workspace.json index df7e8f9d28..f14281de0d 100644 --- a/workspace.json +++ b/workspace.json @@ -1496,6 +1496,26 @@ } } }, + "remix-ui-file-decorators": { + "root": "libs/remix-ui/file-decorators", + "sourceRoot": "libs/remix-ui/file-decorators/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "tsConfig": [ + "libs/remix-ui/file-decorators/tsconfig.lib.json" + ], + "exclude": [ + "**/node_modules/**", + "!libs/remix-ui/file-decorators/**/*" + ] + } + } + } + }, "vyper": { "root": "apps/vyper", "sourceRoot": "apps/vyper/src",