commit
5b546a73c4
@ -1,129 +0,0 @@ |
||||
/* global localStorage */ |
||||
const yo = require('yo-yo') |
||||
const modalDialog = require('../ui/modaldialog') |
||||
|
||||
const defaultProfile = { |
||||
methods: [], |
||||
location: 'sidePanel', |
||||
type: 'iframe' |
||||
} |
||||
|
||||
module.exports = class LocalPlugin { |
||||
/** |
||||
* Open a modal to create a local plugin |
||||
* @param {Profile[]} plugins The list of the plugins in the store |
||||
* @returns {Promise<{api: any, profile: any}>} A promise with the new plugin profile |
||||
*/ |
||||
open (plugins) { |
||||
this.profile = JSON.parse(localStorage.getItem('plugins/local')) || defaultProfile |
||||
return new Promise((resolve, reject) => { |
||||
const onValidation = () => { |
||||
try { |
||||
const profile = this.create() |
||||
resolve(profile) |
||||
} catch (err) { |
||||
reject(err) |
||||
} |
||||
} |
||||
modalDialog('Local Plugin', this.form(), |
||||
{ fn: () => onValidation() }, |
||||
{ fn: () => resolve() } |
||||
) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Create the object to add to the plugin-list |
||||
*/ |
||||
create () { |
||||
const profile = { |
||||
icon: 'assets/img/localPlugin.webp', |
||||
methods: [], |
||||
location: 'sidePanel', |
||||
type: 'iframe', |
||||
...this.profile, |
||||
hash: `local-${this.profile.name}` |
||||
} |
||||
if (!profile.location) throw new Error('Plugin should have a location') |
||||
if (!profile.name) throw new Error('Plugin should have a name') |
||||
if (!profile.url) throw new Error('Plugin should have an URL') |
||||
localStorage.setItem('plugins/local', JSON.stringify(profile)) |
||||
return profile |
||||
} |
||||
|
||||
updateName ({ target }) { |
||||
this.profile.name = target.value |
||||
} |
||||
|
||||
updateUrl ({ target }) { |
||||
this.profile.url = target.value |
||||
} |
||||
|
||||
updateDisplayName ({ target }) { |
||||
this.profile.displayName = target.value |
||||
} |
||||
|
||||
updateProfile (key, e) { |
||||
this.profile[key] = e.target.value |
||||
} |
||||
|
||||
updateMethods ({ target }) { |
||||
if (target.value) { |
||||
try { |
||||
this.profile.methods = target.value.split(',') |
||||
} catch (e) {} |
||||
} |
||||
} |
||||
|
||||
/** The form to create a local plugin */ |
||||
form () { |
||||
const name = this.profile.name || '' |
||||
const url = this.profile.url || '' |
||||
const displayName = this.profile.displayName || '' |
||||
const methods = (this.profile.methods && this.profile.methods.join(',')) || '' |
||||
const radioSelection = (key, label, message) => { |
||||
return this.profile[key] === label |
||||
? yo`<div class="radio">
|
||||
<input class="form-check-input" type="radio" name="${key}" onclick="${e => this.updateProfile(key, e)}" value="${label}" id="${label}" data-id="localPluginRadioButton${label}" checked="checked" /> |
||||
<label class="form-check-label" for="${label}">${message}</label> |
||||
</div>` |
||||
: yo`<div class="radio">
|
||||
<input class="form-check-input" type="radio" name="${key}" onclick="${e => this.updateProfile(key, e)}" value="${label}" id="${label}" data-id="localPluginRadioButton${label}" /> |
||||
<label class="form-check-label" for="${label}">${message}</label> |
||||
</div>` |
||||
} |
||||
|
||||
return yo` |
||||
<form id="local-plugin-form"> |
||||
<div class="form-group"> |
||||
<label for="plugin-name">Plugin Name <small>(required)</small></label> |
||||
<input class="form-control" onchange="${e => this.updateName(e)}" value="${name}" id="plugin-name" data-id="localPluginName" placeholder="Should be camelCase"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label for="plugin-displayname">Display Name</label> |
||||
<input class="form-control" onchange="${e => this.updateDisplayName(e)}" value="${displayName}" id="plugin-displayname" data-id="localPluginDisplayName" placeholder="Name in the header"> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="plugin-methods">Api (comma separated list of methods name)</label> |
||||
<input class="form-control" onchange="${e => this.updateMethods(e)}" value="${methods}" id="plugin-methods" data-id="localPluginMethods" placeholder="Name in the header"> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="plugin-url">Url <small>(required)</small></label> |
||||
<input class="form-control" onchange="${e => this.updateUrl(e)}" value="${url}" id="plugin-url" data-id="localPluginUrl" placeholder="ex: https://localhost:8000"> |
||||
</div> |
||||
<h6>Type of connection <small>(required)</small></h6> |
||||
<div class="form-check form-group"> |
||||
${radioSelection('type', 'iframe', 'Iframe')} |
||||
${radioSelection('type', 'ws', 'Websocket')} |
||||
</div> |
||||
<h6>Location in remix <small>(required)</small></h6> |
||||
<div class="form-check form-group"> |
||||
${radioSelection('location', 'sidePanel', 'Side Panel')} |
||||
${radioSelection('location', 'mainPanel', 'Main Panel')} |
||||
${radioSelection('location', 'none', 'None')} |
||||
</div> |
||||
</form>` |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,19 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"no-unused-vars": "off", |
||||
"@typescript-eslint/no-unused-vars": "error" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
# remix-ui-plugin-manager |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test remix-ui-plugin-manager` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
||||
export * from './lib/remix-ui-plugin-manager' |
@ -0,0 +1,54 @@ |
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import React from 'react' |
||||
import '../remix-ui-plugin-manager.css' |
||||
interface PluginCardProps { |
||||
profile: any |
||||
buttonText: string |
||||
deactivatePlugin: (pluginName: string) => void |
||||
} |
||||
|
||||
function ActivePluginCard ({ |
||||
profile, |
||||
buttonText, |
||||
deactivatePlugin |
||||
}: PluginCardProps) { |
||||
return ( |
||||
<div className="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentActiveTile"> |
||||
<article className="list-group-item py-1 mb-1 plugins-list-group-item" title={profile.displayName || profile.name}> |
||||
<div className="remixui_row justify-content-between align-items-center mb-2"> |
||||
<h6 className="remixui_displayName plugin-name"> |
||||
<div> |
||||
{ profile.displayName || profile.name } |
||||
{ profile.documentation && |
||||
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer"> |
||||
<i aria-hidden="true" className="fas fa-book"/> |
||||
</a> |
||||
} |
||||
{ profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g) |
||||
? <small title="Version Alpha" className="remixui_versionWarning plugin-version">alpha</small> |
||||
: profile.version && profile.version.match(/\b(\w*beta\w*)\b/g) |
||||
? <small title="Version Beta" className="remixui_versionWarning plugin-version">beta</small> |
||||
: null |
||||
} |
||||
</div> |
||||
{<button |
||||
onClick={() => { |
||||
deactivatePlugin(profile.name) |
||||
} } |
||||
className="btn btn-secondary btn-sm" |
||||
data-id={`pluginManagerComponentDeactivateButton${profile.name}`} |
||||
> |
||||
{buttonText} |
||||
</button>} |
||||
</h6> |
||||
</div> |
||||
<div className="remixui_description d-flex text-body plugin-text mb-2"> |
||||
{profile.icon ? <img src={profile.icon} className="mr-1 mt-1 remixui_pluginIcon" alt="profile icon" /> : null} |
||||
<span className="remixui_descriptiontext">{profile.description}</span> |
||||
</div> |
||||
</article> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default ActivePluginCard |
@ -0,0 +1,36 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import React from 'react' |
||||
import { PluginManagerComponent } from '../../types' |
||||
import ActivePluginCard from './ActivePluginCard' |
||||
import ModuleHeading from './moduleHeading' |
||||
|
||||
interface ActivePluginCardContainerProps { |
||||
pluginComponent: PluginManagerComponent |
||||
setActiveProfiles: React.Dispatch<React.SetStateAction<Profile<any>[]>> |
||||
activeProfiles: Profile[] |
||||
} |
||||
function ActivePluginCardContainer ({ pluginComponent }: ActivePluginCardContainerProps) { |
||||
const deactivatePlugin = (pluginName: string) => { |
||||
pluginComponent.deactivateP(pluginName) |
||||
} |
||||
|
||||
return ( |
||||
<React.Fragment> |
||||
{(pluginComponent.activePlugins && pluginComponent.activePlugins.length) ? <ModuleHeading headingLabel="Active Modules" count={pluginComponent.activePlugins.length} /> : null} |
||||
{pluginComponent.activePlugins && pluginComponent.activePlugins.map((profile, idx) => { |
||||
return ( |
||||
<ActivePluginCard |
||||
buttonText="Deactivate" |
||||
profile={profile} |
||||
deactivatePlugin={deactivatePlugin} |
||||
key={idx} |
||||
/> |
||||
) |
||||
}) |
||||
} |
||||
</React.Fragment> |
||||
) |
||||
} |
||||
|
||||
export default ActivePluginCardContainer |
@ -0,0 +1,59 @@ |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import React from 'react' |
||||
import '../remix-ui-plugin-manager.css' |
||||
interface PluginCardProps { |
||||
profile: Profile & { |
||||
icon?: string |
||||
} |
||||
buttonText: string |
||||
activatePlugin: (plugin: string) => void |
||||
} |
||||
|
||||
function InactivePluginCard ({ |
||||
profile, |
||||
buttonText, |
||||
activatePlugin |
||||
}: PluginCardProps) { |
||||
return ( |
||||
<div className="list-group list-group-flush plugins-list-group" data-id="pluginManagerComponentActiveTile"> |
||||
<article className="list-group-item py-1 mb-1 plugins-list-group-item" title={profile.displayName || profile.name}> |
||||
<div className="remixui_row justify-content-between align-items-center mb-2"> |
||||
<h6 className="remixui_displayName plugin-name"> |
||||
<div> |
||||
{ profile.displayName || profile.name } |
||||
{ profile.documentation && |
||||
<a href={profile.documentation} className="px-1" title="link to documentation" target="_blank" rel="noreferrer"> |
||||
<i aria-hidden="true" className="fas fa-book"/> |
||||
</a> |
||||
} |
||||
{ profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g) |
||||
? <small title="Version Alpha" className="remixui_versionWarning plugin-version">alpha</small> |
||||
: profile.version && profile.version.match(/\b(\w*beta\w*)\b/g) |
||||
? <small title="Version Beta" className="remixui_versionWarning plugin-version">beta</small> |
||||
: null |
||||
} |
||||
</div> |
||||
{ |
||||
<button |
||||
onClick={() => { |
||||
activatePlugin(profile.name) |
||||
}} |
||||
className="btn btn-success btn-sm" |
||||
data-id={`pluginManagerComponentActivateButton${profile.name}`} |
||||
> |
||||
{buttonText} |
||||
</button> |
||||
} |
||||
</h6> |
||||
</div> |
||||
<div className="remixui_description d-flex text-body plugin-text mb-2"> |
||||
{ profile.icon ? <img src={profile.icon} className="mr-1 mt-1 remixui_pluginIcon" alt="profile icon"/> : null } |
||||
<span className="remixui_descriptiontext">{profile.description}</span> |
||||
</div> |
||||
</article> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default InactivePluginCard |
@ -0,0 +1,47 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import React from 'react' |
||||
import { PluginManagerComponent, PluginManagerProfile } from '../../types' |
||||
import InactivePluginCard from './InactivePluginCard' |
||||
import ModuleHeading from './moduleHeading' |
||||
|
||||
interface InactivePluginCardContainerProps { |
||||
pluginComponent: PluginManagerComponent |
||||
setInactiveProfiles: React.Dispatch<React.SetStateAction<Profile<any>[]>> |
||||
inactiveProfiles: Profile<any>[] |
||||
} |
||||
|
||||
interface LocalPluginInterface { |
||||
profile: Partial<PluginManagerProfile> |
||||
activateService: {} |
||||
requestQueue: [] |
||||
options: { queueTimeout: number } |
||||
id: number |
||||
pendingRequest: {} |
||||
listener: [] |
||||
iframe: {} |
||||
} |
||||
function InactivePluginCardContainer ({ pluginComponent }: InactivePluginCardContainerProps) { |
||||
const activatePlugin = (pluginName: string) => { |
||||
pluginComponent.activateP(pluginName) |
||||
} |
||||
|
||||
return ( |
||||
<React.Fragment> |
||||
{(pluginComponent.inactivePlugins && pluginComponent.inactivePlugins.length) ? <ModuleHeading headingLabel="Inactive Modules" count={pluginComponent.inactivePlugins.length} /> : null} |
||||
{pluginComponent.inactivePlugins && pluginComponent.inactivePlugins.map((profile, idx) => { |
||||
return ( |
||||
<InactivePluginCard |
||||
buttonText="Activate" |
||||
profile={profile} |
||||
key={idx} |
||||
activatePlugin={activatePlugin} |
||||
/> |
||||
) |
||||
}) |
||||
} |
||||
</React.Fragment> |
||||
) |
||||
} |
||||
|
||||
export default InactivePluginCardContainer |
@ -0,0 +1,218 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import React, { useEffect, useReducer, useState } from 'react' |
||||
import { ModalDialog } from '@remix-ui/modal-dialog' |
||||
import { Toaster } from '@remix-ui/toaster' |
||||
import { IframePlugin, WebsocketPlugin } from '@remixproject/engine-web' |
||||
|
||||
import { localPluginReducerActionType, localPluginToastReducer } from '../reducers/pluginManagerReducer' |
||||
import { FormStateProps, PluginManagerComponent } from '../../types' |
||||
|
||||
interface LocalPluginFormProps { |
||||
closeModal: () => void |
||||
visible: boolean |
||||
pluginManager: PluginManagerComponent |
||||
} |
||||
|
||||
const initialState: FormStateProps = { |
||||
name: '', |
||||
displayName: '', |
||||
url: '', |
||||
type: 'iframe', |
||||
hash: '', |
||||
methods: [], |
||||
location: 'sidePanel' |
||||
} |
||||
|
||||
const defaultProfile = { |
||||
methods: [], |
||||
location: 'sidePanel', |
||||
type: 'iframe', |
||||
name: '', |
||||
displayName: '', |
||||
url: '', |
||||
hash: '' |
||||
} |
||||
|
||||
function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFormProps) { |
||||
const [errorMsg, dispatchToastMsg] = useReducer(localPluginToastReducer, '') |
||||
const [name, setName] = useState<string>('') |
||||
const [displayName, setDisplayName] = useState<string>('') |
||||
const [url, setUrl] = useState<string>('') |
||||
const [type, setType] = useState<'iframe' | 'ws'>('iframe') |
||||
const [location, setLocation] = useState<'sidePanel' | 'mainPanel' | 'none'>('sidePanel') |
||||
const [methods, setMethods] = useState<string>('') |
||||
|
||||
useEffect(() => { |
||||
const storagePlugin:FormStateProps = localStorage.getItem('plugins/local') ? JSON.parse(localStorage.getItem('plugins/local')) : defaultProfile |
||||
setName(storagePlugin.name) |
||||
setUrl(storagePlugin.url) |
||||
setLocation(storagePlugin.location as 'sidePanel' | 'mainPanel' | 'none') |
||||
setMethods(storagePlugin.methods) |
||||
setType(storagePlugin.type) |
||||
setDisplayName(storagePlugin.displayName) |
||||
}, []) |
||||
|
||||
const handleModalOkClick = async () => { |
||||
try { |
||||
if (!name) throw new Error('Plugin should have a name') |
||||
if (pluginManager.appManager.getIds().includes(name)) { |
||||
throw new Error('This name has already been used') |
||||
} |
||||
if (!location) throw new Error('Plugin should have a location') |
||||
if (!url) throw new Error('Plugin should have an URL') |
||||
const newMethods = typeof methods === 'string' ? methods.split(',').filter(val => val) : [] |
||||
const targetPlugin = { |
||||
name: name, |
||||
displayName: displayName, |
||||
description: '', |
||||
documentation: '', |
||||
events: [], |
||||
hash: '', |
||||
kind: '', |
||||
methods: newMethods, |
||||
url: url, |
||||
type: type, |
||||
location: location, |
||||
icon: 'assets/img/localPlugin.webp' |
||||
} |
||||
const localPlugin = type === 'iframe' ? new IframePlugin(initialState) : new WebsocketPlugin(initialState) |
||||
localPlugin.profile.hash = `local-${name}` |
||||
targetPlugin.description = localPlugin.profile.description !== undefined ? localPlugin.profile.description : '' |
||||
targetPlugin.events = localPlugin.profile.events !== undefined ? localPlugin.profile.events : [] |
||||
targetPlugin.kind = localPlugin.profile.kind !== undefined ? localPlugin.profile.kind : '' |
||||
localPlugin.profile = { ...localPlugin.profile, ...targetPlugin } |
||||
pluginManager.activateAndRegisterLocalPlugin(localPlugin) |
||||
} catch (error) { |
||||
const action: localPluginReducerActionType = { type: 'show', payload: `${error.message}` } |
||||
dispatchToastMsg(action) |
||||
console.log(error) |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<><ModalDialog |
||||
handleHide={closeModal} |
||||
id="pluginManagerLocalPluginModalDialog" |
||||
hide={visible} |
||||
title="Local Plugin" |
||||
okLabel="OK" |
||||
okFn={ handleModalOkClick } |
||||
cancelLabel="Cancel" |
||||
cancelFn={closeModal} |
||||
> |
||||
<form id="local-plugin-form"> |
||||
<div className="form-group"> |
||||
<label htmlFor="plugin-name">Plugin Name <small>(required)</small></label> |
||||
<input |
||||
className="form-control" |
||||
onChange={e => setName(e.target.value)} |
||||
value={ name} |
||||
id="plugin-name" |
||||
data-id="localPluginName" |
||||
placeholder="Should be camelCase" /> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="plugin-displayname">Display Name</label> |
||||
<input |
||||
className="form-control" |
||||
onChange={e => setDisplayName(e.target.value)} |
||||
value={ displayName } |
||||
id="plugin-displayname" |
||||
data-id="localPluginDisplayName" |
||||
placeholder="Name in the header" /> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="plugin-methods">Api (comma separated list of methods name)</label> |
||||
<input |
||||
className="form-control" |
||||
onChange={e => setMethods(e.target.value)} |
||||
value={ methods } |
||||
id="plugin-methods" |
||||
data-id="localPluginMethods" |
||||
placeholder="Name in the header" /> |
||||
</div> |
||||
|
||||
<div className="form-group"> |
||||
<label htmlFor="plugin-url">Url <small>(required)</small></label> |
||||
<input |
||||
className="form-control" |
||||
onChange={e => setUrl(e.target.value)} |
||||
value={ url } |
||||
id="plugin-url" |
||||
data-id="localPluginUrl" |
||||
placeholder="ex: https://localhost:8000" /> |
||||
</div> |
||||
<h6>Type of connection <small>(required)</small></h6> |
||||
<div className="form-check form-group"> |
||||
<div className="radio"> |
||||
<input |
||||
className="form-check-input" |
||||
type="radio" |
||||
name="type" |
||||
value="iframe" |
||||
id="iframe" |
||||
data-id='localPluginRadioButtoniframe' |
||||
checked={type === 'iframe'} |
||||
onChange={(e) => setType(e.target.value as 'iframe' | 'ws')} /> |
||||
<label className="form-check-label" htmlFor="iframe">Iframe</label> |
||||
</div> |
||||
<div className="radio"> |
||||
<input |
||||
className="form-check-input" |
||||
type="radio" |
||||
name="type" |
||||
value="ws" |
||||
id="ws" |
||||
data-id='localPluginRadioButtonws' |
||||
checked={type === 'ws'} |
||||
onChange={(e) => setType(e.target.value as 'iframe' | 'ws')} /> |
||||
<label className="form-check-label" htmlFor="ws">Websocket</label> |
||||
</div> |
||||
</div> |
||||
<h6>Location in remix <small>(required)</small></h6> |
||||
<div className="form-check form-group"> |
||||
<div className="radio"> |
||||
<input |
||||
className="form-check-input" |
||||
type="radio" |
||||
name="location" |
||||
value="sidePanel" |
||||
id="sidePanel" |
||||
data-id='localPluginRadioButtonsidePanel' |
||||
checked={location === 'sidePanel'} |
||||
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} /> |
||||
<label className="form-check-label" htmlFor="sidePanel">Side Panel</label> |
||||
</div> |
||||
<div className="radio"> |
||||
<input |
||||
className="form-check-input" |
||||
type="radio" |
||||
name="location" |
||||
value="mainPanel" |
||||
id="mainPanel" |
||||
data-id='localPluginRadioButtonmainPanel' |
||||
checked={location === 'mainPanel'} |
||||
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} /> |
||||
<label className="form-check-label" htmlFor="mainPanel">Main Panel</label> |
||||
</div> |
||||
<div className="radio"> |
||||
<input |
||||
className="form-check-input" |
||||
type="radio" |
||||
name="location" |
||||
value="none" |
||||
id="none" |
||||
data-id='localPluginRadioButtonnone' |
||||
checked={location === 'none'} |
||||
onChange={(e) => setLocation(e.target.value as 'sidePanel' | 'mainPanel' | 'none')} /> |
||||
<label className="form-check-label" htmlFor="none">None</label> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</ModalDialog> |
||||
{errorMsg ? <Toaster message={errorMsg} /> : null} |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default LocalPluginForm |
@ -0,0 +1,20 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import React from 'react' |
||||
|
||||
interface ModuleHeadingProps { |
||||
headingLabel: string |
||||
count: number |
||||
} |
||||
|
||||
function ModuleHeading ({ headingLabel, count }: ModuleHeadingProps) { |
||||
return ( |
||||
<nav className="plugins-list-header justify-content-between navbar navbar-expand-lg bg-light navbar-light align-items-center"> |
||||
<span className="navbar-brand plugins-list-title h6 mb-0 mr-2">{headingLabel}</span> |
||||
<span className="badge badge-primary" style={{ cursor: 'default' }} data-id="pluginManagerComponentInactiveTilesCount"> |
||||
{count} |
||||
</span> |
||||
</nav> |
||||
) |
||||
} |
||||
|
||||
export default ModuleHeading |
@ -0,0 +1,144 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import React, { Fragment, useState } from 'react' |
||||
/* eslint-disable-line */ |
||||
import { ModalDialog } from '@remix-ui/modal-dialog' |
||||
import useLocalStorage from '../custom-hooks/useLocalStorage' |
||||
import { PluginPermissions } from '../../types' |
||||
|
||||
interface PermissionSettingsProps { |
||||
pluginSettings: any |
||||
} |
||||
|
||||
function PermisssionsSettings ({ pluginSettings }: PermissionSettingsProps) { |
||||
const [modalVisibility, setModalVisibility] = useState<boolean>(true) |
||||
const [permissions, setPermissions] = useLocalStorage<PluginPermissions>('plugins/permissions', {} as PluginPermissions) |
||||
const [permissionCache, setpermissionCache] = useState<PluginPermissions>() |
||||
const closeModal = () => setModalVisibility(true) |
||||
const openModal = () => { |
||||
const currentValue = JSON.parse(window.localStorage.getItem('plugins/permissions') || '{}') |
||||
setpermissionCache(currentValue) |
||||
setPermissions(currentValue) |
||||
setModalVisibility(!modalVisibility) |
||||
} |
||||
|
||||
const cancel = () => { |
||||
setPermissions(permissionCache) |
||||
} |
||||
|
||||
const getState = (targetPlugin:string, funcName:string, pluginName :string) => { |
||||
return permissions[targetPlugin][funcName][pluginName].allow |
||||
} |
||||
|
||||
const handleCheckboxClick = (targetPlugin:string, funcName:string, pluginName :string) => { |
||||
setPermissions((permissions) => { |
||||
permissions[targetPlugin][funcName][pluginName].allow = !permissions[targetPlugin][funcName][pluginName].allow |
||||
return permissions |
||||
}) |
||||
} |
||||
|
||||
function clearFunctionPermission (targetPlugin:string, funcName:string, pluginName :string) { |
||||
setPermissions((permissions) => { |
||||
delete permissions[targetPlugin][funcName][pluginName] |
||||
if (Object.keys(permissions[targetPlugin][funcName]).length === 0) delete permissions[targetPlugin][funcName] |
||||
if (Object.keys(permissions[targetPlugin]).length === 0) delete permissions[targetPlugin] |
||||
return permissions |
||||
}) |
||||
} |
||||
|
||||
function clearTargetPermission (targetPlugin: string) { |
||||
setPermissions((permissions) => { |
||||
delete permissions[targetPlugin] |
||||
return permissions |
||||
}) |
||||
} |
||||
|
||||
function RenderPluginHeader ({ headingName }) { |
||||
return ( |
||||
<div className="pb-2 remixui_permissionKey"> |
||||
<h3>{headingName} permissions:</h3> |
||||
<i |
||||
onClick={() => { |
||||
clearTargetPermission(headingName) |
||||
}} |
||||
className="far fa-trash-alt" |
||||
data-id={`pluginManagerSettingsClearAllPermission-${headingName}`}> |
||||
|
||||
</i> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
function RenderPermissions ({ targetPlugin }) { |
||||
return <>{Object.keys(permissions[targetPlugin]).map(funcName => { |
||||
return Object.keys(permissions[targetPlugin][funcName]).map((pluginName, index) => ( |
||||
<div className="form-group remixui_permissionKey" key={pluginName}> |
||||
{ permissions && Object.keys(permissions).length > 0 |
||||
? ( |
||||
<><div className="remixui_checkbox"> |
||||
<span className="mr-2"> |
||||
<input |
||||
type="checkbox" |
||||
onChange={() => handleCheckboxClick(targetPlugin, funcName, pluginName)} |
||||
checked={getState(targetPlugin, funcName, pluginName)} |
||||
id={`permission-checkbox-${targetPlugin}-${funcName}-${pluginName}`} |
||||
aria-describedby={`module ${pluginName} asks permission for ${funcName}`} /> |
||||
<label |
||||
className="ml-4" |
||||
htmlFor={`permission-checkbox-${targetPlugin}-${funcName}-${targetPlugin}`} |
||||
data-id={`permission-label-${targetPlugin}-${funcName}-${targetPlugin}`} |
||||
> |
||||
Allow <u>{pluginName}</u> to call <u>{funcName}</u> |
||||
</label> |
||||
</span> |
||||
</div><i |
||||
onClick={() => { |
||||
clearFunctionPermission(targetPlugin, funcName, pluginName) |
||||
} } |
||||
className="fa fa-trash-alt" |
||||
data-id={`pluginManagerSettingsRemovePermission-${targetPlugin}-${funcName}-${targetPlugin}`} /></> |
||||
) : null |
||||
} |
||||
</div> |
||||
)) |
||||
})}</> |
||||
} |
||||
|
||||
return ( |
||||
<Fragment> |
||||
<ModalDialog |
||||
handleHide={closeModal} |
||||
cancelFn={cancel} |
||||
hide={modalVisibility} |
||||
title="Plugin Manager Permissions" |
||||
okLabel="OK" |
||||
cancelLabel="Cancel" |
||||
> |
||||
{permissions && Object.keys(permissions).length > 0 |
||||
? (<h4 className="text-center">Current Permission Settings</h4>) |
||||
: (<h4 className="text-center">No Permission requested yet.</h4>) |
||||
} |
||||
<form className="remixui_permissionForm" data-id="pluginManagerSettingsPermissionForm"> |
||||
<div className="p-2"> |
||||
{ |
||||
Object.keys(permissions).map(targetPlugin => ( |
||||
<div key={`container-${targetPlugin}`}> |
||||
<RenderPluginHeader key={`header-${targetPlugin}`} headingName={targetPlugin} /> |
||||
<RenderPermissions key={`permissions-${targetPlugin}`} targetPlugin={targetPlugin}/> |
||||
</div> |
||||
)) |
||||
} |
||||
</div> |
||||
</form> |
||||
</ModalDialog> |
||||
<footer className="bg-light remixui_permissions remix-bg-opacity"> |
||||
<button |
||||
onClick={openModal} |
||||
className="btn btn-primary settings-button" |
||||
data-id="pluginManagerPermissionsButton"> |
||||
Permissions |
||||
</button> |
||||
</footer> |
||||
</Fragment> |
||||
) |
||||
} |
||||
export default PermisssionsSettings |
@ -0,0 +1,66 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import React, { Fragment, ReactNode, useEffect, useState } from 'react' |
||||
import { PluginManagerComponent, PluginManagerSettings } from '../../types' |
||||
import PermisssionsSettings from './permissionsSettings' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import LocalPluginForm from './LocalPluginForm' |
||||
|
||||
interface RootViewProps { |
||||
pluginComponent: PluginManagerComponent |
||||
pluginManagerSettings: PluginManagerSettings |
||||
children: ReactNode |
||||
} |
||||
|
||||
export interface pluginDeactivated { |
||||
flag: boolean |
||||
profile: Profile |
||||
} |
||||
|
||||
export interface pluginActivated { |
||||
flag: boolean |
||||
profile: Profile |
||||
} |
||||
|
||||
function RootView ({ pluginComponent, pluginManagerSettings, children }: RootViewProps) { |
||||
const [visible, setVisible] = useState<boolean>(true) |
||||
const [filterPlugins, setFilterPlugin] = useState<string>('') |
||||
|
||||
const openModal = () => { |
||||
setVisible(false) |
||||
} |
||||
const closeModal = () => setVisible(true) |
||||
|
||||
useEffect(() => { |
||||
pluginComponent.getAndFilterPlugins(filterPlugins) |
||||
}, [filterPlugins]) |
||||
return ( |
||||
<Fragment> |
||||
<div id="pluginManager" data-id="pluginManagerComponentPluginManager"> |
||||
<header className="form-group remixui_pluginSearch plugins-header py-3 px-4 border-bottom" data-id="pluginManagerComponentPluginManagerHeader"> |
||||
<input |
||||
type="text" |
||||
onChange={(event) => { |
||||
setFilterPlugin(event.target.value.toLowerCase()) |
||||
}} |
||||
value={filterPlugins} |
||||
className="form-control" |
||||
placeholder="Search" |
||||
data-id="pluginManagerComponentSearchInput" |
||||
/> |
||||
<button onClick={openModal} className="remixui_pluginSearchButton btn bg-transparent text-dark border-0 mt-2 text-underline" data-id="pluginManagerComponentPluginSearchButton"> |
||||
Connect to a Local Plugin |
||||
</button> |
||||
</header> |
||||
{children} |
||||
<PermisssionsSettings pluginSettings={pluginManagerSettings}/> |
||||
</div> |
||||
<LocalPluginForm |
||||
closeModal={closeModal} |
||||
visible={visible} |
||||
pluginManager={pluginComponent} |
||||
/> |
||||
</Fragment> |
||||
) |
||||
} |
||||
|
||||
export default RootView |
@ -0,0 +1,78 @@ |
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react' |
||||
|
||||
type SetValue<T> = Dispatch<SetStateAction<T>> |
||||
|
||||
function useLocalStorage<T> (key: string, initialValue: T): [T, SetValue<T>] { |
||||
// Get from local storage then
|
||||
// parse stored json or return initialValue
|
||||
const readValue = (): T => { |
||||
// Prevent build error "window is undefined" but keep keep working
|
||||
if (typeof window === 'undefined') { |
||||
return initialValue |
||||
} |
||||
|
||||
try { |
||||
const item = window.localStorage.getItem(key) |
||||
return item ? (JSON.parse(item) as T) : initialValue |
||||
} catch (error) { |
||||
console.warn(`Error reading localStorage key “${key}”:`, error) |
||||
return initialValue |
||||
} |
||||
} |
||||
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = useState<T>(readValue) |
||||
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue: SetValue<T> = value => { |
||||
// Prevent build error "window is undefined" but keeps working
|
||||
if (typeof window === 'undefined') { |
||||
console.warn( |
||||
`Tried setting localStorage key “${key}” even though environment is not a client` |
||||
) |
||||
} |
||||
|
||||
try { |
||||
// Allow value to be a function so we have the same API as useState
|
||||
const newValue = value instanceof Function ? value(storedValue) : value |
||||
|
||||
// Save to local storage
|
||||
window.localStorage.setItem(key, JSON.stringify(newValue)) |
||||
|
||||
// Save state
|
||||
setStoredValue(newValue) |
||||
|
||||
// We dispatch a custom event so every useLocalStorage hook are notified
|
||||
window.dispatchEvent(new Event('local-storage')) |
||||
} catch (error) { |
||||
console.warn(`Error setting localStorage key “${key}”:`, error) |
||||
} |
||||
} |
||||
|
||||
useEffect(() => { |
||||
setStoredValue(readValue()) |
||||
}, []) |
||||
|
||||
useEffect(() => { |
||||
const handleStorageChange = () => { |
||||
setStoredValue(readValue()) |
||||
} |
||||
|
||||
// this only works for other documents, not the current one
|
||||
window.addEventListener('storage', handleStorageChange) |
||||
|
||||
// this is a custom event, triggered in writeValueToLocalStorage
|
||||
window.addEventListener('local-storage', handleStorageChange) |
||||
|
||||
return () => { |
||||
window.removeEventListener('storage', handleStorageChange) |
||||
window.removeEventListener('local-storage', handleStorageChange) |
||||
} |
||||
}, []) |
||||
|
||||
return [storedValue, setValue] |
||||
} |
||||
|
||||
export default useLocalStorage |
@ -0,0 +1,14 @@ |
||||
|
||||
export type localPluginReducerActionType = { |
||||
type: 'show' | 'close', |
||||
payload?: any |
||||
} |
||||
|
||||
export function localPluginToastReducer (currentState: string, toastAction: localPluginReducerActionType) { |
||||
switch (toastAction.type) { |
||||
case 'show': |
||||
return `Cannot create Plugin : ${toastAction.payload!}` |
||||
default: |
||||
return currentState |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
.remixui_pluginSearch { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
background-color: var(--light); |
||||
padding: 10px; |
||||
position: sticky; |
||||
top: 0; |
||||
z-index: 2; |
||||
margin-bottom: 0px; |
||||
} |
||||
.remixui_pluginSearchInput { |
||||
height: 38px; |
||||
} |
||||
.remixui_pluginSearchButton { |
||||
font-size: 13px; |
||||
} |
||||
.remixui_displayName { |
||||
width: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
} |
||||
.remixui_pluginIcon { |
||||
height: 0.7rem; |
||||
width: 0.7rem; |
||||
filter: invert(0.5); |
||||
} |
||||
.remixui_description { |
||||
font-size: 13px; |
||||
line-height: 18px; |
||||
} |
||||
.remixui_descriptiontext { |
||||
display: block; |
||||
} |
||||
.remixui_descriptiontext:first-letter { |
||||
text-transform: uppercase; |
||||
} |
||||
.remixui_row { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
.remixui_isStuck { |
||||
background-color: var(--primary); |
||||
/* color: */ |
||||
} |
||||
.remixui_versionWarning { |
||||
padding: 4px; |
||||
margin: 0 8px; |
||||
font-weight: 700; |
||||
font-size: 9px; |
||||
line-height: 12px; |
||||
text-transform: uppercase; |
||||
cursor: default; |
||||
border: 1px solid; |
||||
border-radius: 2px; |
||||
} |
||||
|
||||
.remixui_permissions { |
||||
position: sticky; |
||||
bottom: 0; |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
align-items: center; |
||||
padding: 5px 20px; |
||||
} |
||||
.remixui_permissions button { |
||||
padding: 2px 5px; |
||||
cursor: pointer; |
||||
} |
||||
.remixui_permissionForm h4 { |
||||
font-size: 1.3rem; |
||||
text-align: center; |
||||
} |
||||
.remixui_permissionForm h6 { |
||||
font-size: 1.1rem; |
||||
} |
||||
.remixui_permissionForm hr { |
||||
width: 80%; |
||||
} |
||||
.remixui_permissionKey { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
.remixui_permissionKey i { |
||||
cursor: pointer; |
||||
} |
||||
.remixui_checkbox { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.remixui_checkbox label { |
||||
margin: 0; |
||||
font-size: 1rem; |
||||
} |
@ -0,0 +1,29 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import React, { useState } from 'react' |
||||
import { RemixUiPluginManagerProps } from '../types' |
||||
import ActivePluginCardContainer from './components/ActivePluginCardContainer' |
||||
import InactivePluginCardContainer from './components/InactivePluginCardContainer' |
||||
import RootView from './components/rootView' |
||||
import './remix-ui-plugin-manager.css' |
||||
|
||||
export const RemixUiPluginManager = ({ pluginComponent, pluginManagerSettings }: RemixUiPluginManagerProps) => { |
||||
const [activeProfiles, setActiveProfiles] = useState<Profile[]>(pluginComponent.activePlugins) |
||||
const [inactiveProfiles, setinactiveProfiles] = useState<Profile[]>(pluginComponent.inactivePlugins) |
||||
return ( |
||||
<RootView pluginComponent={pluginComponent} pluginManagerSettings={pluginManagerSettings}> |
||||
<section data-id="pluginManagerComponentPluginManagerSection"> |
||||
<ActivePluginCardContainer |
||||
pluginComponent={pluginComponent} |
||||
setActiveProfiles={setActiveProfiles} |
||||
activeProfiles={activeProfiles} |
||||
/> |
||||
<InactivePluginCardContainer |
||||
pluginComponent={pluginComponent} |
||||
setInactiveProfiles={setinactiveProfiles} |
||||
inactiveProfiles={inactiveProfiles} |
||||
/> |
||||
</section> |
||||
</RootView> |
||||
) |
||||
} |
@ -0,0 +1,208 @@ |
||||
import { PermissionHandler } from './app/ui/persmission-handler' |
||||
import { PluginManager } from '@remixproject/engine/lib/manager' |
||||
import { EventEmitter } from 'events' |
||||
import { Engine } from '@remixproject/engine/lib/engine' |
||||
import { PluginBase, Profile } from '@remixproject/plugin-utils' |
||||
import { IframePlugin, ViewPlugin, WebsocketPlugin } from '@remixproject/engine-web' |
||||
/* eslint-disable camelcase */ |
||||
|
||||
interface SetPluginOptionType { |
||||
queueTimeout: number |
||||
} |
||||
|
||||
export class RemixEngine extends Engine { |
||||
event: EventEmitter; |
||||
setPluginOption ({ name, kind }) : SetPluginOptionType |
||||
onRegistration (plugin) : void |
||||
} |
||||
|
||||
export function isNative(name: any): any; |
||||
/** |
||||
* Checks if plugin caller 'from' is allowed to activate plugin 'to' |
||||
* The caller can have 'canActivate' as a optional property in the plugin profile. |
||||
* This is an array containing the 'name' property of the plugin it wants to call. |
||||
* canActivate = ['plugin1-to-call','plugin2-to-call',....] |
||||
* or the plugin is allowed by default because it is native |
||||
* |
||||
* @param {any, any} |
||||
* @returns {boolean} |
||||
*/ |
||||
export function canActivate(from: any, to: any): boolean; |
||||
export class RemixAppManager extends PluginManager { |
||||
constructor(); |
||||
event: EventEmitter; |
||||
pluginsDirectory: string; |
||||
pluginLoader: PluginLoader; |
||||
permissionHandler: PermissionHandler; |
||||
getAll(): import('@remixproject/plugin-utils').Profile<any>[]; |
||||
getIds(): string[]; |
||||
isDependent(name: any): any; |
||||
isRequired(name: any): any; |
||||
registeredPlugins(): Promise<any>; |
||||
turnPluginOn(name: string | string[]); |
||||
turnPluginOff(name: string); |
||||
} |
||||
|
||||
export class PluginManagerSettings { |
||||
openDialog(): void; |
||||
permissions: any; |
||||
currentSetting: any; |
||||
onValidation(): void; |
||||
/** Clear one permission from a plugin */ |
||||
clearPersmission(from: string, to: string, method: string): void; |
||||
/** Clear all persmissions from a plugin */ |
||||
clearAllPersmission(to: string): void; |
||||
settings(): any; |
||||
render(): any; |
||||
} |
||||
|
||||
export type PluginPermissions = { |
||||
fileManager : { |
||||
writeFile: { |
||||
pluginName: { |
||||
allow: boolean |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
export class PluginManagerComponent extends ViewPlugin extends Plugin implements PluginBase { |
||||
constructor(appManager: RemixAppManager, engine: Engine) |
||||
appManager: RemixAppManager |
||||
pluginSettings: PluginManagerSettings |
||||
app: PluginApi<any> |
||||
engine: Engine |
||||
htmlElement: HTMLDivElement |
||||
views: { root: null, items: {} } |
||||
localPlugin: LocalPlugin |
||||
pluginNames: string[] |
||||
inactivePlugins: Profile[] |
||||
activePlugins: Profile[] |
||||
filter: string |
||||
isActive(name: string): boolean |
||||
activateP(name: string): void |
||||
deactivateP(name: string): void |
||||
onActivation(): void |
||||
renderComponent(): void |
||||
openLocalPlugin(): Promise<void> |
||||
render(): HTMLDivElement |
||||
getAndFilterPlugins: (filter?: string, profiles?: Profile[]) => void |
||||
triggerEngineEventListener: () => void |
||||
activateAndRegisterLocalPlugin: (localPlugin: IframePlugin | WebsocketPlugin) => Promise<void> |
||||
activeProfiles: string[] |
||||
_paq: any |
||||
} |
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export = LocalPlugin; |
||||
declare class LocalPlugin { |
||||
/** |
||||
* Open a modal to create a local plugin |
||||
* @param {Profile[]} plugins The list of the plugins in the store |
||||
* @returns {Promise<{api: any, profile: any}>} A promise with the new plugin profile |
||||
*/ |
||||
open(plugins: any[]): Promise<{ |
||||
api: any; |
||||
profile: any; |
||||
}>; |
||||
|
||||
profile: any; |
||||
/** |
||||
* Create the object to add to the plugin-list |
||||
*/ |
||||
create(): any; |
||||
updateName({ target }: { |
||||
target: any; |
||||
}): void; |
||||
|
||||
updateUrl({ target }: { |
||||
target: any; |
||||
}): void; |
||||
|
||||
updateDisplayName({ target }: { |
||||
target: any; |
||||
}): void; |
||||
|
||||
updateProfile(key: any, e: any): void; |
||||
updateMethods({ target }: { |
||||
target: any; |
||||
}): void; |
||||
|
||||
/** The form to create a local plugin */ |
||||
form(): any; |
||||
} |
||||
|
||||
export interface PluginManagerContextProviderProps { |
||||
children: React.ReactNode |
||||
pluginComponent: PluginManagerComponent |
||||
} |
||||
|
||||
export interface RemixUiPluginManagerProps { |
||||
pluginComponent: PluginManagerComponent |
||||
pluginManagerSettings: PluginManagerSettings |
||||
} |
||||
/** @class Reference loaders. |
||||
* A loader is a get,set based object which load a workspace from a defined sources. |
||||
* (localStorage, queryParams) |
||||
**/ |
||||
declare class PluginLoader { |
||||
get currentLoader(): any; |
||||
donotAutoReload: string[]; |
||||
loaders: {}; |
||||
current: string; |
||||
set(plugin: any, actives: any): void; |
||||
get(): any; |
||||
} |
||||
|
||||
export type PluginManagerSettings = { |
||||
openDialog: () => void |
||||
onValidation: () => void |
||||
clearPermission: (from: any, to: any, method: any) => void |
||||
settings: () => HTMLElement |
||||
render: () => HTMLElement |
||||
} |
||||
|
||||
export interface DefaultLocalPlugin extends Profile { |
||||
name: string |
||||
displayName: string |
||||
url: string |
||||
type: string |
||||
hash: string |
||||
methods: any |
||||
location: string |
||||
} |
||||
|
||||
export interface FormStateProps { |
||||
name: string |
||||
displayName: string |
||||
url: string |
||||
type: 'iframe' | 'ws' |
||||
hash: string |
||||
methods: any |
||||
location: string |
||||
} |
||||
|
||||
export type PluginManagerProfile = Profile & { |
||||
name: string, |
||||
displayName: string, |
||||
methods: Array<any>, |
||||
events?: Array<any>, |
||||
icon: 'assets/img/pluginManager.webp', |
||||
description: string, |
||||
kind?: string, |
||||
location: 'sidePanel' | 'mainPanel' | 'none', |
||||
documentation: 'https://remix-ide.readthedocs.io/en/latest/plugin_manager.html', |
||||
version: any |
||||
type: 'iframe' | 'ws' |
||||
hash: string |
||||
} |
||||
export type LocalPlugin = { |
||||
create: () => Profile |
||||
updateName: (target: string) => void |
||||
updateDisplayName: (displayName: string) => void |
||||
updateProfile: (key: string, e: Event) => void |
||||
updateMethods: (target: any) => void |
||||
form: () => HTMLElement |
||||
} |
||||
|
||||
export { } |
@ -0,0 +1,17 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"jsx": "react", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"resolveJsonModule": true |
||||
}, |
||||
"files": [], |
||||
"include": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./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"] |
||||
} |
Loading…
Reference in new issue