Merge branch 'master' of gitee.com:openInula/inula
This commit is contained in:
commit
530fb24289
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "openinula",
|
||||
"name": "inula",
|
||||
"description": "OpenInula is a JavaScript framework library.",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { injectSrc, injectCode } from '../utils/injectUtils';
|
||||
import { checkMessage } from '../utils/transferUtils';
|
||||
import { DevToolContentScript, DevToolHook, DevToolBackground } from '../utils/constants';
|
||||
import { changeSource } from '../utils/transferUtils';
|
||||
|
||||
// 页面的 window 对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入 hook
|
||||
const rendererURL = chrome.runtime.getURL('/injector.js');
|
||||
if (window.performance.getEntriesByType('navigation')) {
|
||||
const entryType = (window.performance.getEntriesByType('navigation')[0] as any).type;
|
||||
if (entryType === 'navigate') {
|
||||
injectSrc(rendererURL);
|
||||
} else if (entryType === 'reload' && !(window as any).__INULA_DEV_HOOK__) {
|
||||
let rendererCode;
|
||||
const request = new XMLHttpRequest();
|
||||
request.addEventListener('load', function () {
|
||||
rendererCode = this.responseText;
|
||||
});
|
||||
request.open('GET', rendererURL, false);
|
||||
request.send();
|
||||
injectCode(rendererCode);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听来自页面的信息
|
||||
window.addEventListener(
|
||||
'message',
|
||||
event => {
|
||||
// 只监听来自本页面的消息
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (checkMessage(data, DevToolHook)) {
|
||||
changeSource(data, DevToolContentScript);
|
||||
// 传递给 background
|
||||
chrome.runtime.sendMessage(data);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// 监听来自 background 的消息
|
||||
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||
// 该方法可以监听页面 contentScript 和插件的消息
|
||||
// 没有 tab 信息说明消息来自插件
|
||||
if (!sender.tab && checkMessage(message, DevToolBackground)) {
|
||||
changeSource(message, DevToolContentScript);
|
||||
// 传递消息给页面
|
||||
window.postMessage(message, '*');
|
||||
}
|
||||
sendResponse({ status: 'ok' });
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { render, createElement } from 'openinula';
|
||||
import Panel from '../panel/Panel';
|
||||
import PanelX from '../panelX/PanelX';
|
||||
|
||||
let panelCreated = false;
|
||||
|
||||
const viewSource = () => {
|
||||
setTimeout(() => {
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$type != null) {
|
||||
if (
|
||||
window.$type &&
|
||||
window.$type.prototype &&
|
||||
window.$type.prototype.render
|
||||
) {
|
||||
// 类组件
|
||||
inspect(window.$type.prototype.render);
|
||||
} else {
|
||||
// 函数组件
|
||||
inspect(window.$type);
|
||||
}
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const inspectVNode = () => {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`
|
||||
window.__INULA_DEV_HOOK__ && window.__INULA_DEV_HOOK__.$0 !== $0
|
||||
? (inspect(window.__INULA_DEV_HOOK__.$0.realNode), true)
|
||||
: false
|
||||
`,
|
||||
(_, error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let currentPanel = null;
|
||||
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'window.__INULA_DEV_HOOK__',
|
||||
function (isInula, error) {
|
||||
if (!isInula || panelCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelCreated = true;
|
||||
chrome.devtools.panels.create(
|
||||
'Inula',
|
||||
'',
|
||||
'panel.html',
|
||||
(extensionPanel) => {
|
||||
extensionPanel.onShown.addListener((panel) => {
|
||||
if (currentPanel === panel) {
|
||||
return;
|
||||
}
|
||||
currentPanel = panel;
|
||||
const container = panel.document.getElementById('root');
|
||||
const element = createElement(Panel, { viewSource, inspectVNode });
|
||||
render(element, container);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
chrome.devtools.panels.create(
|
||||
'InulaX',
|
||||
'',
|
||||
'panelX.html',
|
||||
(extensionPanel) => {
|
||||
extensionPanel.onShown.addListener((panel) => {
|
||||
if (currentPanel === panel) {
|
||||
return;
|
||||
}
|
||||
currentPanel = panel;
|
||||
const container = panel.document.getElementById('root');
|
||||
const element = createElement(PanelX, {});
|
||||
render(element, container);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
<!--
|
||||
~ Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
~
|
||||
~ openInula is licensed under Mulan PSL v2.
|
||||
~ You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
~ You may obtain a copy of Mulan PSL v2 at:
|
||||
~
|
||||
~ http://license.coscl.org.cn/MulanPSL2
|
||||
~
|
||||
~ THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
~ EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
~ MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
~ See the Mulan PSL v2 for more details.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src *; style-src 'self' 'unsafe-inline'; srcipt-src 'self' 'unsafe-inline' 'unsafe-eval' ">
|
||||
<script src="inula.development.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<p>Inula dev tools!</p>
|
||||
</div>
|
||||
</body>
|
||||
<script> src="main.js"</script>
|
||||
</html>
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula, { useState } from 'openinula';
|
||||
import { Modal } from './Modal';
|
||||
import { highlight, sendMessage } from './utils';
|
||||
|
||||
function executeAction(storeId: string, name: string, args: any[]) {
|
||||
sendMessage({
|
||||
type: 'inulax run action',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
storeId,
|
||||
action: name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
function queryAction(storeId: string, name: string, args: any[]) {
|
||||
sendMessage({
|
||||
type: 'inulax queue action',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
storeId,
|
||||
action: name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
export function ActionRunner({ foo, storeId, actionName }) {
|
||||
const [data, setState] = useState({
|
||||
modal: false,
|
||||
gatheredAttrs: [],
|
||||
query: false,
|
||||
});
|
||||
const modalIsOpen = data.modal;
|
||||
const gatheredAttrs = data.gatheredAttrs;
|
||||
function setData(val) {
|
||||
const newData = {
|
||||
modal: data.modal,
|
||||
gatheredAttrs: data.gatheredAttrs,
|
||||
};
|
||||
|
||||
Object.entries(val).forEach(([key, value]) => (newData[key] = value));
|
||||
|
||||
setState(newData as any);
|
||||
}
|
||||
|
||||
const plainFunction = foo.replace(/\{.*}/gms, '');
|
||||
const attributes = plainFunction
|
||||
.replace(/^.*\(/g, '')
|
||||
.replace(/\).*$/, '')
|
||||
.split(/, ?/)
|
||||
.filter((item, index) => index > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
title={'Run action'}
|
||||
onClick={() => {
|
||||
if (attributes.length > 0) {
|
||||
setData({ modal: false, gatheredAttrs: [], query: false });
|
||||
} else {
|
||||
executeAction(storeId, actionName, gatheredAttrs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<b
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
☼
|
||||
<span
|
||||
title={'Add to action queue'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (attributes.len > 0) {
|
||||
setData({ modal: true, gatheredAttrs: [], query: true });
|
||||
} else {
|
||||
queryAction(storeId, actionName, gatheredAttrs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
⌛︎{' '}
|
||||
</span>
|
||||
</b>
|
||||
<span>
|
||||
<i>{plainFunction}</i>
|
||||
{' {...}'}
|
||||
</span>
|
||||
</span>
|
||||
{modalIsOpen ? (
|
||||
<Modal
|
||||
closeModal={() => {
|
||||
setData({ modal: false });
|
||||
}}
|
||||
then={data => {
|
||||
if (gatheredAttrs.length === attributes.length - 1) {
|
||||
setData({ modal: false });
|
||||
executeAction(storeId, actionName, gatheredAttrs.concat(data));
|
||||
} else {
|
||||
setData({
|
||||
gatheredAttrs: gatheredAttrs.concat([data]),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h3>{data.query ? 'Query action:' : 'Run action:'}</h3>
|
||||
<p>{highlight(plainFunction, attributes[gatheredAttrs.length])}</p>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState } from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import { Tree } from './Tree';
|
||||
import {displayValue, omit} from './utils';
|
||||
|
||||
type Mutation = {
|
||||
mutation: boolean;
|
||||
items?: Mutation[];
|
||||
attributes?: { [key: string]: Mutation };
|
||||
values?: Mutation[];
|
||||
entries?: Mutation[];
|
||||
from?: any;
|
||||
to?: any;
|
||||
};
|
||||
|
||||
export function DiffTree({
|
||||
mutation,
|
||||
indent = 0,
|
||||
index = '',
|
||||
expand = false,
|
||||
search = '',
|
||||
forcedExpand = false,
|
||||
omitAttrs = [],
|
||||
doNotDisplayIcon = false,
|
||||
forcedLabel = null,
|
||||
className,
|
||||
}: {
|
||||
mutation: Mutation;
|
||||
indent: number;
|
||||
index?: string | number;
|
||||
expand?: boolean;
|
||||
search: string;
|
||||
forcedExpand?: boolean;
|
||||
omitAttrs: string[];
|
||||
doNotDisplayIcon?: boolean;
|
||||
forcedLabel?: string | number | null;
|
||||
className?: string;
|
||||
}) {
|
||||
if (omitAttrs.length && mutation.attributes) {
|
||||
mutation.attributes = omit(mutation.attributes, ...omitAttrs);
|
||||
mutation.from = mutation.from && omit(mutation.from, ...omitAttrs);
|
||||
mutation.to = mutation.to && omit(mutation.to, ...omitAttrs);
|
||||
}
|
||||
const [expanded, setExpanded] = useState(expand);
|
||||
|
||||
const deleted = mutation.mutation && !('to' in mutation);
|
||||
const newValue = mutation.mutation && !('from' in mutation);
|
||||
const mutated = mutation.mutation;
|
||||
|
||||
const isArray = mutated && mutation.items;
|
||||
const isObject = mutated && mutation.attributes;
|
||||
const isMap = mutated && mutation.entries;
|
||||
const isSet = mutated && mutation.values;
|
||||
const isPrimitive = !isArray && !isObject && !isMap && !isSet;
|
||||
|
||||
if (!mutated) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
className={styles.added}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.from}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
className={styles.deleted}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
className={`${
|
||||
expanded
|
||||
? 'expanded'
|
||||
: `not-expanded ${
|
||||
mutated && !isPrimitive && !expanded ? styles.changed : ''
|
||||
}`
|
||||
}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
{new Array(Math.max(indent, 0)).fill(<span> </span>)}
|
||||
{isPrimitive ? (
|
||||
// 如果两个 value 是基本变量并且不同,则简单显示不同点
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tree
|
||||
data={mutation.from}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.deleted}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.added}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// 如果至少有一个是复杂变量,则需要展开按钮
|
||||
<>
|
||||
{forcedExpand ? '' : expanded ? <span>▼</span> : <span>▶</span>}
|
||||
{index === 0 || index ? (
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{isArray ? (
|
||||
// 如果都是数组进行比较
|
||||
expanded ? (
|
||||
[
|
||||
Array(Math.max(mutation.from.length, mutation.to.length))
|
||||
.fill(true)
|
||||
.map((i, index) => {
|
||||
return (
|
||||
<div>
|
||||
{mutation.items[index].mutation ? (
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.items[index],
|
||||
to: mutation.to[index],
|
||||
}}
|
||||
indent={indent}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={index}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={mutation.to[index]}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
]
|
||||
) : (
|
||||
forcedLabel || `Array(${mutation.to?.length})`
|
||||
)
|
||||
) : isSet ? (
|
||||
expanded ? (
|
||||
<div>
|
||||
<div>
|
||||
{forcedLabel || `Set(${mutation.to?.values.length})`}
|
||||
</div>
|
||||
{Array(
|
||||
Math.max(
|
||||
mutation.from?.values.length,
|
||||
mutation.to?.values.length
|
||||
)
|
||||
)
|
||||
.fill(true)
|
||||
.map((i ,index) => (
|
||||
<div>
|
||||
{mutation.values[index].mutation ? (
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.values[index],
|
||||
}}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={mutation.to?.values[index]}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
{forcedLabel || `Set(${mutation.to?.values.length})`}
|
||||
</span>
|
||||
)
|
||||
) : isMap ? (
|
||||
expanded ? (
|
||||
<>
|
||||
<span>
|
||||
{forcedLabel || `Map(${mutation.to?.entries.length})`}
|
||||
</span>
|
||||
{Array(
|
||||
Math.max(
|
||||
mutation.from?.entries.length,
|
||||
mutation.to?.entries.length
|
||||
)
|
||||
)
|
||||
.fill(true)
|
||||
.map((i, index) =>
|
||||
mutation.entries[index].mutation ? (
|
||||
<div>
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.entries[index],
|
||||
}}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={'[map item]'}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Tree
|
||||
data={mutation.to?.entries[index]}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={'[map item]'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
{forcedLabel || `Map(${mutation.to?.entries.length})`}
|
||||
</span>
|
||||
)
|
||||
) : expanded ? (
|
||||
// 如果都是 object 进行比较
|
||||
Object.entries(mutation.attributes).map(([key, item]) => {
|
||||
return item.mutation ? (
|
||||
<span onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
<DiffTree
|
||||
mutation={item}
|
||||
index={key}
|
||||
indent={indent}
|
||||
search={search}
|
||||
className={!expanded && mutated ? '' : styles.changed}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
) : (
|
||||
<span onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
<Tree
|
||||
data={mutation.to[key]}
|
||||
index={key}
|
||||
indent={indent}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
forcedLabel || '{ ... }'
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'openinula';
|
||||
import { DevToolPanel } from '../utils/constants';
|
||||
import {
|
||||
initBackgroundConnection,
|
||||
addBackgroundMessageListener,
|
||||
removeBackgroundMessageListener,
|
||||
} from '../panelConnection';
|
||||
import { Table } from './Table';
|
||||
import { Tree } from './Tree';
|
||||
import {fullTextSearch, omit} from './utils';
|
||||
import styles from './PanelX.less';
|
||||
import { Checkbox } from '../utils/Checkbox';
|
||||
import { DiffTree } from './DiffTree';
|
||||
|
||||
const eventTypes = {
|
||||
INITIALIZED: 'inulax store initialized',
|
||||
STATE_CHANGE: 'inulax state change',
|
||||
SUBSCRIBED: 'inulax subscribed',
|
||||
UNSUBSCRIBED: 'inulax unsubscribed',
|
||||
ACTION: 'inulax action',
|
||||
ACTION_QUEUED: 'inulax action queued',
|
||||
QUEUE_PENDING: 'inulax queue pending',
|
||||
QUEUE_FINISHED: 'inulax queue finished',
|
||||
};
|
||||
|
||||
const otherTypes = {
|
||||
GET_EVENTS: 'inulax getEvents',
|
||||
GET_PERSISTENCE: 'inulax getPersistence',
|
||||
EVENTS: 'inulax events',
|
||||
FLUSH_EVENTS: 'inulax flush events',
|
||||
SET_PERSISTENT: 'inulax setPersistent',
|
||||
RESET_EVENTS: 'inulax resetEvents',
|
||||
};
|
||||
|
||||
function extractDataByType(message, search) {
|
||||
if (message.type === eventTypes.ACTION) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tree
|
||||
data={{
|
||||
Action: `${message.data.action.action}${
|
||||
message.data.fromQueue ? ' (queued)' : ''
|
||||
}`
|
||||
}}
|
||||
expand={true}
|
||||
indent={-4}
|
||||
forcedExpand={true}
|
||||
search={search}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type === eventTypes.STATE_CHANGE) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<b>{`${message.data.change.vNodes.length} nodes changed:`}</b>
|
||||
<Tree
|
||||
data={message.data.change.vNodes.map(vNode => {
|
||||
return (
|
||||
<span>
|
||||
<i>{vNode.type}</i>()
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={styles.grey}>N/A</span>
|
||||
}
|
||||
|
||||
export default function EventLog({ setNextStore, setEventFilter, eventFilter }) {
|
||||
const [log, setLog] = useState([]);
|
||||
const [initlized, setInitlized] = useState(false);
|
||||
const [persistent, setPersistent] = useState(false);
|
||||
const filterField = useRef(null);
|
||||
|
||||
const addFilter = (key, value) => {
|
||||
const filters = { ...eventFilter };
|
||||
filters[key] = value;
|
||||
|
||||
setEventFilter(filters);
|
||||
};
|
||||
|
||||
const removeFilter = key => {
|
||||
const filters = { ...eventFilter };
|
||||
delete filters[key];
|
||||
|
||||
setEventFilter(filters);
|
||||
};
|
||||
|
||||
if (!initlized) {
|
||||
setTimeout(() => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_PERSISTENCE,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const lisener = message => {
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
if (message.payload.type === otherTypes.EVENTS) {
|
||||
setLog(message.payload.events);
|
||||
setInitlized(true);
|
||||
} else if (message.payload.type === otherTypes.SET_PERSISTENT) {
|
||||
setPersistent(message.payload.persistent);
|
||||
} else if (message.payload.type === otherTypes.FLUSH_EVENTS) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
initBackgroundConnection('panel');
|
||||
addBackgroundMessageListener(lisener);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(lisener);
|
||||
};
|
||||
});
|
||||
|
||||
const filters = Object.entries(eventFilter);
|
||||
const usedTypes = { all: 0 };
|
||||
|
||||
const processedData = log
|
||||
.filter(event => {
|
||||
if (!Object.values(eventTypes).includes(event.message.type)) {
|
||||
return false;
|
||||
}
|
||||
usedTypes.all++;
|
||||
if (!usedTypes[event.message.type]) {
|
||||
usedTypes[event.message.type] = 1;
|
||||
} else {
|
||||
usedTypes[event.message.type]++;
|
||||
}
|
||||
|
||||
if (!filters.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !filters.some(([key, value]) => {
|
||||
if (key === 'fulltext') {
|
||||
const result = fullTextSearch(event, value);
|
||||
return !result;
|
||||
}
|
||||
const keys = key.split('.');
|
||||
let search = event;
|
||||
keys.forEach(attr => {
|
||||
search = search[attr];
|
||||
});
|
||||
return value !== search;
|
||||
});
|
||||
})
|
||||
.map(event => {
|
||||
const date = new Date(event.timestamp);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
timestamp: event.timestamp,
|
||||
type: event.message.type,
|
||||
time: `${date.toLocaleTimeString()} - ${date.toLocaleDateString()}`,
|
||||
state: event.message.type === eventTypes.STATE_CHANGE ? (
|
||||
<DiffTree
|
||||
mutation={event.message.data.change.mutation}
|
||||
expand={true}
|
||||
forcedExpand={true}
|
||||
indent={0}
|
||||
search={eventFilter['fulltext']}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
doNotDisplayIcon={true}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={event.message.data.store.$s}
|
||||
expand={true}
|
||||
search={eventFilter['fulltext']}
|
||||
forcedExpand={true}
|
||||
indent={-4}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
/>
|
||||
),
|
||||
storeClick: (
|
||||
<span
|
||||
className={styles.link}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setNextStore(event.message.data.store.id);
|
||||
}}
|
||||
>
|
||||
{event.message.data.store.id}
|
||||
</span>
|
||||
),
|
||||
additionalData: extractDataByType(
|
||||
event.message,
|
||||
eventFilter['fulltext']
|
||||
),
|
||||
storeId: event.message.data.store.id,
|
||||
event,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginTop: '0px', margin: '5px' }}>
|
||||
<input
|
||||
ref={filterField}
|
||||
type={'text'}
|
||||
placeholder={'Filter:'}
|
||||
className={`${styles.compositeInput} ${styles.left}`}
|
||||
onKeyUp={() => {
|
||||
if (!filterField.current.value) {
|
||||
removeFilter('fulltext');
|
||||
}
|
||||
addFilter('fulltext', filterField.current.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className={`${styles.bold} ${styles.compositeInput} ${styles.right}`}
|
||||
onClick={() => {
|
||||
filterField.current.value = '';
|
||||
removeFilter('fulltext');
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
<span className={styles.grey}>{' | '}</span>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.SET_PERSISTENT,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
persistent: !persistent,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
|
||||
setPersistent(!persistent);
|
||||
}}
|
||||
>
|
||||
<Checkbox value={persistent}></Checkbox> Persistent events
|
||||
</span>
|
||||
{' | '}
|
||||
<button
|
||||
onClick={() => {
|
||||
// 重置 events
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.RESET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{eventFilter['message.data.store.id'] ? (
|
||||
<span>
|
||||
{' | '}
|
||||
<b
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
setNextStore(eventFilter['message.data.store.id']);
|
||||
}}
|
||||
>{` Displaying: [${eventFilter['message.data.store.id']}] `}</b>
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFilter('message.data.store.id');
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ marginTop: '0px', margin: '5px' }}>
|
||||
<button
|
||||
className={`${styles.filterButton} ${log.length ? '' : styles.grey} ${
|
||||
eventFilter['message.type'] ? '' : styles.active
|
||||
}`}
|
||||
onClick={() => {
|
||||
removeFilter('message.type');
|
||||
}}
|
||||
>
|
||||
All({usedTypes.all})
|
||||
</button>
|
||||
{Object.values(eventTypes).map(eventType => {
|
||||
return (
|
||||
<button
|
||||
className={`${styles.filterButton} ${
|
||||
usedTypes[eventType] ? '' : styles.grey
|
||||
} ${
|
||||
eventFilter['message.type'] === eventType ? styles.active : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
addFilter('message.type', eventType);
|
||||
}}
|
||||
>
|
||||
{`${eventType.replace('inulax ', '')}(${
|
||||
usedTypes[eventType] || 0
|
||||
})`}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Table
|
||||
data={processedData}
|
||||
dataKey={'id'}
|
||||
displayKeys={[
|
||||
['type', 'Event type:'],
|
||||
['storeClick', 'Store:'],
|
||||
['time', 'Time:'],
|
||||
['state', 'State:'],
|
||||
['additionalData', 'Additional data:'],
|
||||
]}
|
||||
displayDataProcessor={data => {
|
||||
const message = data.event.message;
|
||||
return {
|
||||
type: data.type,
|
||||
store: {
|
||||
actions: Object.fromEntries(
|
||||
Object.entries(message.data.store.$config.actions).map(
|
||||
([id, action]) => {
|
||||
return [
|
||||
id,
|
||||
(action as string).replace(/\{.*}/gms, '{...}').replace('function ', ''),
|
||||
];
|
||||
}
|
||||
)
|
||||
),
|
||||
computed: Object.fromEntries(
|
||||
Object.keys(message.data.store.$c).map(key => [
|
||||
key,
|
||||
message.data.store.expanded[key],
|
||||
])
|
||||
),
|
||||
state: message.data.store.$s,
|
||||
id: message.data.store.id,
|
||||
},
|
||||
// data: omit(data, 'storeClick', 'additionalData'),
|
||||
};
|
||||
}}
|
||||
search={eventFilter.fulltext ? eventFilter.fulltext : ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula, { useRef, useState } from 'openinula';
|
||||
|
||||
export function Modal({
|
||||
closeModal,
|
||||
then,
|
||||
children,
|
||||
}: {
|
||||
closeModal: () => void;
|
||||
then: (value: any) => void;
|
||||
children?: any[];
|
||||
}) {
|
||||
const inputRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.value = '';
|
||||
}, 10);
|
||||
|
||||
const tryGatherData = () => {
|
||||
let data;
|
||||
try {
|
||||
data = eval(inputRef.current.value);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (then) {
|
||||
then(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0 , 0.3)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
top: 'calc(50vh - 50px)',
|
||||
left: 'calc(50vw - 125px)',
|
||||
width: '250px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
position: 'fixed',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<p>{children}</p>
|
||||
<p>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={'text'}
|
||||
onKeyPress={({key}) => {
|
||||
if (key === 'Enter') {
|
||||
tryGatherData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{error ? <p>Variable parsing error</p> : null}
|
||||
<p>
|
||||
<button
|
||||
onClick={() => {
|
||||
tryGatherData();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
@import '../components/assets.less';
|
||||
|
||||
.displayData {
|
||||
background-color: rgb(241, 243, 244);
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
div.wrapper {
|
||||
margin: 15px;
|
||||
position: relative;
|
||||
width: calc(100% - 30px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.table {
|
||||
display: table;
|
||||
vertical-align: top;
|
||||
width: calc(100%);
|
||||
background-color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.row {
|
||||
display: table-row;
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
background-color: rgb(241, 243, 244);
|
||||
.default {
|
||||
background-color: rgb(241, 243, 244);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.cell {
|
||||
display: table-cell;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.half {
|
||||
width: calc(50% - 8px);
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.header {
|
||||
background-color: rgb(241, 243, 244);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.row.active {
|
||||
background-color: #00a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.tab {
|
||||
border: 1px solid grey;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
&.active {
|
||||
border-bottom: none;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
span.highlighted {
|
||||
background-color: #ff0;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #00a;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: #909;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #00a;
|
||||
}
|
||||
|
||||
.compositeInput {
|
||||
background-color: white;
|
||||
border: 1px solid grey;
|
||||
display: inline-block;
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
&.left {
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
border-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
background-color: transparent;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: 0;
|
||||
&.active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.added {
|
||||
background-color: #afa;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #0a0;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background-color: #faa;
|
||||
text-decoration-line: line-through;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #a00;
|
||||
}
|
||||
}
|
||||
|
||||
.changed {
|
||||
background-color: #ffa;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #ca0;
|
||||
}
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.floatingButton {
|
||||
right: 5px;
|
||||
position: absolute;
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
font-size: 10px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
max-height: calc(100vh - 65px);
|
||||
overflow: auto;
|
||||
|
||||
div.row {
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState } from 'openinula';
|
||||
import EventLog from './EventLog';
|
||||
import Stores from './Stores';
|
||||
import styles from './PanelX.less';
|
||||
|
||||
export default function PanelX() {
|
||||
const [active, setActive] = useState('stores');
|
||||
const [nextStoreId, setNextStoreId] = useState(null);
|
||||
const [eventFilter, setEventFilter] = useState({});
|
||||
|
||||
function showFilterEvents(filter) {
|
||||
setActive('events');
|
||||
setEventFilter(filter);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'stores',
|
||||
title: 'Stores',
|
||||
getComponent: () => (
|
||||
<Stores
|
||||
nextStoreId={nextStoreId}
|
||||
showFilteredEvents={showFilterEvents}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
title: 'Events',
|
||||
getComponents: () => (
|
||||
<EventLog
|
||||
setNextStore={id => {
|
||||
setNextStoreId(id);
|
||||
setActive('stores');
|
||||
}}
|
||||
setEventFilter={setEventFilter}
|
||||
eventFilter={eventFilter}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{tabs.map(tab =>
|
||||
tab.id === active ? (
|
||||
<button
|
||||
className={`${styles.tab} ${styles.active}`}
|
||||
disabled={true}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={styles.tab}
|
||||
onClick={() => {
|
||||
setActive(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{tabs.find(item => item.id === active).getComponent()}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'openinula';
|
||||
import {
|
||||
initBackgroundConnection,
|
||||
addBackgroundMessageListener,
|
||||
removeBackgroundMessageListener,
|
||||
postMessageToBackground,
|
||||
} from '../panelConnection';
|
||||
import { Table } from './Table';
|
||||
import { omit, sendMessage } from './utils';
|
||||
import styles from './PanelX.less';
|
||||
import { Highlight, RemoveHighlight } from '../utils/constants';
|
||||
import { ActionRunner } from './ActionRunner';
|
||||
import { Tree } from './Tree';
|
||||
|
||||
export default function Stores({ nextStoreId, showFilteredEvents }) {
|
||||
const [stores, setStores] = useState([]);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
if (!initialized) {
|
||||
setTimeout(() => {
|
||||
sendMessage({
|
||||
type: 'inulax getStores',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const listener = message => {
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
// 过滤 inula 消息
|
||||
if (message.payload.type === 'inulax stores') {
|
||||
// Stores 更新
|
||||
setStores(message.payload.stores);
|
||||
setInitialized(true);
|
||||
} else if (message.payload.type === 'inulax flush stores') {
|
||||
// Flush store
|
||||
sendMessage({
|
||||
type: 'inulax getStores',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
} else if (message.payload.type === 'inulax observed components') {
|
||||
// observed components 更新
|
||||
setStores(
|
||||
stores.map(store => {
|
||||
store.observedComponents = message.payload.data[store.id] || [];
|
||||
return store;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
initBackgroundConnection('panel');
|
||||
addBackgroundMessageListener(listener);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(listener);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState } from 'openinula';
|
||||
import {Tree} from './Tree';
|
||||
import styles from './PanelX.less';
|
||||
|
||||
type displayKeysType = [string, string][];
|
||||
|
||||
export function Table({
|
||||
data,
|
||||
dataKey = 'id',
|
||||
displayKeys,
|
||||
activate,
|
||||
displayDataProcessor,
|
||||
search = '',
|
||||
}: {
|
||||
data;
|
||||
dataKey: string;
|
||||
displayKeys: displayKeysType;
|
||||
activate?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
displayDataProcessor: (data: { [key: string]: any }) => {
|
||||
[key: string]: any;
|
||||
};
|
||||
search: string;
|
||||
}) {
|
||||
const [keyToDisplay, setKeyToDisplay] = useState(false);
|
||||
const [manualOverride, setManualOverride] = useState(false);
|
||||
let displayRow = null;
|
||||
|
||||
if (!manualOverride && activate) {
|
||||
data.forEach(item => {
|
||||
if (displayRow) {
|
||||
return;
|
||||
}
|
||||
let notMatch = false;
|
||||
Object.entries(activate).forEach(([key, value]) => {
|
||||
if (notMatch) {
|
||||
return;
|
||||
}
|
||||
if (item[key] !== value) {
|
||||
notMatch = true;
|
||||
}
|
||||
});
|
||||
if (notMatch) {
|
||||
return;
|
||||
}
|
||||
displayRow = item;
|
||||
});
|
||||
} else if (manualOverride && keyToDisplay) {
|
||||
data.forEach(item => {
|
||||
if (displayRow) {
|
||||
return;
|
||||
}
|
||||
if (item[dataKey] === keyToDisplay) {
|
||||
displayRow = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (displayRow) {
|
||||
const [attr, title] = displayKeys[0];
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={`${styles.table} ${styles.half}`}>
|
||||
<div className={styles.row}>
|
||||
<div className={`${styles.cell} ${styles.header}`}>{title}</div>
|
||||
</div>
|
||||
<div className={styles.scrollable}>
|
||||
<span></span>
|
||||
{data.map(row => (
|
||||
<div
|
||||
className={`${styles.row} ${
|
||||
keyToDisplay === row[dataKey] ? styles.active : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setManualOverride(true);
|
||||
setKeyToDisplay(
|
||||
keyToDisplay === row[dataKey] ? null : row[dataKey]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className={styles.cell}>{row?.[attr] || ''}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.table} ${styles.half} ${styles.displayData}`}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.cell}>
|
||||
<b>Data:</b>
|
||||
<button
|
||||
className={styles.floatingButton}
|
||||
onClick={() => {
|
||||
setKeyToDisplay(null);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.scrollable}>
|
||||
<span></span>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.cell}>
|
||||
<Tree
|
||||
data={
|
||||
displayDataProcessor
|
||||
? displayDataProcessor(displayRow)
|
||||
: displayRow
|
||||
}
|
||||
indent={displayRow[displayKeys[0][0]]}
|
||||
expand={true}
|
||||
search={search}
|
||||
forcedExpand={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.table}>
|
||||
<div className={`${styles.row} ${styles.header}`}>
|
||||
{displayKeys.map(([key, title]) => (
|
||||
<div className={styles.cell}>{title}</div>
|
||||
))}
|
||||
</div>
|
||||
{data.map(item => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setManualOverride(true);
|
||||
setKeyToDisplay(item[dataKey]);
|
||||
}}
|
||||
className={styles.row}
|
||||
>
|
||||
{displayKeys.map(([key, title]) => (
|
||||
<div className={styles.cell}>{item[key]}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState } from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import {Modal} from './Modal';
|
||||
import { displayValue, omit } from './utils';
|
||||
|
||||
export function Tree({
|
||||
data,
|
||||
indent = 0,
|
||||
index = '',
|
||||
expand = false,
|
||||
search = '',
|
||||
forcedExpand = false,
|
||||
onEdit = null,
|
||||
omitAttrs = [],
|
||||
className,
|
||||
forcedLabel = null,
|
||||
}: {
|
||||
data: any;
|
||||
indent?: number;
|
||||
index?: string | number;
|
||||
expand?: boolean;
|
||||
search?: string;
|
||||
forcedExpand?: boolean;
|
||||
className?: string | undefined
|
||||
omitAttrs?: string[];
|
||||
onEdit?: (path: any[], value: any) => void | null;
|
||||
forcedLabel?: string | number | null;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(expand);
|
||||
const [modal, setModal] = useState(false);
|
||||
const isArray = Array.isArray(data);
|
||||
const isObject = data && !isArray && typeof data === 'object';
|
||||
const isSet = isObject && data?._type === 'Set';
|
||||
const isWeakSet = isObject && data?._type === 'WeakSet';
|
||||
const isMap = isObject && data?._type === 'Map';
|
||||
const isWeakMap = isObject && data?._type === 'WeakMap';
|
||||
const isVNode = isObject && data.vtype;
|
||||
const canBeExpanded = isArray || (isObject && !isWeakSet && !isWeakMap);
|
||||
|
||||
if (isObject && omitAttrs?.length) {
|
||||
data = omit(data, ...omitAttrs);
|
||||
}
|
||||
|
||||
return canBeExpanded ? (
|
||||
<div
|
||||
style={{ fontFamily: 'monoSpace' }}
|
||||
className={`${expanded ? 'expanded' : 'not-expanded'} ${className}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
{new Array(Math.max(indent, 0)).fill(<span> </span>)}
|
||||
{forcedExpand || isVNode ? null : expanded ? (
|
||||
<span>▼</span>
|
||||
) : (
|
||||
<span>▶</span>
|
||||
)}
|
||||
{index === 0 || index ? (
|
||||
<>
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{forcedLabel
|
||||
? forcedLabel
|
||||
: expanded
|
||||
? isVNode
|
||||
? null
|
||||
: Array.isArray(data)
|
||||
? `Array(${data.length})`
|
||||
: isMap
|
||||
? `Map(${data.entries.length})`
|
||||
: isSet
|
||||
? `Set(${data.values.length})`
|
||||
: '{ ... }'
|
||||
: isWeakMap
|
||||
? 'WeakMap()'
|
||||
: isWeakSet
|
||||
? 'WeakSet()'
|
||||
: isMap
|
||||
? `Map(${data.entries.length})`
|
||||
: isSet
|
||||
? `Set(${data.values.length})`
|
||||
: Array.isArray(data)
|
||||
? `Array(${data.length})`
|
||||
: '{ ... }'}
|
||||
</span>
|
||||
{expanded || isVNode ? (
|
||||
isArray ? (
|
||||
<>
|
||||
{data.map((value, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={value}
|
||||
indent={indent + 4}
|
||||
index={index}
|
||||
search={search}
|
||||
className={className}
|
||||
onEdit={
|
||||
onEdit
|
||||
? (path, val) => {
|
||||
onEdit(path.concat([index]), val);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : isVNode ? (
|
||||
data
|
||||
) : isMap ? (
|
||||
<div>
|
||||
{data.entries.map(([key, value]) => {
|
||||
return (
|
||||
<Tree
|
||||
data={{key, value}}
|
||||
indent={indent + 4}
|
||||
search={search}
|
||||
className={className}
|
||||
// TODO: editable sets
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : isSet ? (
|
||||
data.values.map(item => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={item}
|
||||
indent={indent + 4}
|
||||
search={search}
|
||||
className={className}
|
||||
// TODO: editable sets
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
Object.entries(data).map(([key, value]) => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={value}
|
||||
indent={indent + 4}
|
||||
index={key}
|
||||
search={search}
|
||||
className={className}
|
||||
onEdit={
|
||||
onEdit
|
||||
? (path, val) => {
|
||||
onEdit(path.concat([key]), val);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={'not-expanded'}>
|
||||
{new Array(indent).fill(<span> </span>)}
|
||||
<span className={`${className}`}>
|
||||
{typeof index !== 'undefined' ? (
|
||||
<>
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{displayValue(data, search)}
|
||||
{onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map
|
||||
<>
|
||||
<b
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setModal(true);
|
||||
}}
|
||||
>
|
||||
☼
|
||||
</b>
|
||||
{onEdit && modal ? (
|
||||
<Modal
|
||||
closeModal={() => {
|
||||
setModal(false);
|
||||
}}
|
||||
then={data => {
|
||||
onEdit([], data);
|
||||
setModal(false);
|
||||
}}
|
||||
>
|
||||
<h3>Edit value:</h3> {index}
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import PanelX from './PanelX';
|
||||
|
||||
export default PanelX;
|
|
@ -0,0 +1,49 @@
|
|||
<!--
|
||||
~ Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
~
|
||||
~ openInula is licensed under Mulan PSL v2.
|
||||
~ You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
~ You may obtain a copy of Mulan PSL v2 at:
|
||||
~
|
||||
~ http://license.coscl.org.cn/MulanPSL2
|
||||
~
|
||||
~ THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
~ EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
~ MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
~ See the Mulan PSL v2 for more details.
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html style="display: flex">
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="inula.development.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="panelX.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import * as Inula from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import { DevToolPanel } from '../utils/constants';
|
||||
|
||||
export function highlight(source, search) {
|
||||
if (!search || !source?.split) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const parts = source.split(search);
|
||||
const result = [];
|
||||
|
||||
for (let i= 0; i < parts.length * 2 - 1; i++) {
|
||||
if (i % 2) {
|
||||
result.push(<span className={styles.highlighted}>{search}</span>);
|
||||
} else {
|
||||
result.push(parts[i / 2]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function displayValue(val: any, search = '') {
|
||||
if (typeof val === 'boolean') {
|
||||
return (
|
||||
<span>
|
||||
{highlight(val ? 'true' : 'false', search)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (val === '') {
|
||||
return <span className={styles.red}>{'""'}</span>;
|
||||
}
|
||||
|
||||
if (typeof val === 'undefined') {
|
||||
return <span className={styles.grey}>{highlight('undefined', search)}</span>;
|
||||
}
|
||||
|
||||
if (val === 'null') {
|
||||
return <span className={styles.grey}>{highlight('null', search)}</span>;
|
||||
}
|
||||
|
||||
if (typeof val === 'string') {
|
||||
if (val.match(/^function\s?\(/)) {
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{highlight(
|
||||
val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''),
|
||||
search
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className={styles.red}>"{highlight(val, search)}"</span>;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
return <span className={styles.blue}>{highlight('' + val, search)}</span>;
|
||||
}
|
||||
if (typeof val === 'function') {
|
||||
const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, '');
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{highlight(args, search)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (typeof val === 'object') {
|
||||
if (val?._type === 'WeakSet') {
|
||||
return <span>WeakSet()</span>;
|
||||
}
|
||||
|
||||
if (val?._type === 'WeakMap') {
|
||||
return <span>WeakMap()</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fullTextSearch(value, search) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(val => fullTextSearch(val, search));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (value?._type === 'Set') {
|
||||
return value.values.some(val => fullTextSearch(val, search));
|
||||
}
|
||||
if (value?._type === 'Map') {
|
||||
return value.entries.some(
|
||||
(key, val) => fullTextSearch(key, search) || fullTextSearch(val, search)
|
||||
);
|
||||
}
|
||||
return Object.values(value).some(val => fullTextSearch(val, search));
|
||||
}
|
||||
|
||||
return value.toString().includes(search);
|
||||
}
|
||||
|
||||
export function omit(obj, ...attrs) {
|
||||
const res = { ...obj };
|
||||
attrs.forEach(attr => delete res[attr]);
|
||||
return res;
|
||||
}
|
||||
|
||||
export function stringify(data) {
|
||||
if (typeof data === 'string' && data.startsWith('function(')) {
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{data.match(/^function\([\w,]*\)/g)[0].substring(8)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return displayValue(data);
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return `Array(${data.length})`;
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return `{${Object.entries(data).map(([key, value]) => {
|
||||
if (typeof value === 'string' && value.startsWith('function(')) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{value.match(/^function\([\w,]*\)/g)[0].substring(8)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (!value) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{displayValue(value)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Array(${value.length})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if ((value as any)?._type === 'WeakSet') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'WeakSet()'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'WeakMap') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'WeakMap'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'Set') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Set(${(value as Set<any>).size})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'Map') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Map(${(value as Map<any, any>).size})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// object
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'{...}'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {displayValue(value)}
|
||||
</span>
|
||||
);
|
||||
})}}`;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function sendMessage(payload) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload,
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}
|
|
@ -38,7 +38,7 @@ export function injectSrc(src) {
|
|||
).appendChild(script);
|
||||
}
|
||||
|
||||
function injectCode(code) {
|
||||
export function injectCode(code) {
|
||||
const script = document.createElement('script');
|
||||
script.textContent = code;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "inula-intl",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "",
|
||||
"main": "build/intl.umd.js",
|
||||
"type": "commonjs",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "inula-request",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"description": "Inula-request brings you a convenient request experience!",
|
||||
"main": "./dist/inulaRequest.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -41,4 +41,7 @@ export default {
|
|||
presets: ['@babel/preset-env']
|
||||
})
|
||||
],
|
||||
external:[
|
||||
'openinula'
|
||||
],
|
||||
};
|
||||
|
|
|
@ -157,10 +157,6 @@ openinula团队会关注所有Pull Request,我们会review以及合入你的
|
|||
1. `npm run build` 同时构建openinula UMD的prod版本和dev版本
|
||||
2. `build-types` 单独构建openinula的类型提示@types目录
|
||||
|
||||
#### 配套开发工具
|
||||
|
||||
- [openinula-devtool](https://www.XXXX.com): 可视化openinula项目页面的vDom树
|
||||
|
||||
## 开源许可协议
|
||||
|
||||
请查阅 License 获取开源许可协议的更多信息.
|
||||
|
|
|
@ -187,7 +187,7 @@ describe('Redux adapter', () => {
|
|||
reduxStore.dispatch({ type: 'toggle' });
|
||||
reduxStore.dispatch({ type: 'toggle' });
|
||||
|
||||
expect(counter).toBe(3); // NOTE: first action is always store initialization
|
||||
expect(counter).toBe(2); // execute dispatch two times, applyMiddleware was called same times
|
||||
});
|
||||
|
||||
it('Should apply multiple enhancers', async () => {
|
||||
|
@ -226,7 +226,7 @@ describe('Redux adapter', () => {
|
|||
|
||||
reduxStore.dispatch({ type: 'toggle' });
|
||||
|
||||
expect(counter).toBe(2); // NOTE: first action is always store initialization
|
||||
expect(counter).toBe(1); // execute dispatch two times, applyMiddleware was called same times
|
||||
expect(lastAction).toBe('toggle');
|
||||
expect(middlewareCallList[0]).toBe('callCounter');
|
||||
expect(middlewareCallList[1]).toBe('lastFunctionStorage');
|
||||
|
|
|
@ -67,18 +67,50 @@ export function isPromise(obj: any): boolean {
|
|||
return isObject(obj) && typeof obj.then === 'function';
|
||||
}
|
||||
|
||||
export function isSame(x, y) {
|
||||
if (typeof Object.is !== 'function') {
|
||||
if (x === y) {
|
||||
// +0 != -0
|
||||
return x !== 0 || 1 / x === 1 / y;
|
||||
} else {
|
||||
// NaN == NaN
|
||||
return x !== x && y !== y;
|
||||
}
|
||||
} else {
|
||||
return Object.is(x, y);
|
||||
export function isSame(x: unknown, y: unknown): boolean {
|
||||
// 如果两个对象是同一个引用,直接返回true
|
||||
if (x === y) {
|
||||
return true;
|
||||
}
|
||||
// 如果两个对象类型不同,直接返回false
|
||||
if (typeof x !== typeof y) {
|
||||
return false;
|
||||
}
|
||||
// 如果两个对象都是null或undefined,直接返回true
|
||||
if (x == null || y == null) {
|
||||
return true;
|
||||
}
|
||||
// 如果两个对象都是基本类型,比较他们的值是否相等
|
||||
if (typeof x !== 'object') {
|
||||
return x === y;
|
||||
}
|
||||
// 如果两个对象都是数组,比较他们的长度是否相等,然后递归比较每个元素是否相等
|
||||
if (Array.isArray(x) && Array.isArray(y)) {
|
||||
if (x.length !== y.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
if (!isSame(x[i], y[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 两个对象都是普通对象,首先比较他们的属性数量是否相等,然后递归比较每个属性的值是否相等
|
||||
if (typeof x === 'object' && typeof y === 'object') {
|
||||
const keys1 = Object.keys(x!).sort();
|
||||
const keys2 = Object.keys(y!).sort();
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < keys1.length; i++) {
|
||||
if (!isSame(x![keys1[i]], y![keys2[i]])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getDetailedType(val: any) {
|
||||
|
|
|
@ -27,12 +27,12 @@ export {
|
|||
createDispatchHook,
|
||||
} from './reduxReact';
|
||||
|
||||
export type ReduxStoreHandler = {
|
||||
reducer: (state: any, action: { type: string }) => any;
|
||||
dispatch: (action: { type: string }) => void;
|
||||
getState: () => any;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void;
|
||||
export type ReduxStoreHandler<T = any> = {
|
||||
reducer(state: T, action: { type: string }): any;
|
||||
dispatch(action: { type: string }): void;
|
||||
getState(): T;
|
||||
subscribe(listener: () => void): () => void;
|
||||
replaceReducer(reducer: (state: T, action: { type: string }) => any): void;
|
||||
};
|
||||
|
||||
export type ReduxAction = {
|
||||
|
@ -53,6 +53,9 @@ export type ReduxMiddleware = (
|
|||
|
||||
type Reducer = (state: any, action: ReduxAction) => any;
|
||||
|
||||
type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler;
|
||||
type StoreEnhancer = (next: StoreCreator) => StoreCreator;
|
||||
|
||||
function mergeData(state, data) {
|
||||
if (!data) {
|
||||
state.stateWrapper = data;
|
||||
|
@ -87,7 +90,7 @@ function mergeData(state, data) {
|
|||
state.stateWrapper = data;
|
||||
}
|
||||
|
||||
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler {
|
||||
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
|
||||
const store = createStoreX({
|
||||
id: 'defaultStore',
|
||||
state: { stateWrapper: preloadedState },
|
||||
|
@ -130,12 +133,14 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?):
|
|||
dispatch: store.$a.dispatch,
|
||||
};
|
||||
|
||||
enhancers && enhancers(result);
|
||||
|
||||
result.dispatch({ type: 'InulaX' });
|
||||
|
||||
store.reduxHandler = result;
|
||||
|
||||
if (typeof enhancers === 'function') {
|
||||
return enhancers(createStore)(reducer, preloadedState);
|
||||
}
|
||||
|
||||
return result as ReduxStoreHandler;
|
||||
}
|
||||
|
||||
|
@ -150,19 +155,23 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer {
|
|||
};
|
||||
}
|
||||
|
||||
function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void {
|
||||
middlewares = middlewares.slice();
|
||||
middlewares.reverse();
|
||||
let dispatch = store.dispatch;
|
||||
middlewares.forEach(middleware => {
|
||||
dispatch = middleware(store)(dispatch);
|
||||
});
|
||||
store.dispatch = dispatch;
|
||||
function applyMiddlewares(createStore: StoreCreator, middlewares: ReduxMiddleware[]): StoreCreator {
|
||||
return (reducer, preloadedState) => {
|
||||
middlewares = middlewares.slice();
|
||||
middlewares.reverse();
|
||||
const storeObj = createStore(reducer, preloadedState);
|
||||
let dispatch = storeObj.dispatch;
|
||||
middlewares.forEach(middleware => {
|
||||
dispatch = middleware(storeObj)(dispatch);
|
||||
});
|
||||
storeObj.dispatch = dispatch;
|
||||
return storeObj;
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void {
|
||||
return store => {
|
||||
return applyMiddlewares(store, middlewares);
|
||||
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (createStore: StoreCreator) => StoreCreator {
|
||||
return createStore => {
|
||||
return applyMiddlewares(createStore, middlewares);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -170,7 +179,7 @@ type ActionCreator = (...params: any[]) => ReduxAction;
|
|||
type ActionCreators = { [key: string]: ActionCreator };
|
||||
export type BoundActionCreator = (...params: any[]) => void;
|
||||
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
||||
type Dispatch = (action) => any;
|
||||
type Dispatch = (action: ReduxAction) => any;
|
||||
|
||||
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
||||
const boundActionCreators = {};
|
||||
|
@ -183,12 +192,12 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis
|
|||
return boundActionCreators;
|
||||
}
|
||||
|
||||
export function compose(...middlewares: ReduxMiddleware[]) {
|
||||
return (store: ReduxStoreHandler, extraArgument: any) => {
|
||||
let val;
|
||||
middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {
|
||||
export function compose<T = StoreCreator>(...middlewares: ((...args: any[]) => any)[]): (...args: any[]) => T {
|
||||
return (...args) => {
|
||||
let val: any;
|
||||
middlewares.reverse().forEach((middleware, index) => {
|
||||
if (!index) {
|
||||
val = middleware(store, extraArgument);
|
||||
val = middleware(...args);
|
||||
return;
|
||||
}
|
||||
val = middleware(val);
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho
|
|||
import { createContext } from '../../renderer/components/context/CreateContext';
|
||||
import { createElement } from '../../external/JSXElement';
|
||||
import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux';
|
||||
import { forwardRef } from '../../renderer/components/ForwardRef';
|
||||
|
||||
const DefaultContext = createContext(null);
|
||||
type Context = typeof DefaultContext;
|
||||
|
@ -40,29 +41,27 @@ export function createStoreHook(context: Context): () => ReduxStoreHandler {
|
|||
};
|
||||
}
|
||||
|
||||
export function createSelectorHook(context: Context): (selector?: (any) => any) => any {
|
||||
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
|
||||
return function (selector = state => state) {
|
||||
const [b, fr] = useState(false);
|
||||
export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any {
|
||||
const store = createStoreHook(context)();
|
||||
return function useSelector(selector = state => state) {
|
||||
const [state, setState] = useState(() => store.getState());
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(() => fr(!b));
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
setState(store.getState());
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
return selector(store.getState());
|
||||
return selector(state);
|
||||
};
|
||||
}
|
||||
|
||||
export function createDispatchHook(context: Context): () => BoundActionCreator {
|
||||
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
|
||||
return function () {
|
||||
return action => {
|
||||
store.dispatch(action);
|
||||
};
|
||||
}.bind(store);
|
||||
const store = createStoreHook(context)();
|
||||
return function useDispatch() {
|
||||
return store.dispatch;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSelector = selector => {
|
||||
|
@ -90,6 +89,11 @@ type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
|||
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
||||
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
||||
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
||||
type ConnectOption<State = any> = {
|
||||
areStatesEqual?: (oldState: State, newState: State) => boolean;
|
||||
context?: Context;
|
||||
forwardRef?: boolean;
|
||||
}
|
||||
|
||||
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({} as StateProps),
|
||||
|
@ -99,10 +103,7 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
|||
dispatchProps,
|
||||
ownProps
|
||||
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps),
|
||||
options?: {
|
||||
areStatesEqual?: (oldState: any, newState: any) => boolean;
|
||||
context?: Context;
|
||||
}
|
||||
options?: ConnectOption
|
||||
): Connector<OwnProps, MergedProps> {
|
||||
if (!options) {
|
||||
options = {};
|
||||
|
@ -114,37 +115,31 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
|||
|
||||
//this component should mimic original type of component used
|
||||
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
||||
const [f, forceReload] = useState(true);
|
||||
|
||||
const store = useStore() as unknown as ReduxStoreHandler;
|
||||
const store = useStore() as ReduxStoreHandler;
|
||||
const [state, setState] = useState(() => store.getState());
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(() => forceReload(!f));
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
setState(store.getState());
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const previous = useRef({
|
||||
const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({
|
||||
state: {},
|
||||
mappedState: {},
|
||||
}) as {
|
||||
current: {
|
||||
state: { [key: string]: any };
|
||||
mappedState: StateProps;
|
||||
};
|
||||
};
|
||||
mappedState: {} as StateProps,
|
||||
});
|
||||
|
||||
let mappedState: StateProps;
|
||||
if (options?.areStatesEqual) {
|
||||
if (options.areStatesEqual(previous.current.state, store.getState())) {
|
||||
if (options.areStatesEqual(previous.current.state, state)) {
|
||||
mappedState = previous.current.mappedState as StateProps;
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
||||
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
||||
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
let mappedDispatch: DispatchProps = {} as DispatchProps;
|
||||
|
@ -153,12 +148,14 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
|||
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
||||
store.dispatch(value(...args));
|
||||
setState(store.getState());
|
||||
};
|
||||
});
|
||||
} else {
|
||||
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
||||
}
|
||||
}
|
||||
mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch });
|
||||
const mergedProps = (
|
||||
mergeProps ||
|
||||
((state, dispatch, originalProps) => {
|
||||
|
@ -166,12 +163,18 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
|||
})
|
||||
)(mappedState, mappedDispatch, props);
|
||||
|
||||
previous.current.state = store.getState();
|
||||
previous.current.state = state;
|
||||
|
||||
const node = createElement(Component, mergedProps);
|
||||
return node;
|
||||
return createElement(Component, mergedProps);
|
||||
};
|
||||
|
||||
if (options?.forwardRef) {
|
||||
const forwarded = forwardRef((props, ref) => {
|
||||
return Wrapper({ ...props, ref: ref });
|
||||
});
|
||||
return forwarded as WrappedComponent<OwnProps>;
|
||||
}
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
|
|||
}
|
||||
|
||||
export const thunk = createThunkMiddleware();
|
||||
// @ts-ignore
|
||||
thunk.withExtraArgument = createThunkMiddleware;
|
||||
|
||||
export const withExtraArgument = createThunkMiddleware;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils';
|
||||
import { getStore, getAllStores } from '../store/StoreHandler';
|
||||
import { OBSERVED_COMPONENTS } from './constants';
|
||||
import { VNode } from "../../renderer/vnode/VNode";
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
|
||||
const sessionId = Date.now();
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import { setStateChange } from '../render/FunctionComponent';
|
|||
import { getHookStage, HookStage } from './HookStage';
|
||||
import type { VNode } from '../Types';
|
||||
import { getProcessingVNode } from '../GlobalVar';
|
||||
import { markUpdatedInRender } from "./HookMain";
|
||||
import { markUpdatedInRender } from './HookMain';
|
||||
|
||||
// 构造新的Update数组
|
||||
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
||||
|
|
|
@ -74,7 +74,7 @@ export function getLazyVNodeTag(lazyComp: any): string {
|
|||
} else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) {
|
||||
return typeLazyMap[lazyComp.vtype];
|
||||
}
|
||||
throw Error("Inula can't resolve the content of lazy");
|
||||
throw Error('Inula can\'t resolve the content of lazy');
|
||||
}
|
||||
|
||||
// 创建processing
|
||||
|
|
Loading…
Reference in New Issue