mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Allow pinning of AI root nodes (#9060)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
parent
caea27dbb5
commit
32df17104c
|
@ -134,7 +134,7 @@ describe('Data pinning', () => {
|
||||||
ndv.getters.pinDataButton().should('not.exist');
|
ndv.getters.pinDataButton().should('not.exist');
|
||||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||||
|
|
||||||
ndv.actions.setPinnedData([
|
ndv.actions.pastePinnedData([
|
||||||
{
|
{
|
||||||
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')),
|
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')),
|
||||||
},
|
},
|
||||||
|
|
|
@ -206,7 +206,7 @@ describe('Data mapping', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
workflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
workflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
workflowPage.actions.openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
workflowPage.actions.openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
ndv.actions.setPinnedData([
|
ndv.actions.pastePinnedData([
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -324,7 +324,7 @@ describe('NDV', () => {
|
||||||
];
|
];
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
workflowPage.actions.openNode('Get thread details1');
|
workflowPage.actions.openNode('Get thread details1');
|
||||||
ndv.actions.setPinnedData(PINNED_DATA);
|
ndv.actions.pastePinnedData(PINNED_DATA);
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
workflowPage.actions.executeWorkflow();
|
workflowPage.actions.executeWorkflow();
|
||||||
|
|
|
@ -155,6 +155,17 @@ export class NDV extends BasePage {
|
||||||
|
|
||||||
this.actions.savePinnedData();
|
this.actions.savePinnedData();
|
||||||
},
|
},
|
||||||
|
pastePinnedData: (data: object) => {
|
||||||
|
this.getters.editPinnedDataButton().click();
|
||||||
|
|
||||||
|
this.getters.pinnedDataEditor().click();
|
||||||
|
this.getters
|
||||||
|
.pinnedDataEditor()
|
||||||
|
.type('{selectall}{backspace}', { delay: 0 })
|
||||||
|
.paste(JSON.stringify(data));
|
||||||
|
|
||||||
|
this.actions.savePinnedData();
|
||||||
|
},
|
||||||
clearParameterInput: (parameterName: string) => {
|
clearParameterInput: (parameterName: string) => {
|
||||||
this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`);
|
this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`);
|
||||||
},
|
},
|
||||||
|
|
|
@ -217,8 +217,8 @@
|
||||||
<div :class="[$style.editModeBody, 'ignore-key-press']">
|
<div :class="[$style.editModeBody, 'ignore-key-press']">
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
:model-value="editMode.value"
|
:model-value="editMode.value"
|
||||||
@update:model-value="ndvStore.setOutputPanelEditModeValue($event)"
|
|
||||||
:fill-parent="true"
|
:fill-parent="true"
|
||||||
|
@update:model-value="ndvStore.setOutputPanelEditModeValue($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.editModeFooter">
|
<div :class="$style.editModeFooter">
|
||||||
|
@ -725,45 +725,6 @@ export default defineComponent({
|
||||||
search: '',
|
search: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.init();
|
|
||||||
|
|
||||||
if (!this.isPaneTypeInput) {
|
|
||||||
this.showPinDataDiscoveryTooltip(this.jsonData);
|
|
||||||
}
|
|
||||||
this.ndvStore.setNDVBranchIndex({
|
|
||||||
pane: this.paneType as 'input' | 'output',
|
|
||||||
branchIndex: this.currentOutputIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.paneType === 'output') {
|
|
||||||
this.setDisplayMode();
|
|
||||||
this.activatePane();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasRunError) {
|
|
||||||
const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error;
|
|
||||||
const errorsToTrack = ['unknown error'];
|
|
||||||
|
|
||||||
if (error && errorsToTrack.some((e) => error.message.toLowerCase().includes(e))) {
|
|
||||||
this.$telemetry.track(
|
|
||||||
`User encountered an error: "${error.message}"`,
|
|
||||||
{
|
|
||||||
node: this.node.type,
|
|
||||||
errorMessage: error.message,
|
|
||||||
nodeVersion: this.node.typeVersion,
|
|
||||||
n8nVersion: this.rootStore.versionCli,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withPostHog: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.hidePinDataDiscoveryTooltip();
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(
|
...mapStores(
|
||||||
useNodeTypesStore,
|
useNodeTypesStore,
|
||||||
|
@ -803,21 +764,14 @@ export default defineComponent({
|
||||||
return this.nodeTypesStore.isTriggerNode(this.node.type);
|
return this.nodeTypesStore.isTriggerNode(this.node.type);
|
||||||
},
|
},
|
||||||
canPinData(): boolean {
|
canPinData(): boolean {
|
||||||
// Only "main" inputs can pin data
|
|
||||||
|
|
||||||
if (this.node === null) {
|
if (this.node === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = this.workflowsStore.getCurrentWorkflow();
|
const canPinNode = usePinnedData(this.node).canPinNode(false);
|
||||||
const workflowNode = workflow.getNode(this.node.name);
|
|
||||||
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, this.nodeType!);
|
|
||||||
const inputNames = NodeHelpers.getConnectionTypes(inputs);
|
|
||||||
|
|
||||||
const nonMainInputs = !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!nonMainInputs &&
|
canPinNode &&
|
||||||
!this.isPaneTypeInput &&
|
!this.isPaneTypeInput &&
|
||||||
this.pinnedData.isValidNodeType.value &&
|
this.pinnedData.isValidNodeType.value &&
|
||||||
!(this.binaryData && this.binaryData.length > 0)
|
!(this.binaryData && this.binaryData.length > 0)
|
||||||
|
@ -1035,6 +989,87 @@ export default defineComponent({
|
||||||
return this.hasNodeRun && !this.inputData.length && this.search;
|
return this.hasNodeRun && !this.inputData.length && this.search;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
node(newNode: INodeUi, prevNode: INodeUi) {
|
||||||
|
if (newNode.id === prevNode.id) return;
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
hasNodeRun() {
|
||||||
|
if (this.paneType === 'output') this.setDisplayMode();
|
||||||
|
},
|
||||||
|
inputDataPage: {
|
||||||
|
handler(data: INodeExecutionData[]) {
|
||||||
|
if (this.paneType && data) {
|
||||||
|
this.ndvStore.setNDVPanelDataIsEmpty({
|
||||||
|
panel: this.paneType as 'input' | 'output',
|
||||||
|
isEmpty: data.every((item) => isEmpty(item.json)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
jsonData(data: IDataObject[], prevData: IDataObject[]) {
|
||||||
|
if (isEqual(data, prevData)) return;
|
||||||
|
this.refreshDataSize();
|
||||||
|
this.showPinDataDiscoveryTooltip(data);
|
||||||
|
},
|
||||||
|
binaryData(newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) {
|
||||||
|
if (newData.length && !prevData.length && this.displayMode !== 'binary') {
|
||||||
|
this.switchToBinary();
|
||||||
|
} else if (!newData.length && this.displayMode === 'binary') {
|
||||||
|
this.onDisplayModeChange('table');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentOutputIndex(branchIndex: number) {
|
||||||
|
this.ndvStore.setNDVBranchIndex({
|
||||||
|
pane: this.paneType as 'input' | 'output',
|
||||||
|
branchIndex,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
search(newSearch: string) {
|
||||||
|
this.$emit('search', newSearch);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
if (!this.isPaneTypeInput) {
|
||||||
|
this.showPinDataDiscoveryTooltip(this.jsonData);
|
||||||
|
}
|
||||||
|
this.ndvStore.setNDVBranchIndex({
|
||||||
|
pane: this.paneType as 'input' | 'output',
|
||||||
|
branchIndex: this.currentOutputIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.paneType === 'output') {
|
||||||
|
this.setDisplayMode();
|
||||||
|
this.activatePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasRunError) {
|
||||||
|
const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error;
|
||||||
|
const errorsToTrack = ['unknown error'];
|
||||||
|
|
||||||
|
if (error && errorsToTrack.some((e) => error.message.toLowerCase().includes(e))) {
|
||||||
|
this.$telemetry.track(
|
||||||
|
`User encountered an error: "${error.message}"`,
|
||||||
|
{
|
||||||
|
node: this.node.type,
|
||||||
|
errorMessage: error.message,
|
||||||
|
nodeVersion: this.node.typeVersion,
|
||||||
|
n8nVersion: this.rootStore.versionCli,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withPostHog: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.hidePinDataDiscoveryTooltip();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getResolvedNodeOutputs() {
|
getResolvedNodeOutputs() {
|
||||||
if (this.node && this.nodeType) {
|
if (this.node && this.nodeType) {
|
||||||
|
@ -1500,48 +1535,6 @@ export default defineComponent({
|
||||||
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
|
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
node(newNode: INodeUi, prevNode: INodeUi) {
|
|
||||||
if (newNode.id === prevNode.id) return;
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
hasNodeRun() {
|
|
||||||
if (this.paneType === 'output') this.setDisplayMode();
|
|
||||||
},
|
|
||||||
inputDataPage: {
|
|
||||||
handler(data: INodeExecutionData[]) {
|
|
||||||
if (this.paneType && data) {
|
|
||||||
this.ndvStore.setNDVPanelDataIsEmpty({
|
|
||||||
panel: this.paneType as 'input' | 'output',
|
|
||||||
isEmpty: data.every((item) => isEmpty(item.json)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
jsonData(data: IDataObject[], prevData: IDataObject[]) {
|
|
||||||
if (isEqual(data, prevData)) return;
|
|
||||||
this.refreshDataSize();
|
|
||||||
this.showPinDataDiscoveryTooltip(data);
|
|
||||||
},
|
|
||||||
binaryData(newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) {
|
|
||||||
if (newData.length && !prevData.length && this.displayMode !== 'binary') {
|
|
||||||
this.switchToBinary();
|
|
||||||
} else if (!newData.length && this.displayMode === 'binary') {
|
|
||||||
this.onDisplayModeChange('table');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentOutputIndex(branchIndex: number) {
|
|
||||||
this.ndvStore.setNDVBranchIndex({
|
|
||||||
pane: this.paneType as 'input' | 'output',
|
|
||||||
branchIndex,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
search(newSearch: string) {
|
|
||||||
this.$emit('search', newSearch);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import type { XYPosition } from '@/Interface';
|
import type { XYPosition } from '@/Interface';
|
||||||
import {
|
import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
|
||||||
NOT_DUPLICATABE_NODE_TYPES,
|
|
||||||
PIN_DATA_NODE_TYPES_DENYLIST,
|
|
||||||
STICKY_NODE_TYPE,
|
|
||||||
} from '@/constants';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue';
|
import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue';
|
||||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
|
||||||
import type { INode, INodeTypeDescription } from 'n8n-workflow';
|
import type { INode, INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { getMousePosition } from '../utils/nodeViewUtils';
|
import { getMousePosition } from '../utils/nodeViewUtils';
|
||||||
import { useI18n } from './useI18n';
|
import { useI18n } from './useI18n';
|
||||||
import { useDataSchema } from './useDataSchema';
|
import { usePinnedData } from './usePinnedData';
|
||||||
|
|
||||||
export type ContextMenuTarget =
|
export type ContextMenuTarget =
|
||||||
| { source: 'canvas' }
|
| { source: 'canvas' }
|
||||||
|
@ -47,7 +42,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const sourceControlStore = useSourceControlStore();
|
const sourceControlStore = useSourceControlStore();
|
||||||
const { getInputDataWithPinned } = useDataSchema();
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const isReadOnly = computed(
|
const isReadOnly = computed(
|
||||||
|
@ -83,13 +78,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
return canAddNodeOfType(nodeType);
|
return canAddNodeOfType(nodeType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canPinNode = (node: INode): boolean => {
|
|
||||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
||||||
const dataToPin = getInputDataWithPinned(node);
|
|
||||||
if (!nodeType || dataToPin.length === 0) return false;
|
|
||||||
return nodeType.outputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(node.type);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasPinData = (node: INode): boolean => {
|
const hasPinData = (node: INode): boolean => {
|
||||||
return !!workflowsStore.pinDataByNodeName(node.name);
|
return !!workflowsStore.pinDataByNodeName(node.name);
|
||||||
};
|
};
|
||||||
|
@ -159,16 +147,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
...selectionActions,
|
...selectionActions,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const nonMainInputs = (node: INode) => {
|
|
||||||
const workflow = workflowsStore.getCurrentWorkflow();
|
|
||||||
const workflowNode = workflow.getNode(node.name);
|
|
||||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
||||||
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, nodeType!);
|
|
||||||
const inputNames = NodeHelpers.getConnectionTypes(inputs);
|
|
||||||
|
|
||||||
return !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main);
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuActions: IActionDropdownItem[] = [
|
const menuActions: IActionDropdownItem[] = [
|
||||||
!onlyStickies && {
|
!onlyStickies && {
|
||||||
id: 'toggle_activation',
|
id: 'toggle_activation',
|
||||||
|
@ -184,7 +162,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
? i18n.baseText('contextMenu.unpin', i18nOptions)
|
? i18n.baseText('contextMenu.unpin', i18nOptions)
|
||||||
: i18n.baseText('contextMenu.pin', i18nOptions),
|
: i18n.baseText('contextMenu.pin', i18nOptions),
|
||||||
shortcut: { keys: ['p'] },
|
shortcut: { keys: ['p'] },
|
||||||
disabled: nodes.some(nonMainInputs) || isReadOnly.value || !nodes.every(canPinNode),
|
disabled: isReadOnly.value || !nodes.every((n) => usePinnedData(n).canPinNode(true)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'copy',
|
id: 'copy',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { INodeExecutionData, IPinData } from 'n8n-workflow';
|
import type { INodeExecutionData, IPinData } from 'n8n-workflow';
|
||||||
import { jsonParse, jsonStringify } from 'n8n-workflow';
|
import { jsonParse, jsonStringify, NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
MAX_EXPECTED_REQUEST_SIZE,
|
MAX_EXPECTED_REQUEST_SIZE,
|
||||||
MAX_PINNED_DATA_SIZE,
|
MAX_PINNED_DATA_SIZE,
|
||||||
|
@ -18,6 +18,8 @@ import { computed, unref } from 'vue';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useNodeType } from '@/composables/useNodeType';
|
import { useNodeType } from '@/composables/useNodeType';
|
||||||
|
import { useDataSchema } from './useDataSchema';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
export type PinDataSource =
|
export type PinDataSource =
|
||||||
| 'pin-icon-click'
|
| 'pin-icon-click'
|
||||||
|
@ -47,6 +49,7 @@ export function usePinnedData(
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
const { getInputDataWithPinned } = useDataSchema();
|
||||||
|
|
||||||
const { pushRef } = storeToRefs(rootStore);
|
const { pushRef } = storeToRefs(rootStore);
|
||||||
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
|
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
|
||||||
|
@ -73,6 +76,26 @@ export function usePinnedData(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function canPinNode(checkDataEmpty = false) {
|
||||||
|
const targetNode = unref(node);
|
||||||
|
if (targetNode === null) return false;
|
||||||
|
|
||||||
|
const nodeType = useNodeTypesStore().getNodeType(targetNode.type, targetNode.typeVersion);
|
||||||
|
const dataToPin = getInputDataWithPinned(targetNode);
|
||||||
|
|
||||||
|
if (!nodeType || (checkDataEmpty && dataToPin.length === 0)) return false;
|
||||||
|
|
||||||
|
const workflow = workflowsStore.getCurrentWorkflow();
|
||||||
|
const outputs = NodeHelpers.getNodeOutputs(workflow, targetNode, nodeType);
|
||||||
|
const mainOutputs = outputs.filter((output) =>
|
||||||
|
typeof output === 'string'
|
||||||
|
? output === NodeConnectionType.Main
|
||||||
|
: output.type === NodeConnectionType.Main,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mainOutputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(targetNode.type);
|
||||||
|
}
|
||||||
|
|
||||||
function isValidJSON(data: string): boolean {
|
function isValidJSON(data: string): boolean {
|
||||||
try {
|
try {
|
||||||
JSON.parse(data);
|
JSON.parse(data);
|
||||||
|
@ -246,6 +269,7 @@ export function usePinnedData(
|
||||||
data,
|
data,
|
||||||
hasData,
|
hasData,
|
||||||
isValidNodeType,
|
isValidNodeType,
|
||||||
|
canPinNode,
|
||||||
setData,
|
setData,
|
||||||
onSetDataSuccess,
|
onSetDataSuccess,
|
||||||
onSetDataError,
|
onSetDataError,
|
||||||
|
|
Loading…
Reference in a new issue