mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Add data pinning functionality (#3511)
* feat: Design system color improvements and button component redesign. * feat: Added button focus state and unit tests. * refactor: Aligned n8n-button usage inside of editor-ui. * test: Updated snapshots. * refactor: Extracted focus outline width into scss variable. * fix: Fixed select input border-radius. * refactor: Removed element-ui references in button. * fix: Fixed scss variable imports. * feat: Added color-neutral variable story. * fix: Fixed color-secondary variable definition. * feat: Added color-white story. * test: Updated button snapshot. * feat: Replaced zoom buttons with new n8n-icon-button. * feat: Added stories for float utilities. * chore: Updated color shades generation code for later use. * chore: Removed color-white code. * chore: Updated story properties for button components. * fix: Added el-button fallback for places where el-button is not replaceable (messagebox). * feat: Reverted to css modules. Replaced el-button with n8n-button at application level. * test: Updated button snapshot. * fix: Fixed element-ui locally referenced buttons (via components: {}). * fix: Updated colors. Removed irrelevant validation. Added ElButton override component. * test: Updated button override snapshot. * fix: Various button adjustments and fixes. * fix: Updated button disabled state. * test: Updated snapshots. * fix: Consolidated css variables changes. * Data pinning (#3512) * refactor: Aligned n8n-button usage inside of editor-ui. * feat: Added edit data button on json hover. * feat: Extracted code editor into separate form component. * feat: Added edit data button on json hover. * feat: Added pinData and edit mode methods. * 🔥 Remove conflict markers * ✏️ Update i18n keys * ⚡ Add JSON validation * 🗃️ Add `pinData` column to `workflow_entity` * 📘 Tighten type * ⚡ Make `pinData` column nullable * ⚡ Adjust workflow endpoints for pin data * 📘 Improve types * ✏️ Improve wording * Inject pindata into items flow (#3420) * ⚡ Inject pin data - Second approach * 🔥 Remove unneeded lint exception * feat: Added edit data button on json hover. * feat: Extracted code editor into separate form component. * feat: Added edit data button on json hover. * fix: Fixed rebase conflicts. * ⏪ Undo button change * 🐛 Fix runNode call Adjust per update inbdb84130d6
* 🧪 Fix workflow tests * 🐛 More merge conflict fixes * feat: Added pin/unpin button and store mutations. * feat: Size check. Various design and ux improvements. * ⚡ Add transformer * ⚡ Hoist pin data * ⚡ Adjust endpoints for hoisted pin data * 📘 Expand interface * 🐛 Fix stray array * 👕 Fix build * 👕 Add lint exception * 👕 Fix header * 🎨 Add color secondary tints * ✨ Create `HeaderMessage` component * ⚡ Adjust `InfoTip` component * ✨ Add `HeaderMessage` to `RunData` * 🐛 Fix console error * 👕 Fix lint * ⚡ Consolidate `HeaderMessage` and `Callout` * ⏪ Undo `InfoTip` changes * 🔥 Remove duplicate icons * ⚡ Simplify template * 🎨 Change cursor for action text * 👕 Fix lint * ⚡ Add URL * 🐛 Fix handler name * ⚡ Use constant * ♻️ Refactor per feedback * fix: Various fixes after data pinning relocation. * fix: Added store mutation for setting pinned data. * feat: Added pinned state for workflow canvas node. * fix: Fixed workflow saving. * fix: Removed pinData hoisting (no longer necessary). * feat: Added canPinData flag to hide for input pane and binary data. Fixed unpin and execute flow. * ⚡ Fixes for canvas pin data (#3587) * ⚡ Fixes for canvas pin data * 📘 Rename type * 🧪 Fix unrelated Public API test * 🔥 Remove logging * feat: Updated pinData mixin to no longer include extra fields. * ⚡ Output same pindata for every run * 🎨 Fix cropping * 🔥 Remove unrelated logging * feat: Moved edit button next to pin button. * feat: Changed data to be inserted for empty state. * chore: Changed invalid editor output translation. * feat: Added error line reporting on JSON Validation. * feat: Migrated pinData edit mode to store. * chore: Merged duplicate node border color condition. * feat: Moved pin data validation to mixin. Added check before closing ndv modal. * fix: Changed pinned data size calculation to discard active node pin data. * feat: Added support for rename and delete node with pin data. * feat: Simplified editing state. Fixed edit mode in input panel after store migration. * feat: Various data pinning improvements. * fix: Fixed callout link underline. * refactor: Added support for both string and objects for data size check. * feat: Added disabled node check for input panel. Fixed monaco editor resizing. * fix: Fixed edit mode footer size. * ⚡ Fix pindata items per run * 👕 Remove unneeded exception * refactor: Added isValidPinData() helper method. * refactor: Changed how string size in bytes in calculated.g * refactor: Updated pinData mixin interface. * refactor: Merged filter and reduce in pinDataSize calculation. * fix: Changed code-editor to correct type. * fix: Added insert test data message to trigger nodes. * feat: Disabled data pinning for multiple output nodes. * refactor: Updated ndv.input.disabled translation to include node name. * refactor: Aligned n8n-button usage inside of editor-ui. * feat: Added edit data button on json hover. * feat: Extracted code editor into separate form component. * feat: Added edit data button on json hover. * feat: Added pinData and edit mode methods. * 🔥 Remove conflict markers * ✏️ Update i18n keys * ⚡ Add JSON validation * 🗃️ Add `pinData` column to `workflow_entity` * 📘 Tighten type * ⚡ Make `pinData` column nullable * ⚡ Adjust workflow endpoints for pin data * 📘 Improve types * ✏️ Improve wording * Inject pindata into items flow (#3420) * ⚡ Inject pin data - Second approach * 🔥 Remove unneeded lint exception * feat: Added edit data button on json hover. * feat: Extracted code editor into separate form component. * feat: Added edit data button on json hover. * fix: Fixed rebase conflicts. * ⏪ Undo button change * 🐛 Fix runNode call Adjust per update inbdb84130d6
* 🧪 Fix workflow tests * 🐛 More merge conflict fixes * feat: Added pin/unpin button and store mutations. * feat: Size check. Various design and ux improvements. * ⚡ Add transformer * ⚡ Hoist pin data * ⚡ Adjust endpoints for hoisted pin data * 📘 Expand interface * 🐛 Fix stray array * 👕 Fix build * 🎨 Add color secondary tints * ✨ Create `HeaderMessage` component * ⚡ Adjust `InfoTip` component * ✨ Add `HeaderMessage` to `RunData` * 🐛 Fix console error * 👕 Fix lint * ⚡ Consolidate `HeaderMessage` and `Callout` * ⏪ Undo `InfoTip` changes * 🔥 Remove duplicate icons * ⚡ Simplify template * 🎨 Change cursor for action text * 👕 Fix lint * ⚡ Add URL * 🐛 Fix handler name * ⚡ Use constant * ♻️ Refactor per feedback * fix: Various fixes after data pinning relocation. * fix: Added store mutation for setting pinned data. * feat: Added pinned state for workflow canvas node. * ⚡ Fixes for canvas pin data (#3587) * ⚡ Fixes for canvas pin data * 📘 Rename type * 🧪 Fix unrelated Public API test * 🔥 Remove logging * feat: Updated pinData mixin to no longer include extra fields. * fix: Removed pinData hoisting (no longer necessary). * chore: Merged duplicate node border color condition. * ⚡ Output same pindata for every run * 🎨 Fix cropping * 🐛 Fix excess closing template tag * fix: Removed rogue template tag after merge. * fix: Fixed code-editor resizing when moving ndv panel. * feat: Added node duplication pin data. * ⚡ Implement telemetry * ♻️ Add clarifications from call * fix: Fixed run data header height. * feat: Removed border from pin data callout. * feat: Added line-break before 'or insert pin data'. * feat: Changed enterEditMode to always insert test data if there's no execution data. * feat: Removed copy output tooltip. * feat: Removed unpin tooltip. * fix: Removed thumbtack icon rotation. * fix: Removed run info from Edit Output title. * feat: Hid edit and pin buttons when editing. * feat: Updated monaco code-editor padding and borders. * feat: Progress on pinData error message format * feat: Updated copy feature to work without any selected value. * feat: Moved save and cancel buttons. Cleared notifications on save. * feat: Changed pin data beforeClosing confirm text. * feat: Closing ndv when discarding or saving pindata on close. * feat: Added split in batches node to pin data denylist. * fix: Added missing margin-bottom to webhook node. * feat: Moved thumbtack icon to the right, replacing the checkmark. * fix: Hid pagination while editing. * feat: Added pin data discovery flow. * feat: Changed pin data discovery flow to avoid tooltip glitching. * fix: Changed copy selection to copy all input data. * feat: Updated pin data validation error message for unexpected single quotes. * fix: Replaced :manual='true' prop with manual shorthand. * fix: Removed unused variable. * chore: Renamed translation key to node.discovery.pinData. * refactor: Extracted isPinDataNodeType to pinData mixin. * fix: Updated watch condition to improve performance. * refactor: Renamed some pin data variables and methods as per review. * fix: Added partial translation for JSON.parse pin data error messages. * chore: Temporarily disabled failing unit test. * 🧪 Fix data pinning workflow retrieval test * 🔥 Remove unused imports * 🔥 Remove leftover line * ⚡ Skip pindata node issues on BE * ⚡ Skip pindata node issues on FE * ⚡ Hide `RunInfo` for pindata node * ⚡ Hide purple banner in edit output mode * feat: Updated data pinning discoverability flow. * fix: Fixed paginated data pinning. * fix: Disabled pin data in read only mode. * 🐛 Fix runtime error with non-array * fix: Loading pin data when opening execution. * ⚡ Adjust stale data warning for pinned data * ⚡ Skip auth in endpoint * ⚡ Mark start node for pinned trigger * ✏️ Comment on passthrough * 🔥 Remove comment * Final pindata metrics changes (#3673) * 🐛 Fix `pinData` tracked as `0` * ⚡ Add `is_pinned` to `nodesGraph` * 📘 Extend `IWorkflowBase` * ⚡ Handle `pinData` being `undefined` * ⚡ Add `data_pinning_tooltip_presented` * ♻️ Refactor to remove circular dependency * fix: Added pin data handling when importing workflow. (#3698) * 🔥 Remove helper from WorkflowExecute * ⚡ Add logic for single pinned trigger * 👕 Remove lint exception * fix: Added pin data handling in importWorkflowExact. * N8N-4077 data pinning discoverability part 2 (#3701) * fix: Fixed pin data discovery tooltip position when moving canvas. * feat: Updated data pinning discovery tooltip copy. * Fix data pinning build (#3702) * ⚡ Disable edit button for disabled node * ⚡ Ensure disabled pinned nodes are passthrough * 🐛 Fix JSON key unfurling in edit mode * ⚡ Improve implementation * 🐛 Fix console error * fix: Fixed copying pinned output data. (#3715) * Fix pinning for webhook responding with output from last node (#3719) * fix: Fixed entering edit mode after refresh. * fix: Fixed type error during build. * fix: RunData import formatting. * chore: Updated pin data types. * fix: Added missing type to stringSizeInBytes. Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * fix: Showing pin data without executing the node only in output pane. * fix: Updated no data message when previous node not executed. * feat: Added expression input and evaluation for pin data nodes without execution. * chore: Fixed linting issues and removed remnant console.log(). * chore: Undone package-lock changes. * fix: Removed pin data store changes. * fix: Created a new object using vuex runExecutionData. * fix: Fixed bug appearing when adding a new node after executing. * fix: Fix editor-ui build * feat: Added green node connectors when having pin data output. * chore: Fixed linting errors. * fix: Added pin data eventBus unsubscribe. * fix: Added pin data color check after adding a connection. * 🎨 Add pindata styles Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
parent
fb67543b2f
commit
15693b0056
|
@ -15,6 +15,7 @@ import {
|
|||
ITelemetrySettings,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||
PinData,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -688,6 +689,7 @@ export interface IWorkflowExecutionDataProcess {
|
|||
executionMode: WorkflowExecuteMode;
|
||||
executionData?: IRunExecutionData;
|
||||
runData?: IRunData;
|
||||
pinData?: PinData;
|
||||
retryOf?: number | string;
|
||||
sessionId?: string;
|
||||
startNodes?: string[];
|
||||
|
|
|
@ -74,6 +74,7 @@ import {
|
|||
IWorkflowBase,
|
||||
LoggerProxy,
|
||||
NodeHelpers,
|
||||
PinData,
|
||||
WebhookHttpMethod,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -130,6 +131,7 @@ import {
|
|||
WorkflowRunner,
|
||||
getCredentialForUser,
|
||||
getCredentialWithoutUser,
|
||||
IWorkflowDb,
|
||||
} from '.';
|
||||
|
||||
import config from '../config';
|
||||
|
@ -157,9 +159,9 @@ import type {
|
|||
} from './requests';
|
||||
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers';
|
||||
import { ExecutionEntity } from './databases/entities/ExecutionEntity';
|
||||
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
||||
import { credentialsController } from './api/credentials.api';
|
||||
import { workflowsController } from './api/workflows.api';
|
||||
import { nodesController } from './api/nodes.api';
|
||||
import { oauth2CredentialController } from './api/oauth2Credential.api';
|
||||
import {
|
||||
|
@ -168,6 +170,7 @@ import {
|
|||
isUserManagementEnabled,
|
||||
} from './UserManagement/UserManagementHelper';
|
||||
import { loadPublicApiVersions } from './PublicApi';
|
||||
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -769,74 +772,6 @@ class App {
|
|||
// Workflow
|
||||
// ----------------------------------------
|
||||
|
||||
// Creates a new workflow
|
||||
this.app.post(
|
||||
`/${this.restEndpoint}/workflows`,
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Create) => {
|
||||
delete req.body.id; // delete if sent
|
||||
|
||||
const newWorkflow = new WorkflowEntity();
|
||||
|
||||
Object.assign(newWorkflow, req.body);
|
||||
|
||||
await validateEntity(newWorkflow);
|
||||
|
||||
await this.externalHooks.run('workflow.create', [newWorkflow]);
|
||||
|
||||
const { tags: tagIds } = req.body;
|
||||
|
||||
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
|
||||
newWorkflow.tags = await Db.collections.Tag.findByIds(tagIds, {
|
||||
select: ['id', 'name'],
|
||||
});
|
||||
}
|
||||
|
||||
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||
|
||||
let savedWorkflow: undefined | WorkflowEntity;
|
||||
|
||||
await getConnection().transaction(async (transactionManager) => {
|
||||
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
const role = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
});
|
||||
|
||||
const newSharedWorkflow = new SharedWorkflow();
|
||||
|
||||
Object.assign(newSharedWorkflow, {
|
||||
role,
|
||||
user: req.user,
|
||||
workflow: savedWorkflow,
|
||||
});
|
||||
|
||||
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
|
||||
});
|
||||
|
||||
if (!savedWorkflow) {
|
||||
LoggerProxy.error('Failed to create workflow', { userId: req.user.id });
|
||||
throw new ResponseHelper.ResponseError('Failed to save workflow');
|
||||
}
|
||||
|
||||
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
|
||||
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
|
||||
requestOrder: tagIds,
|
||||
});
|
||||
}
|
||||
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Reads and returns workflow data from an URL
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/workflows/from-url`,
|
||||
|
@ -962,50 +897,6 @@ class App {
|
|||
}),
|
||||
);
|
||||
|
||||
// Returns a specific workflow
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/workflows/:id`,
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Get) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
let relations = ['workflow', 'workflow.tags'];
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
relations = relations.filter((relation) => relation !== 'workflow.tags');
|
||||
}
|
||||
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
relations,
|
||||
where: whereClause({
|
||||
user: req.user,
|
||||
entityType: 'workflow',
|
||||
entityId: workflowId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to access a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Workflow with ID "${workflowId}" could not be found.`,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
workflow: { id, ...rest },
|
||||
} = shared;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Updates an existing workflow
|
||||
this.app.patch(
|
||||
`/${this.restEndpoint}/workflows/:id`,
|
||||
|
@ -1204,6 +1095,7 @@ class App {
|
|||
): Promise<IExecutionPushResponse> => {
|
||||
const { workflowData } = req.body;
|
||||
const { runData } = req.body;
|
||||
const { pinData } = req.body;
|
||||
const { startNodes } = req.body;
|
||||
const { destinationNode } = req.body;
|
||||
const executionMode = 'manual';
|
||||
|
@ -1211,12 +1103,15 @@ class App {
|
|||
|
||||
const sessionId = GenericHelpers.getSessionId(req);
|
||||
|
||||
const pinnedTrigger = findFirstPinnedTrigger(workflowData, pinData);
|
||||
|
||||
// If webhooks nodes exist and are active we have to wait for till we receive a call
|
||||
if (
|
||||
runData === undefined ||
|
||||
startNodes === undefined ||
|
||||
startNodes.length === 0 ||
|
||||
destinationNode === undefined
|
||||
pinnedTrigger === undefined &&
|
||||
(runData === undefined ||
|
||||
startNodes === undefined ||
|
||||
startNodes.length === 0 ||
|
||||
destinationNode === undefined)
|
||||
) {
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
||||
const nodeTypes = NodeTypes();
|
||||
|
@ -1254,11 +1149,17 @@ class App {
|
|||
destinationNode,
|
||||
executionMode,
|
||||
runData,
|
||||
pinData,
|
||||
sessionId,
|
||||
startNodes,
|
||||
workflowData,
|
||||
userId: req.user.id,
|
||||
};
|
||||
|
||||
if (pinnedTrigger) {
|
||||
data.startNodes = [pinnedTrigger.name];
|
||||
}
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
const executionId = await workflowRunner.run(data);
|
||||
|
||||
|
@ -1269,6 +1170,8 @@ class App {
|
|||
),
|
||||
);
|
||||
|
||||
this.app.use(`/${this.restEndpoint}/workflows`, workflowsController);
|
||||
|
||||
// Retrieves all tags, with or without usage count
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/tags`,
|
||||
|
@ -2927,3 +2830,20 @@ function isOAuth(credType: ICredentialType) {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
const TRIGGER_NODE_SUFFIXES = ['trigger', 'webhook'];
|
||||
|
||||
const isTrigger = (str: string) =>
|
||||
TRIGGER_NODE_SUFFIXES.some((suffix) => str.toLowerCase().includes(suffix));
|
||||
|
||||
function findFirstPinnedTrigger(workflow: IWorkflowDb, pinData?: PinData) {
|
||||
if (!pinData) return;
|
||||
|
||||
const firstPinnedTriggerName = Object.keys(pinData).find(isTrigger);
|
||||
|
||||
if (!firstPinnedTriggerName) return;
|
||||
|
||||
return workflow.nodes.find(
|
||||
({ type, name }) => isTrigger(type) && name === firstPinnedTriggerName,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -485,6 +485,10 @@ export async function executeWebhook(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (workflowData.pinData) {
|
||||
data.data.resultData.pinData = workflowData.pinData;
|
||||
}
|
||||
|
||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||
if (data.data.resultData.error || returnData?.error !== undefined) {
|
||||
if (!didSendResponse) {
|
||||
|
|
|
@ -50,7 +50,7 @@ const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
|||
* @returns {(ITaskData | undefined)}
|
||||
*/
|
||||
export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined {
|
||||
const { runData } = inputData.data.resultData;
|
||||
const { runData, pinData = {} } = inputData.data.resultData;
|
||||
const { lastNodeExecuted } = inputData.data.resultData;
|
||||
|
||||
if (lastNodeExecuted === undefined) {
|
||||
|
@ -61,7 +61,26 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
|
||||
const lastNodeRunData = runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
|
||||
|
||||
let lastNodePinData = pinData[lastNodeExecuted];
|
||||
|
||||
if (lastNodePinData) {
|
||||
if (!Array.isArray(lastNodePinData)) lastNodePinData = [lastNodePinData];
|
||||
|
||||
const itemsPerRun = lastNodePinData.map((item, index) => {
|
||||
return { json: item, pairedItem: { item: index } };
|
||||
});
|
||||
|
||||
return {
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: { main: [itemsPerRun] },
|
||||
source: lastNodeRunData.source,
|
||||
};
|
||||
}
|
||||
|
||||
return lastNodeRunData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -310,7 +310,12 @@ export class WorkflowRunner {
|
|||
|
||||
// Can execute without webhook so go on
|
||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
|
||||
workflowExecution = workflowExecute.run(
|
||||
workflow,
|
||||
undefined,
|
||||
data.destinationNode,
|
||||
data.pinData,
|
||||
);
|
||||
} else {
|
||||
Logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
|
||||
// Execute only the nodes between start and destination nodes
|
||||
|
@ -320,6 +325,7 @@ export class WorkflowRunner {
|
|||
data.runData,
|
||||
data.startNodes,
|
||||
data.destinationNode,
|
||||
data.pinData,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -345,9 +345,22 @@ export class WorkflowRunnerProcess {
|
|||
) {
|
||||
// Execute all nodes
|
||||
|
||||
let startNode;
|
||||
if (
|
||||
this.data.startNodes?.length === 1 &&
|
||||
Object.keys(this.data.pinData ?? {}).includes(this.data.startNodes[0])
|
||||
) {
|
||||
startNode = this.workflow.getNode(this.data.startNodes[0]) ?? undefined;
|
||||
}
|
||||
|
||||
// Can execute without webhook so go on
|
||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
||||
return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode);
|
||||
return this.workflowExecute.run(
|
||||
this.workflow,
|
||||
startNode,
|
||||
this.data.destinationNode,
|
||||
this.data.pinData,
|
||||
);
|
||||
}
|
||||
// Execute only the nodes between start and destination nodes
|
||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
|
||||
|
@ -356,6 +369,7 @@ export class WorkflowRunnerProcess {
|
|||
this.data.runData,
|
||||
this.data.startNodes,
|
||||
this.data.destinationNode,
|
||||
this.data.pinData,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
134
packages/cli/src/api/workflows.api.ts
Normal file
134
packages/cli/src/api/workflows.api.ts
Normal file
|
@ -0,0 +1,134 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable import/no-cycle */
|
||||
|
||||
import express from 'express';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { Db, ResponseHelper, whereClause, WorkflowHelpers } from '..';
|
||||
import config from '../../config';
|
||||
import * as TagHelpers from '../TagHelpers';
|
||||
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
|
||||
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
||||
import { validateEntity } from '../GenericHelpers';
|
||||
import { InternalHooksManager } from '../InternalHooksManager';
|
||||
import { externalHooks } from '../Server';
|
||||
import type { WorkflowRequest } from '../requests';
|
||||
|
||||
export const workflowsController = express.Router();
|
||||
|
||||
/**
|
||||
* POST /workflows
|
||||
*/
|
||||
workflowsController.post(
|
||||
'/',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Create) => {
|
||||
delete req.body.id; // delete if sent
|
||||
|
||||
const newWorkflow = new WorkflowEntity();
|
||||
|
||||
Object.assign(newWorkflow, req.body);
|
||||
|
||||
await validateEntity(newWorkflow);
|
||||
|
||||
await externalHooks.run('workflow.create', [newWorkflow]);
|
||||
|
||||
const { tags: tagIds } = req.body;
|
||||
|
||||
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
|
||||
newWorkflow.tags = await Db.collections.Tag.findByIds(tagIds, {
|
||||
select: ['id', 'name'],
|
||||
});
|
||||
}
|
||||
|
||||
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||
|
||||
let savedWorkflow: undefined | WorkflowEntity;
|
||||
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
const role = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
});
|
||||
|
||||
const newSharedWorkflow = new SharedWorkflow();
|
||||
|
||||
Object.assign(newSharedWorkflow, {
|
||||
role,
|
||||
user: req.user,
|
||||
workflow: savedWorkflow,
|
||||
});
|
||||
|
||||
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
|
||||
});
|
||||
|
||||
if (!savedWorkflow) {
|
||||
LoggerProxy.error('Failed to create workflow', { userId: req.user.id });
|
||||
throw new ResponseHelper.ResponseError('Failed to save workflow');
|
||||
}
|
||||
|
||||
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
|
||||
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
|
||||
requestOrder: tagIds,
|
||||
});
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /workflows/:id
|
||||
*/
|
||||
workflowsController.get(
|
||||
'/:id',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Get) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
let relations = ['workflow', 'workflow.tags'];
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
relations = relations.filter((relation) => relation !== 'workflow.tags');
|
||||
}
|
||||
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
relations,
|
||||
where: whereClause({
|
||||
user: req.user,
|
||||
entityType: 'workflow',
|
||||
entityId: workflowId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to access a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Workflow with ID "${workflowId}" could not be found.`,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
workflow: { id, ...rest },
|
||||
} = shared;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { Length } from 'class-validator';
|
||||
|
||||
import { IConnections, IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
|
||||
import { IConnections, IDataObject, INode, IWorkflowSettings, PinData } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
BeforeUpdate,
|
||||
|
@ -22,7 +22,7 @@ import * as config from '../../../config';
|
|||
import { DatabaseType, IWorkflowDb } from '../..';
|
||||
import { TagEntity } from './TagEntity';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { objectRetriever } from '../utils/transformers';
|
||||
import { objectRetriever, serializer } from '../utils/transformers';
|
||||
|
||||
function resolveDataType(dataType: string) {
|
||||
const dbType = config.getEnv('database.type');
|
||||
|
@ -117,6 +117,13 @@ export class WorkflowEntity implements IWorkflowDb {
|
|||
@OneToMany(() => SharedWorkflow, (sharedWorkflow) => sharedWorkflow.workflow)
|
||||
shared: SharedWorkflow[];
|
||||
|
||||
@Column({
|
||||
type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json',
|
||||
nullable: true,
|
||||
transformer: serializer,
|
||||
})
|
||||
pinData: PinData;
|
||||
|
||||
@BeforeUpdate()
|
||||
setUpdateDate() {
|
||||
this.updatedAt = new Date();
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
import config from '../../../../config';
|
||||
|
||||
export class IntroducePinData1654090101303 implements MigrationInterface {
|
||||
name = 'IntroducePinData1654090101303';
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
logMigrationStart(this.name);
|
||||
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`${tablePrefix}workflow_entity\` ADD \`pinData\` json`,
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN \`pinData\``);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEm
|
|||
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||
import { CommunityNodes1652254514003 } from './1652254514003-CommunityNodes';
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654090101303 } from './1654090101303-IntroducePinData';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -36,4 +37,5 @@ export const mysqlMigrations = [
|
|||
AddUserSettings1652367743993,
|
||||
CommunityNodes1652254514003,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654090101303,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
import config from '../../../../config';
|
||||
|
||||
export class IntroducePinData1654090467022 implements MigrationInterface {
|
||||
name = 'IntroducePinData1654090467022';
|
||||
|
||||
async up(queryRunner: QueryRunner) {
|
||||
logMigrationStart(this.name);
|
||||
|
||||
const schema = config.getEnv('database.postgresdb.schema');
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`SET search_path TO ${schema}`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE ${schema}.${tablePrefix}workflow_entity ADD "pinData" json`,
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner) {
|
||||
const schema = config.getEnv('database.postgresdb.schema');
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`SET search_path TO ${schema}`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE ${schema}.${tablePrefix}workflow_entity DROP COLUMN "pinData"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEm
|
|||
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||
import { CommunityNodes1652254514002 } from './1652254514002-CommunityNodes';
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654090467022 } from './1654090467022-IntroducePinData';
|
||||
|
||||
export const postgresMigrations = [
|
||||
InitialMigration1587669153312,
|
||||
|
@ -32,4 +33,5 @@ export const postgresMigrations = [
|
|||
AddUserSettings1652367743993,
|
||||
CommunityNodes1652254514002,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654090467022,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
import config from '../../../../config';
|
||||
|
||||
export class IntroducePinData1654089251344 implements MigrationInterface {
|
||||
name = 'IntroducePinData1654089251344';
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
logMigrationStart(this.name);
|
||||
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "pinData" text`,
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`ALTER TABLE \`${tablePrefix}workflow_entity\` RENAME TO "temporary_workflow_entity"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE \`${tablePrefix}workflow_entity\` (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO \`${tablePrefix}workflow_entity\` ("id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData") SELECT "id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData" FROM "temporary_workflow_entity"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_workflow_entity"`);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEm
|
|||
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';
|
||||
import { CommunityNodes1652254514001 } from './1652254514001-CommunityNodes'
|
||||
import { AddAPIKeyColumn1652905585850 } from './1652905585850-AddAPIKeyColumn';
|
||||
import { IntroducePinData1654089251344 } from './1654089251344-IntroducePinData';
|
||||
|
||||
const sqliteMigrations = [
|
||||
InitialMigration1588102412422,
|
||||
|
@ -30,6 +31,7 @@ const sqliteMigrations = [
|
|||
AddUserSettings1652367743993,
|
||||
CommunityNodes1652254514001,
|
||||
AddAPIKeyColumn1652905585850,
|
||||
IntroducePinData1654089251344,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
@ -18,3 +18,13 @@ export const objectRetriever: ValueTransformer = {
|
|||
from: (value: string | object): object =>
|
||||
typeof value === 'string' ? (JSON.parse(value) as object) : value,
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformer to store object as string and retrieve string as object.
|
||||
*/
|
||||
export const serializer: ValueTransformer = {
|
||||
to: (value: object | string): string =>
|
||||
typeof value === 'object' ? JSON.stringify(value) : value,
|
||||
from: (value: string | object): object =>
|
||||
typeof value === 'string' ? (JSON.parse(value) as object) : value,
|
||||
};
|
||||
|
|
2
packages/cli/src/requests.d.ts
vendored
2
packages/cli/src/requests.d.ts
vendored
|
@ -8,6 +8,7 @@ import {
|
|||
INodeCredentialTestRequest,
|
||||
IRunData,
|
||||
IWorkflowSettings,
|
||||
PinData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { User } from './databases/entities/User';
|
||||
|
@ -71,6 +72,7 @@ export declare namespace WorkflowRequest {
|
|||
{
|
||||
workflowData: IWorkflowDb;
|
||||
runData: IRunData;
|
||||
pinData: PinData;
|
||||
startNodes?: string[];
|
||||
destinationNode?: string;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,10 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['SharedWorkflow', 'User', 'Workflow'], testDbName);
|
||||
await testDb.truncate(
|
||||
['SharedCredentials', 'SharedWorkflow', 'Tag', 'User', 'Workflow', 'Credentials'],
|
||||
testDbName,
|
||||
);
|
||||
|
||||
config.set('userManagement.disabled', false);
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
|
|
|
@ -201,6 +201,7 @@ export async function truncate(collections: Array<CollectionName>, testDbName: s
|
|||
|
||||
const truncationPromises = collections.map((collection) => {
|
||||
const tableName = toTableName(collection);
|
||||
Db.collections[collection].clear();
|
||||
return testDb.query(
|
||||
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
|
||||
);
|
||||
|
@ -223,7 +224,6 @@ export async function truncate(collections: Array<CollectionName>, testDbName: s
|
|||
}
|
||||
|
||||
return await truncateMappingTables(dbType, collections, testDb);
|
||||
// return Promise.resolve([])
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,6 +17,7 @@ type EndpointGroup =
|
|||
| 'owner'
|
||||
| 'passwordReset'
|
||||
| 'credentials'
|
||||
| 'workflows'
|
||||
| 'publicApi'
|
||||
| 'nodes';
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ import type {
|
|||
TriggerTime,
|
||||
} from './types';
|
||||
import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
||||
import { workflowsController } from '../../../src/api/workflows.api';
|
||||
import { nodesController } from '../../../src/api/nodes.api';
|
||||
import { randomName } from './random';
|
||||
|
||||
|
@ -96,17 +97,18 @@ export async function initTestServer({
|
|||
|
||||
if (routerEndpoints.length) {
|
||||
const { apiRouters } = await loadPublicApiVersions(testServer.publicApiEndpoint);
|
||||
const map: Record<string, express.Router | express.Router[]> = {
|
||||
credentials: credentialsController,
|
||||
nodes: nodesController,
|
||||
publicApi: apiRouters,
|
||||
const map: Record<string, express.Router | express.Router[] | any> = {
|
||||
credentials: { controller: credentialsController, path: 'credentials' },
|
||||
workflows: { controller: workflowsController, path: 'workflows' },
|
||||
nodes: { controller: nodesController, path: 'nodes' },
|
||||
publicApi: apiRouters
|
||||
};
|
||||
|
||||
for (const group of routerEndpoints) {
|
||||
if (group === 'publicApi') {
|
||||
testServer.app.use(...(map[group] as express.Router[]));
|
||||
} else {
|
||||
testServer.app.use(`/${testServer.restEndpoint}/${group}`, map[group]);
|
||||
testServer.app.use(`/${testServer.restEndpoint}/${map[group].path}`, map[group].controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,8 +147,10 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
|
|||
const routerEndpoints: string[] = [];
|
||||
const functionEndpoints: string[] = [];
|
||||
|
||||
const ROUTER_GROUP = ['credentials', 'nodes', 'workflows', 'publicApi'];
|
||||
|
||||
endpointGroups.forEach((group) =>
|
||||
(['credentials', 'nodes', 'publicApi'].includes(group) ? routerEndpoints : functionEndpoints).push(group),
|
||||
(ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group),
|
||||
);
|
||||
|
||||
return [routerEndpoints, functionEndpoints];
|
||||
|
|
107
packages/cli/test/integration/workflows.api.test.ts
Normal file
107
packages/cli/test/integration/workflows.api.test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import express from 'express';
|
||||
|
||||
import * as utils from './shared/utils';
|
||||
import * as testDb from './shared/testDb';
|
||||
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import { PinData } from 'n8n-workflow';
|
||||
|
||||
jest.mock('../../src/telemetry');
|
||||
|
||||
let app: express.Application;
|
||||
let testDbName = '';
|
||||
let globalOwnerRole: Role;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await utils.initTestServer({
|
||||
endpointGroups: ['workflows'],
|
||||
applyAuth: true,
|
||||
});
|
||||
const initResult = await testDb.init();
|
||||
testDbName = initResult.testDbName;
|
||||
|
||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||
|
||||
utils.initTestLogger();
|
||||
utils.initTestTelemetry();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User', 'Workflow', 'SharedWorkflow'], testDbName);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate(testDbName);
|
||||
});
|
||||
|
||||
test('POST /workflows should store pin data for node in workflow', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
|
||||
const workflow = makeWorkflow({ withPinData: true });
|
||||
|
||||
const response = await authOwnerAgent.post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { pinData } = response.body.data as { pinData: PinData };
|
||||
|
||||
expect(pinData).toMatchObject({ Spotify: [{ myKey: 'myValue' }] });
|
||||
});
|
||||
|
||||
test('POST /workflows should set pin data to null if no pin data', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
|
||||
const workflow = makeWorkflow({ withPinData: false });
|
||||
|
||||
const response = await authOwnerAgent.post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { pinData } = response.body.data as { pinData: PinData };
|
||||
|
||||
expect(pinData).toBeNull();
|
||||
});
|
||||
|
||||
test('GET /workflows/:id should return pin data', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
|
||||
const workflow = makeWorkflow({ withPinData: true });
|
||||
|
||||
const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow);
|
||||
|
||||
const { id } = workflowCreationResponse.body.data as { id: string };
|
||||
|
||||
const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`);
|
||||
|
||||
expect(workflowRetrievalResponse.statusCode).toBe(200);
|
||||
|
||||
const { pinData } = workflowRetrievalResponse.body.data as { pinData: PinData };
|
||||
|
||||
expect(pinData).toMatchObject({ Spotify: [{ myKey: 'myValue' }] });
|
||||
});
|
||||
|
||||
function makeWorkflow({ withPinData }: { withPinData: boolean }) {
|
||||
const workflow = new WorkflowEntity();
|
||||
|
||||
workflow.name = 'My Workflow';
|
||||
workflow.active = false;
|
||||
workflow.connections = {};
|
||||
workflow.nodes = [
|
||||
{
|
||||
name: 'Spotify',
|
||||
type: 'n8n-nodes-base.spotify',
|
||||
parameters: { resource: 'track', operation: 'get', id: '123' },
|
||||
typeVersion: 1,
|
||||
position: [740, 240],
|
||||
},
|
||||
];
|
||||
|
||||
if (withPinData) {
|
||||
workflow.pinData = { Spotify: [{ myKey: 'myValue' }] };
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
|
@ -32,6 +32,7 @@ import {
|
|||
LoggerProxy as Logger,
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
PinData,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
WorkflowOperationError,
|
||||
|
@ -59,6 +60,7 @@ export class WorkflowExecute {
|
|||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
pinData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
|
@ -82,7 +84,12 @@ export class WorkflowExecute {
|
|||
// PCancelable to a regular Promise and does so not allow canceling
|
||||
// active executions anymore
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
|
||||
run(
|
||||
workflow: Workflow,
|
||||
startNode?: INode,
|
||||
destinationNode?: string,
|
||||
pinData?: PinData,
|
||||
): PCancelable<IRun> {
|
||||
// Get the nodes to start workflow execution from
|
||||
startNode = startNode || workflow.getStartNode(destinationNode);
|
||||
|
||||
|
@ -121,6 +128,7 @@ export class WorkflowExecute {
|
|||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
pinData,
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
|
@ -152,6 +160,7 @@ export class WorkflowExecute {
|
|||
runData: IRunData,
|
||||
startNodes: string[],
|
||||
destinationNode: string,
|
||||
pinData?: PinData,
|
||||
// @ts-ignore
|
||||
): PCancelable<IRun> {
|
||||
let incomingNodeConnections: INodeConnections | undefined;
|
||||
|
@ -258,6 +267,7 @@ export class WorkflowExecute {
|
|||
},
|
||||
resultData: {
|
||||
runData,
|
||||
pinData,
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
|
@ -683,7 +693,13 @@ export class WorkflowExecute {
|
|||
destinationNode = this.runExecutionData.startData.destinationNode;
|
||||
}
|
||||
|
||||
const workflowIssues = workflow.checkReadyForExecution({ startNode, destinationNode });
|
||||
const pinDataNodeNames = Object.keys(this.runExecutionData.resultData.pinData ?? {});
|
||||
|
||||
const workflowIssues = workflow.checkReadyForExecution({
|
||||
startNode,
|
||||
destinationNode,
|
||||
pinDataNodeNames,
|
||||
});
|
||||
if (workflowIssues !== null) {
|
||||
throw new Error(
|
||||
'The workflow has issues and can for that reason not be executed. Please fix them first.',
|
||||
|
@ -914,24 +930,37 @@ export class WorkflowExecute {
|
|||
}
|
||||
}
|
||||
|
||||
Logger.debug(`Running node "${executionNode.name}" started`, {
|
||||
node: executionNode.name,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const runNodeData = await workflow.runNode(
|
||||
executionData,
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
this.additionalData,
|
||||
NodeExecuteFunctions,
|
||||
this.mode,
|
||||
);
|
||||
nodeSuccessData = runNodeData.data;
|
||||
const { pinData } = this.runExecutionData.resultData;
|
||||
|
||||
if (runNodeData.closeFunction) {
|
||||
// Explanation why we do this can be found in n8n-workflow/Workflow.ts -> runNode
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
closeFunction = runNodeData.closeFunction();
|
||||
if (pinData && !executionNode.disabled && pinData[executionNode.name] !== undefined) {
|
||||
let nodePinData = pinData[executionNode.name];
|
||||
|
||||
if (!Array.isArray(nodePinData)) nodePinData = [nodePinData];
|
||||
|
||||
const itemsPerRun = nodePinData.map((item, index) => {
|
||||
return { json: item, pairedItem: { item: index } };
|
||||
});
|
||||
nodeSuccessData = [itemsPerRun]; // always zeroth runIndex
|
||||
} else {
|
||||
Logger.debug(`Running node "${executionNode.name}" started`, {
|
||||
node: executionNode.name,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const runNodeData = await workflow.runNode(
|
||||
executionData,
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
this.additionalData,
|
||||
NodeExecuteFunctions,
|
||||
this.mode,
|
||||
);
|
||||
nodeSuccessData = runNodeData.data;
|
||||
|
||||
if (runNodeData.closeFunction) {
|
||||
// Explanation why we do this can be found in n8n-workflow/Workflow.ts -> runNode
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
closeFunction = runNodeData.closeFunction();
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug(`Running node "${executionNode.name}" finished successfully`, {
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
import N8nButton from './Button.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Button',
|
||||
component: N8nButton,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
},
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['primary', 'outline', 'light', 'text', 'tertiary'],
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
type: 'boolean',
|
||||
},
|
||||
theme: {
|
||||
type: 'select',
|
||||
options: ['success', 'danger', 'warning'],
|
||||
},
|
||||
float: {
|
||||
type: 'select',
|
||||
options: ['left', 'right'],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=5%3A1147',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template: '<n8n-button v-bind="$props" @click="onClick" />',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Button = Template.bind({});
|
||||
Button.args = {
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
const ManyTemplate = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template:
|
||||
'<div> <n8n-button v-bind="$props" size="large" @click="onClick" /> <n8n-button v-bind="$props" size="medium" @click="onClick" /> <n8n-button v-bind="$props" size="small" @click="onClick" /> <n8n-button v-bind="$props" :loading="true" @click="onClick" /> <n8n-button v-bind="$props" :disabled="true" @click="onClick" /></div>',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Primary = ManyTemplate.bind({});
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Outline = ManyTemplate.bind({});
|
||||
Outline.args = {
|
||||
type: 'outline',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Tertiary = ManyTemplate.bind({});
|
||||
Tertiary.args = {
|
||||
type: 'tertiary',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Light = ManyTemplate.bind({});
|
||||
Light.args = {
|
||||
type: 'light',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const WithIcon = ManyTemplate.bind({});
|
||||
WithIcon.args = {
|
||||
label: 'Button',
|
||||
icon: 'plus-circle',
|
||||
};
|
||||
|
||||
export const Text = ManyTemplate.bind({});
|
||||
Text.args = {
|
||||
type: 'text',
|
||||
label: 'Button',
|
||||
icon: 'plus-circle',
|
||||
};
|
|
@ -0,0 +1,167 @@
|
|||
/* tslint:disable:variable-name */
|
||||
import N8nButton from './Button.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryFn } from "@storybook/vue";
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Button',
|
||||
component: N8nButton,
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'],
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
|
||||
},
|
||||
},
|
||||
float: {
|
||||
type: 'select',
|
||||
options: ['left', 'right'],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=5%3A1147',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template: '<n8n-button v-bind="$props" @click="onClick" />',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Button = Template.bind({});
|
||||
Button.args = {
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template: `<div>
|
||||
<n8n-button v-bind="$props" size="large" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" @click="onClick" />
|
||||
<n8n-button v-bind="$props" :loading="true" @click="onClick" />
|
||||
<n8n-button v-bind="$props" :disabled="true" @click="onClick" />
|
||||
</div>`,
|
||||
methods,
|
||||
});
|
||||
|
||||
const AllColorsTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template: `<div>
|
||||
<n8n-button v-bind="$props" type="primary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" type="secondary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" type="tertiary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" type="success" @click="onClick" />
|
||||
<n8n-button v-bind="$props" type="warning" @click="onClick" />
|
||||
<n8n-button v-bind="$props" type="danger" @click="onClick" />
|
||||
</div>`,
|
||||
methods,
|
||||
});
|
||||
|
||||
const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
template: `<div>
|
||||
<n8n-button v-bind="$props" size="large" type="primary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="large" type="secondary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="large" type="tertiary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="large" type="success" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="large" type="warning" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="large" type="danger" @click="onClick" />
|
||||
<br/>
|
||||
<br/>
|
||||
<n8n-button v-bind="$props" size="medium" type="primary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" type="secondary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" type="tertiary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" type="success" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" type="warning" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="medium" type="danger" @click="onClick" />
|
||||
<br/>
|
||||
<br/>
|
||||
<n8n-button v-bind="$props" size="small" type="primary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" type="secondary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" type="tertiary" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" type="success" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" type="warning" @click="onClick" />
|
||||
<n8n-button v-bind="$props" size="small" type="danger" @click="onClick" />
|
||||
</div>`,
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Primary = AllSizesTemplate.bind({});
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Secondary = AllSizesTemplate.bind({});
|
||||
Secondary.args = {
|
||||
type: 'secondary',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Tertiary = AllSizesTemplate.bind({});
|
||||
Tertiary.args = {
|
||||
type: 'tertiary',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Success = AllSizesTemplate.bind({});
|
||||
Success.args = {
|
||||
type: 'success',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Warning = AllSizesTemplate.bind({});
|
||||
Warning.args = {
|
||||
type: 'warning',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Danger = AllSizesTemplate.bind({});
|
||||
Danger.args = {
|
||||
type: 'danger',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Outline = AllColorsAndSizesTemplate.bind({});
|
||||
Outline.args = {
|
||||
outline: true,
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Text = AllColorsAndSizesTemplate.bind({});
|
||||
Text.args = {
|
||||
text: true,
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const WithIcon = AllSizesTemplate.bind({});
|
||||
WithIcon.args = {
|
||||
label: 'Button',
|
||||
icon: 'plus-circle',
|
||||
};
|
||||
|
|
@ -1,62 +1,45 @@
|
|||
<template functional>
|
||||
<component
|
||||
:is="$options.components.ElButton"
|
||||
:plain="props.type === 'outline'"
|
||||
:disabled="props.disabled"
|
||||
:size="props.size"
|
||||
:loading="props.loading"
|
||||
:title="props.title || props.label"
|
||||
:class="$options.getClass(props, $style)"
|
||||
:round="!props.circle && props.round"
|
||||
:circle="props.circle"
|
||||
:style="$options.styles(props)"
|
||||
@click="(e) => listeners.click && listeners.click(e)"
|
||||
<template>
|
||||
<button
|
||||
:class="classes"
|
||||
:disabled="disabled || loading"
|
||||
:aria-disabled="ariaDisabled"
|
||||
:aria-busy="ariaBusy"
|
||||
aria-live="polite"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<span :class="$style.icon" v-if="props.loading || props.icon">
|
||||
<component
|
||||
:is="$options.components.N8nSpinner"
|
||||
v-if="props.loading"
|
||||
:size="props.size"
|
||||
<span :class="$style.icon" v-if="loading || icon">
|
||||
<n8n-spinner
|
||||
v-if="loading"
|
||||
:size="size"
|
||||
/>
|
||||
<component
|
||||
:is="$options.components.N8nIcon"
|
||||
v-else-if="props.icon"
|
||||
:icon="props.icon"
|
||||
:size="props.size"
|
||||
<n8n-icon
|
||||
v-else-if="icon"
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="props.label || $slots.default">
|
||||
<slot>
|
||||
{{ props.label }}
|
||||
</slot>
|
||||
<span v-if="label || $slots.default">
|
||||
<slot>{{ label }}</slot>
|
||||
</span>
|
||||
</component>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import N8nSpinner from '../N8nSpinner';
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
name: 'n8n-button',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value: string): boolean =>
|
||||
['primary', 'outline', 'light', 'text', 'tertiary'].includes(value),
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
validator: (value: string): boolean =>
|
||||
['success', 'warning', 'danger'].includes(value),
|
||||
['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
|
@ -72,13 +55,18 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
},
|
||||
round: {
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
circle: {
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array],
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -87,47 +75,82 @@ export default {
|
|||
validator: (value: string): boolean =>
|
||||
['left', 'right'].includes(value),
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
transparentBackground: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ElButton,
|
||||
N8nSpinner,
|
||||
N8nIcon,
|
||||
},
|
||||
styles: (props: {
|
||||
fullWidth?: boolean;
|
||||
float?: string;
|
||||
}): { float?: string; width?: string } => {
|
||||
return {
|
||||
...(props.float ? { float: props.float } : {}),
|
||||
...(props.fullWidth ? { width: '100%' } : {}),
|
||||
};
|
||||
computed: {
|
||||
ariaBusy(): string {
|
||||
return this.loading ? 'true' : 'false';
|
||||
},
|
||||
ariaDisabled(): string {
|
||||
return this.disabled ? 'true' : 'false';
|
||||
},
|
||||
classes(): string {
|
||||
return `button ${this.$style['button']} ${this.$style[this.type]}` +
|
||||
`${this.size ? ` ${this.$style[this.size]}` : ''}` +
|
||||
`${this.outline ? ` ${this.$style['outline']}` : ''}` +
|
||||
`${this.loading ? ` ${this.$style['loading']}` : ''}` +
|
||||
`${this.float ? ` ${this.$style[`float-${this.float}`]}` : ''}` +
|
||||
`${this.text ? ` ${this.$style['text']}` : ''}` +
|
||||
`${this.disabled ? ` ${this.$style['disabled']}` : ''}` +
|
||||
`${this.block ? ` ${this.$style['block']}` : ''}` +
|
||||
`${this.icon || this.loading ? ` ${this.$style['icon']}` : ''}`;
|
||||
},
|
||||
},
|
||||
getClass(props: { type: string; theme?: string, transparentBackground: boolean }, $style: any): string {
|
||||
const theme = props.type === 'text' || props.type === 'tertiary'
|
||||
? props.type
|
||||
: `${props.type}-${props.theme || 'primary'}`;
|
||||
|
||||
if (props.transparentBackground) {
|
||||
return `${$style[theme]} ${$style['transparent']}`;
|
||||
}
|
||||
|
||||
return $style[theme];
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@import "../../utils";
|
||||
@import '../../../theme/src/mixins/utils';
|
||||
@import '../../../theme/src/common/var';
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
border: var(--border-width-base) $button-border-color var(--border-style-base);
|
||||
color: $button-font-color;
|
||||
background-color: $button-background-color;
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-radius: $button-border-radius;
|
||||
padding: $button-padding-vertical $button-padding-horizontal;
|
||||
font-size: $button-font-size;
|
||||
|
||||
-webkit-appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
|
||||
@include utils-user-select(none);
|
||||
|
||||
&:hover {
|
||||
color: $button-hover-color;
|
||||
border-color: $button-hover-border-color;
|
||||
background-color: $button-hover-background-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $button-focus-outline-color;
|
||||
outline: $focus-outline-width solid $button-focus-outline-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $button-active-color;
|
||||
border-color: $button-active-border-color;
|
||||
background-color: $button-active-background-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: none;
|
||||
}
|
||||
|
@ -143,172 +166,258 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
$active-shade-percent: 10%;
|
||||
$color-primary-shade: lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-($active-shade-percent)
|
||||
);
|
||||
$loading-overlay-background-color: rgba(255, 255, 255, 0);
|
||||
|
||||
$color-success-shade: lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
-($active-shade-percent)
|
||||
);
|
||||
/**
|
||||
* Colors
|
||||
*/
|
||||
|
||||
$color-warning-shade: lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
-($active-shade-percent)
|
||||
);
|
||||
|
||||
$color-danger-shade: lightness(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
-($active-shade-percent)
|
||||
);
|
||||
|
||||
.primary-primary {
|
||||
composes: button;
|
||||
}
|
||||
|
||||
.primary-success {
|
||||
composes: button;
|
||||
--button-background-color: var(--color-success);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-success);
|
||||
--button-active-color: var(--color-text-xlight);
|
||||
--button-active-border-color: #{$color-success-shade};
|
||||
--button-active-background-color: #{$color-success-shade};
|
||||
}
|
||||
|
||||
.primary-warning {
|
||||
composes: button;
|
||||
--button-background-color: var(--color-warning);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-warning);
|
||||
--button-active-color: var(--color-text-xlight);
|
||||
--button-active-border-color: #{$color-warning-shade};
|
||||
--button-active-background-color: #{$color-warning-shade};
|
||||
}
|
||||
|
||||
.primary-danger {
|
||||
composes: button;
|
||||
--button-background-color: var(--color-danger);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-danger);
|
||||
--button-active-color: var(--color-text-xlight);
|
||||
--button-active-border-color: #{$color-danger-shade};
|
||||
--button-active-background-color: #{$color-danger-shade};
|
||||
}
|
||||
|
||||
.outline {
|
||||
--button-background-color: var(--color-foreground-xlight);
|
||||
--button-disabled-background-color: var(--color-foreground-xlight);
|
||||
--button-active-background-color: var(--color-foreground-xlight);
|
||||
}
|
||||
|
||||
.outline-primary {
|
||||
composes: button;
|
||||
composes: outline;
|
||||
.secondary {
|
||||
--button-color: var(--color-primary);
|
||||
--button-active-border-color: #{$color-primary-shade};
|
||||
--button-active-color: #{$color-primary-shade};
|
||||
}
|
||||
|
||||
.outline-success {
|
||||
composes: button;
|
||||
composes: outline;
|
||||
--button-color: var(--color-success);
|
||||
--button-border-color: var(--color-success);
|
||||
--button-active-color: #{$color-success-shade};
|
||||
--button-active-border-color: #{$color-success-shade};
|
||||
}
|
||||
|
||||
.outline-warning {
|
||||
composes: button;
|
||||
composes: outline;
|
||||
--button-color: var(--color-warning);
|
||||
--button-border-color: var(--color-warning);
|
||||
--button-active-color: #{$color-warning-shade};
|
||||
--button-active-border-color: #{$color-warning-shade};
|
||||
}
|
||||
|
||||
.outline-danger {
|
||||
composes: button;
|
||||
composes: outline;
|
||||
--button-color: var(--color-danger);
|
||||
--button-border-color: var(--color-danger);
|
||||
--button-active-color: #{$color-danger-shade};
|
||||
--button-active-border-color: #{$color-danger-shade};
|
||||
}
|
||||
|
||||
.light-primary {
|
||||
composes: button;
|
||||
--button-color: var(--color-primary);
|
||||
--button-border-color: var(--color-primary-tint-2);
|
||||
--button-background-color: var(--color-primary-tint-2);
|
||||
--button-active-background-color: var(--color-primary-tint-2);
|
||||
--button-active-color: #{$color-primary-shade};
|
||||
--button-active-border-color: #{$color-primary-shade};
|
||||
}
|
||||
|
||||
.light-success {
|
||||
composes: button;
|
||||
--button-color: var(--color-success);
|
||||
--button-border-color: var(--color-success-tint-1);
|
||||
--button-background-color: var(--color-success-tint-1);
|
||||
--button-active-background-color: var(--color-success-tint-1);
|
||||
--button-active-color: #{$color-success-shade};
|
||||
--button-active-border-color: #{$color-success-shade};
|
||||
}
|
||||
|
||||
.light-warning {
|
||||
composes: button;
|
||||
--button-color: var(--color-warning);
|
||||
--button-border-color: var(--color-warning-tint-2);
|
||||
--button-background-color: var(--color-warning-tint-2);
|
||||
--button-active-background-color: var(--color-warning-tint-2);
|
||||
--button-active-color: #{$color-warning-shade};
|
||||
--button-active-border-color: #{$color-warning-shade};
|
||||
}
|
||||
|
||||
.light-danger {
|
||||
composes: button;
|
||||
--button-color: var(--color-danger);
|
||||
--button-border-color: var(--color-danger-tint-1);
|
||||
--button-background-color: var(--color-danger-tint-1);
|
||||
--button-active-background-color: var(--color-danger-tint-1);
|
||||
--button-active-color: #{$color-danger-shade};
|
||||
--button-active-border-color: #{$color-danger-shade};
|
||||
}
|
||||
|
||||
.text {
|
||||
composes: button;
|
||||
--button-color: var(--color-text-light);
|
||||
--button-border-color: transparent;
|
||||
--button-background-color: transparent;
|
||||
--button-active-background-color: transparent;
|
||||
--button-active-color: var(--color-primary);
|
||||
--button-active-border-color: transparent;
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
composes: button;
|
||||
font-weight: var(--font-weight-regular) !important;
|
||||
--button-color: var(--color-text-dark);
|
||||
--button-border-color: var(--color-foreground-base);
|
||||
--button-background-color: var(--color-background-base);
|
||||
--button-border-color: var(--color-primary);
|
||||
--button-background-color: var(--color-background-xlight);
|
||||
|
||||
--button-active-background-color: var(--color-primary-tint-2);
|
||||
--button-active-color: var(--color-primary);
|
||||
--button-active-border-color: var(--color-primary);
|
||||
|
||||
--button-disabled-border-color: var(--color-foreground-xdark);
|
||||
--button-hover-background-color: var(--color-primary-tint-3);
|
||||
--button-hover-color: var(--color-primary);
|
||||
--button-hover-border-color: var(--color-primary);
|
||||
|
||||
--button-focus-outline-color: var(--color-primary-tint-1);
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
font-weight: var(--font-weight-regular) !important;
|
||||
|
||||
--button-background-color: var(--color-background-xlight);
|
||||
--button-color: var(--color-text-dark);
|
||||
--button-border-color: var(--color-neutral-850);
|
||||
|
||||
--button-active-background-color: var(--color-primary-tint-2);
|
||||
--button-active-color: var(--color-primary);
|
||||
--button-active-border-color: var(--color-primary);
|
||||
|
||||
--button-hover-background-color: var(--color-neutral-950);
|
||||
--button-hover-color: var(--color-text-dark);
|
||||
--button-hover-border-color: var(--color-neutral-800);
|
||||
|
||||
--button-focus-outline-color: hsla(var(--color-neutral-h), var(--color-neutral-s), var(--color-neutral-l), 0.2);
|
||||
}
|
||||
|
||||
.success {
|
||||
--button-background-color: var(--color-success);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-success);
|
||||
|
||||
--button-active-background-color: var(--color-success-350);
|
||||
--button-active-border-color: var(--color-success-350);
|
||||
|
||||
--button-hover-background-color: var(--color-success-450);
|
||||
--button-hover-border-color: var(--color-success-450);
|
||||
|
||||
--button-focus-outline-color: hsla(var(--color-success-h), var(--color-success-s), var(--color-success-l), 0.33);
|
||||
}
|
||||
|
||||
.warning {
|
||||
--button-background-color: var(--color-warning);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-warning);
|
||||
|
||||
--button-active-background-color: var(--color-warning-500);
|
||||
--button-active-border-color: var(--color-warning-500);
|
||||
|
||||
--button-hover-background-color: var(--color-warning-650);
|
||||
--button-hover-border-color: var(--color-warning-650);
|
||||
|
||||
--button-focus-outline-color: hsla(var(--color-warning-h), var(--color-warning-s), var(--color-warning-l), 0.33);
|
||||
}
|
||||
|
||||
.danger {
|
||||
--button-background-color: var(--color-danger);
|
||||
--button-color: var(--color-text-xlight);
|
||||
--button-border-color: var(--color-danger);
|
||||
--button-active-color: var(--color-text-xlight);
|
||||
|
||||
--button-active-background-color: var(--color-danger-600);
|
||||
--button-active-border-color: var(--color-danger-600);
|
||||
|
||||
--button-hover-background-color: var(--color-danger-700);
|
||||
--button-hover-border-color: var(--color-danger-700);
|
||||
|
||||
--button-focus-outline-color: hsla(var(--color-danger-h), var(--color-danger-s), var(--color-danger-l), 0.33);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sizes
|
||||
*/
|
||||
|
||||
.mini {
|
||||
--button-padding-vertical: var(--spacing-4xs);
|
||||
--button-padding-horizontal: var(--spacing-2xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
&.icon-button {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.small {
|
||||
--button-padding-vertical: var(--spacing-3xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
&.icon-button {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.medium {
|
||||
--button-padding-vertical: var(--spacing-2xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
&.icon-button {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.large {
|
||||
&.icon-button {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-s);
|
||||
--button-font-size: var(--font-size-m);
|
||||
|
||||
&.icon-button {
|
||||
height: 46px;
|
||||
width: 46px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifiers
|
||||
*/
|
||||
|
||||
.outline {
|
||||
--button-color: var(--color-primary);
|
||||
--button-background-color: transparent;
|
||||
--button-disabled-background-color: transparent;
|
||||
--button-active-background-color: transparent;
|
||||
|
||||
&.primary {
|
||||
--button-color: var(--color-primary);
|
||||
--button-border-color: var(--color-primary);
|
||||
--button-active-background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
--button-color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
&.success {
|
||||
--button-color: var(--color-success);
|
||||
--button-border-color: var(--color-success);
|
||||
--button-active-background-color: var(--color-success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
--button-color: var(--color-warning);
|
||||
--button-border-color: var(--color-warning);
|
||||
--button-active-background-color: var(--color-warning);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
--button-color: var(--color-danger);
|
||||
--button-border-color: var(--color-danger);
|
||||
--button-active-background-color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
--button-color: var(--color-text-light);
|
||||
--button-border-color: transparent;
|
||||
--button-background-color: transparent;
|
||||
--button-active-color: var(--color-text-light);
|
||||
--button-active-background-color: transparent;
|
||||
--button-active-border-color: transparent;
|
||||
--button-hover-color: var(--color-text-light);
|
||||
--button-hover-background-color: transparent;
|
||||
--button-hover-border-color: transparent;
|
||||
|
||||
&.primary {
|
||||
--button-color: var(--color-primary);
|
||||
--button-active-color: var(--color-primary);
|
||||
--button-hover-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
--button-color: var(--color-primary-tint-1);
|
||||
--button-active-color: var(--color-primary-tint-1);
|
||||
--button-hover-color: var(--color-primary-tint-1);
|
||||
}
|
||||
|
||||
&.success {
|
||||
--button-color: var(--color-success);
|
||||
--button-active-color: var(--color-success);
|
||||
--button-hover-color: var(--color-success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
--button-color: var(--color-warning);
|
||||
--button-active-color: var(--color-warning);
|
||||
--button-hover-color: var(--color-warning);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
--button-color: var(--color-danger);
|
||||
--button-active-color: var(--color-danger);
|
||||
--button-hover-color: var(--color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.loading,
|
||||
.active {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
&:before {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border-radius: inherit;
|
||||
background-color: $loading-overlay-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
&,
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: not-allowed;
|
||||
background-image: none;
|
||||
color: $button-disabled-font-color;
|
||||
background-color: $button-disabled-background-color;
|
||||
border-color: $button-disabled-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.transparent {
|
||||
|
@ -323,4 +432,16 @@ $color-danger-shade: lightness(
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import {render} from '@testing-library/vue';
|
||||
import N8nButton from "../Button.vue";
|
||||
import ElButton from "../overrides/ElButton.vue";
|
||||
|
||||
const slots = {
|
||||
default: 'Button',
|
||||
};
|
||||
const stubs = ['n8n-spinner', 'n8n-icon'];
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nButton', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = render(N8nButton, {
|
||||
slots,
|
||||
stubs,
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
describe('loading', () => {
|
||||
it('should render loading spinner', () => {
|
||||
const wrapper = render(N8nButton, {
|
||||
props: {
|
||||
loading: true,
|
||||
},
|
||||
slots,
|
||||
stubs,
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('icon', () => {
|
||||
it('should render icon button', () => {
|
||||
const wrapper = render(N8nButton, {
|
||||
props: {
|
||||
icon: 'plus-circle',
|
||||
},
|
||||
slots,
|
||||
stubs,
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overrides', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = render(ElButton, {
|
||||
props: {
|
||||
icon: 'plus-circle',
|
||||
type: 'secondary',
|
||||
},
|
||||
slots,
|
||||
stubs,
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`components > N8nButton > overrides > should render correctly 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_cccce_115 _secondary_cccce_170 _medium_cccce_254 _icon_cccce_239\\" icon=\\"plus-circle\\" type=\\"secondary\\"><span class=\\"_icon_cccce_239\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
|
||||
|
||||
exports[`components > N8nButton > props > icon > should render icon button 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_cccce_115 _primary_cccce_288 _medium_cccce_254 _icon_cccce_239\\"><span class=\\"_icon_cccce_239\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
|
||||
|
||||
exports[`components > N8nButton > props > loading > should render loading spinner 1`] = `"<button disabled=\\"disabled\\" aria-disabled=\\"false\\" aria-busy=\\"true\\" aria-live=\\"polite\\" class=\\"button _button_cccce_115 _primary_cccce_288 _medium_cccce_254 _loading_cccce_352 _icon_cccce_239\\"><span class=\\"_icon_cccce_239\\"><n8n-spinner-stub size=\\"medium\\" type=\\"dots\\"></n8n-spinner-stub></span><span>Button</span></button>"`;
|
||||
|
||||
exports[`components > N8nButton > should render correctly 1`] = `
|
||||
"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_cccce_115 _primary_cccce_288 _medium_cccce_254\\">
|
||||
<!----><span>Button</span></button>"
|
||||
`;
|
|
@ -1,43 +0,0 @@
|
|||
import { N8nComponent, N8nComponentSize } from '../component';
|
||||
|
||||
/** Button type */
|
||||
export type ButtonType = 'primary' | 'outline' | 'light' | 'text';
|
||||
|
||||
/** Button themes */
|
||||
export type ButtonTheme = 'success' | 'warning' | 'danger';
|
||||
|
||||
/** Button Component */
|
||||
export declare class N8nButton extends N8nComponent {
|
||||
/** Button text */
|
||||
label: string;
|
||||
|
||||
/** Button title on hover */
|
||||
title: string;
|
||||
|
||||
/** Color scheme */
|
||||
theme: ButtonTheme;
|
||||
|
||||
/** Button size */
|
||||
size: N8nComponentSize;
|
||||
|
||||
/** Button type */
|
||||
type: ButtonType;
|
||||
|
||||
/** Determine whether it's a circular button */
|
||||
circle: boolean;
|
||||
|
||||
/** Determine whether it's loading */
|
||||
loading: boolean;
|
||||
|
||||
/** Disable the button */
|
||||
disabled: boolean;
|
||||
|
||||
/** Button icon, accepts an icon name of font awesome icon component */
|
||||
icon: string;
|
||||
|
||||
/** Full width */
|
||||
fullWidth: boolean;
|
||||
|
||||
/** Float left or right */
|
||||
float: boolean;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<n8n-button
|
||||
ref="button"
|
||||
v-bind="attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot/>
|
||||
</n8n-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import N8nButton from '../Button.vue';
|
||||
|
||||
const classToTypeMap = {
|
||||
'btn--cancel': 'secondary',
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
N8nButton,
|
||||
},
|
||||
computed: {
|
||||
attrs() {
|
||||
let type = 'primary';
|
||||
Object.entries(classToTypeMap).forEach(([className, mappedType]) => {
|
||||
if (this.$refs.button && (this.$refs.button as Vue).$el.classList.contains(className)) {
|
||||
type = mappedType;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type,
|
||||
...this.$attrs,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1 @@
|
|||
export { default as N8nElButton } from './ElButton.vue';
|
|
@ -0,0 +1,107 @@
|
|||
import N8nCallout from './Callout.vue';
|
||||
import { StoryFn } from '@storybook/vue';
|
||||
import N8nLink from '../N8nLink';
|
||||
import N8nText from '../N8nText';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Callout',
|
||||
component: N8nCallout,
|
||||
argTypes: {
|
||||
theme: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
|
||||
},
|
||||
},
|
||||
message: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/tPpJvbrnHbP8C496cYuwyW/Node-pinning?node-id=15%3A5777',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const template: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nLink,
|
||||
N8nText,
|
||||
N8nCallout,
|
||||
},
|
||||
template: `
|
||||
<n8n-callout v-bind="$props">
|
||||
${args.default}
|
||||
<template #actions v-if="actions">
|
||||
${args.actions}
|
||||
</template>
|
||||
<template #trailingContent v-if="trailingContent">
|
||||
${args.trailingContent}
|
||||
</template>
|
||||
</n8n-callout>
|
||||
`,
|
||||
});
|
||||
|
||||
export const customCallout = template.bind({});
|
||||
customCallout.args = {
|
||||
theme: 'custom',
|
||||
icon: 'code-branch',
|
||||
default: `
|
||||
<n8n-text
|
||||
size="small"
|
||||
>
|
||||
This is a callout.
|
||||
</n8n-text>
|
||||
`,
|
||||
actions: `
|
||||
<n8n-link
|
||||
size="small"
|
||||
>
|
||||
Do something!
|
||||
</n8n-link>
|
||||
`,
|
||||
};
|
||||
|
||||
export const secondaryCallout = template.bind({});
|
||||
secondaryCallout.args = {
|
||||
theme: 'secondary',
|
||||
icon: 'thumbtack',
|
||||
default: `
|
||||
<n8n-text
|
||||
size="small"
|
||||
:bold="true"
|
||||
>
|
||||
This data is pinned.
|
||||
</n8n-text>
|
||||
`,
|
||||
actions: `
|
||||
<n8n-link
|
||||
theme="secondary"
|
||||
size="small"
|
||||
:bold="true"
|
||||
>
|
||||
Unpin
|
||||
</n8n-link>
|
||||
`,
|
||||
trailingContent: `
|
||||
<n8n-link
|
||||
theme="secondary"
|
||||
size="small"
|
||||
:bold="true"
|
||||
:underline="true"
|
||||
to="https://n8n.io"
|
||||
>
|
||||
Learn more
|
||||
</n8n-link>
|
||||
`,
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
/* tslint:disable:variable-name */
|
||||
import N8nIconButton from './IconButton.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryFn } from "@storybook/vue";
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Icon Button',
|
||||
|
@ -7,31 +9,12 @@ export default {
|
|||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['primary', 'outline', 'light', 'text'],
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
options: ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'],
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['small', 'medium', 'large', 'xlarge'],
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['success', 'warning', 'danger'],
|
||||
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -44,7 +27,7 @@ const methods = {
|
|||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nIconButton,
|
||||
|
@ -59,7 +42,7 @@ Button.args = {
|
|||
title: 'my title',
|
||||
};
|
||||
|
||||
const ManyTemplate = (args, { argTypes }) => ({
|
||||
const ManyTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nIconButton,
|
||||
|
@ -80,7 +63,8 @@ export const Outline = ManyTemplate.bind({});
|
|||
Outline.args = {
|
||||
icon: 'plus',
|
||||
title: 'my title',
|
||||
type: 'outline',
|
||||
type: 'primary',
|
||||
outline: true,
|
||||
};
|
||||
|
||||
export const Light = ManyTemplate.bind({});
|
|
@ -1,14 +1,8 @@
|
|||
<template functional>
|
||||
<component :is="$options.components.N8nButton"
|
||||
:type="props.type"
|
||||
:disabled="props.disabled"
|
||||
:size="props.size"
|
||||
:loading="props.loading"
|
||||
:title="props.title"
|
||||
:icon="props.icon"
|
||||
:theme="props.theme"
|
||||
@click="(e) => listeners.click && listeners.click(e)"
|
||||
circle
|
||||
<template>
|
||||
<n8n-button
|
||||
:class="`icon-button ${$style['icon-button']} ${$style[size]}`"
|
||||
v-bind="$props"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -23,9 +17,7 @@ export default {
|
|||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
|
@ -35,16 +27,73 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array],
|
||||
required: true,
|
||||
},
|
||||
theme: {
|
||||
float: {
|
||||
type: String,
|
||||
validator: (value: string): boolean =>
|
||||
['left', 'right'].includes(value),
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.icon-button {
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mini {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.small {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.large {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
height: 46px;
|
||||
width: 46px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template functional>
|
||||
<component :is="$options.components.N8nRoute" :to="props.to" :newWindow="props.newWindow"
|
||||
@click="listeners.click"
|
||||
@click="listeners.click"
|
||||
>
|
||||
<span
|
||||
:class="$style[`${props.underline ? `${props.theme}-underline` : props.theme}`]"
|
||||
|
@ -42,7 +42,7 @@ export default {
|
|||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value: string): boolean =>
|
||||
['primary', 'danger', 'text'].includes(value),
|
||||
['primary', 'danger', 'text', 'secondary'].includes(value),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
@ -60,10 +60,10 @@ export default {
|
|||
|
||||
&:active {
|
||||
color: saturation(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-(30%)
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-(30%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -72,12 +72,7 @@ export default {
|
|||
color: var(--color-text-base);
|
||||
|
||||
&:active {
|
||||
color: saturation(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-(30%)
|
||||
);
|
||||
color: saturation(--color-primary-h, --color-primary-s, --color-primary-l, -(30%));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,15 +80,23 @@ export default {
|
|||
color: var(--color-danger);
|
||||
|
||||
&:active {
|
||||
color: saturation(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
-(20%)
|
||||
);
|
||||
color: saturation(--color-danger-h, --color-danger-s, --color-danger-l, -(20%));
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--color-secondary);
|
||||
|
||||
&:active {
|
||||
color: saturation(--color-secondary-h, --color-secondary-s, --color-secondary-l, -(20%));
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--color-secondary-tint-2);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.primary-underline {
|
||||
composes: primary;
|
||||
text-decoration: underline;
|
||||
|
@ -104,5 +107,9 @@ export default {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.secondary-underline {
|
||||
composes: secondary;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -38,8 +38,8 @@ export default Vue.extend({
|
|||
default: 'warning',
|
||||
},
|
||||
content: {
|
||||
required: true,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fullContent: {
|
||||
type: String,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {fireEvent, render} from '@testing-library/vue';
|
||||
import { render } from '@testing-library/vue';
|
||||
import N8nNotice from "../Notice.vue";
|
||||
|
||||
describe('components', () => {
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import N8nPanelCallout from './PanelCallout.vue';
|
||||
import { StoryFn } from '@storybook/vue';
|
||||
import N8nLink from '../N8nLink';
|
||||
import N8nText from '../N8nText';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Callout',
|
||||
component: N8nPanelCallout,
|
||||
argTypes: {
|
||||
theme: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
|
||||
},
|
||||
},
|
||||
message: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/tPpJvbrnHbP8C496cYuwyW/Node-pinning?node-id=15%3A5777',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const template: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nLink,
|
||||
N8nText,
|
||||
N8nPanelCallout,
|
||||
},
|
||||
template: `
|
||||
<n8n-panel-callout v-bind="$props">
|
||||
${args.default}
|
||||
<template #actions v-if="actions">
|
||||
${args.actions}
|
||||
</template>
|
||||
<template #trailingContent v-if="trailingContent">
|
||||
${args.trailingContent}
|
||||
</template>
|
||||
</n8n-panel-callout>
|
||||
`,
|
||||
});
|
||||
|
||||
export const customCallout = template.bind({});
|
||||
customCallout.args = {
|
||||
theme: 'custom',
|
||||
icon: 'code-branch',
|
||||
default: `
|
||||
<n8n-text
|
||||
size="small"
|
||||
>
|
||||
This is a callout.
|
||||
</n8n-text>
|
||||
`,
|
||||
actions: `
|
||||
<n8n-link
|
||||
size="small"
|
||||
>
|
||||
Do something!
|
||||
</n8n-link>
|
||||
`,
|
||||
};
|
||||
|
||||
export const secondaryCallout = template.bind({});
|
||||
secondaryCallout.args = {
|
||||
theme: 'secondary',
|
||||
icon: 'thumbtack',
|
||||
default: `
|
||||
<n8n-text
|
||||
size="small"
|
||||
:bold="true"
|
||||
>
|
||||
This data is pinned.
|
||||
</n8n-text>
|
||||
`,
|
||||
actions: `
|
||||
<n8n-link
|
||||
theme="secondary"
|
||||
size="small"
|
||||
:bold="true"
|
||||
>
|
||||
Unpin
|
||||
</n8n-link>
|
||||
`,
|
||||
trailingContent: `
|
||||
<n8n-link
|
||||
theme="secondary"
|
||||
size="small"
|
||||
:bold="true"
|
||||
:underline="true"
|
||||
to="https://n8n.io"
|
||||
>
|
||||
Learn more
|
||||
</n8n-link>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div :class="classes" role="alert">
|
||||
|
||||
<div :class="$style['message-section']">
|
||||
<div :class="$style.icon">
|
||||
<n8n-icon
|
||||
:icon="getIcon"
|
||||
:size="theme === 'secondary' ? 'medium' : 'large'"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<slot name="trailingContent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
const CALLOUT_DEFAULT_ICONS = {
|
||||
info: 'info-circle',
|
||||
success: 'check-circle',
|
||||
warning: 'exclamation-triangle',
|
||||
danger: 'times-circle',
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'n8n-panel-callout',
|
||||
components: {
|
||||
N8nIcon,
|
||||
},
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value: string): boolean =>
|
||||
['info', 'success', 'secondary', 'warning', 'danger', 'custom'].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'info-circle'
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
classes(): string[] {
|
||||
return [
|
||||
this.$style['callout'],
|
||||
this.$style[this.theme],
|
||||
];
|
||||
},
|
||||
getIcon(): string {
|
||||
if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) {
|
||||
return CALLOUT_DEFAULT_ICONS[this.theme];
|
||||
}
|
||||
|
||||
return this.icon;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.callout {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-2xs);
|
||||
padding: var(--spacing-xs);
|
||||
border: var(--border-width-base) var(--border-style-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
align-items: center;
|
||||
line-height: var(--font-line-height-loose);
|
||||
}
|
||||
|
||||
.message-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info, .custom {
|
||||
border-color: var(--color-foreground-base);
|
||||
background-color: var(--color-background-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-color: var(--color-warning-tint-1);
|
||||
background-color: var(--color-warning-tint-2);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: var(--color-success-tint-1);
|
||||
background-color: var(--color-success-tint-2);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.danger {
|
||||
border-color: var(--color-danger-tint-1);
|
||||
background-color: var(--color-danger-tint-2);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-secondary);
|
||||
background-color: var(--color-secondary-tint-2);
|
||||
border-color: var(--color-secondary-tint-1);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nPanelCallout from './PanelCallout.vue';
|
||||
|
||||
export default N8nPanelCallout;
|
|
@ -39,7 +39,9 @@ import N8nActionToggle from './N8nActionToggle';
|
|||
import N8nAvatar from './N8nAvatar';
|
||||
import N8nBadge from './N8nBadge';
|
||||
import N8nButton from './N8nButton';
|
||||
import { N8nElButton } from './N8nButton/overrides';
|
||||
import N8nCallout from './N8nCallout';
|
||||
import N8nPanelCallout from './N8nPanelCallout';
|
||||
import N8nCard from './N8nCard';
|
||||
import N8nFormBox from './N8nFormBox';
|
||||
import N8nFormInput from './N8nFormInput';
|
||||
|
@ -81,7 +83,9 @@ export {
|
|||
N8nAvatar,
|
||||
N8nBadge,
|
||||
N8nButton,
|
||||
N8nElButton,
|
||||
N8nCallout,
|
||||
N8nPanelCallout,
|
||||
N8nCard,
|
||||
N8nHeading,
|
||||
N8nFormBox,
|
||||
|
|
|
@ -91,6 +91,19 @@ import ColorCircles from './ColorCircles.vue';
|
|||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Neutral
|
||||
|
||||
<Canvas>
|
||||
<Story name="neutral">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-neutral']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Text
|
||||
|
||||
<Canvas>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { Meta, Story, Canvas } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Utilities/Float"
|
||||
/>
|
||||
|
||||
# `.float-left`
|
||||
|
||||
<Canvas>
|
||||
<Story name="float-left">
|
||||
{{
|
||||
template: `<div>
|
||||
<span class="float-left">Float left</span>
|
||||
</div>`,
|
||||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
# `.float-right`
|
||||
|
||||
<Canvas>
|
||||
<Story name="float-right">
|
||||
{{
|
||||
template: `<div>
|
||||
<span class="float-right">Float right</span>
|
||||
</div>`,
|
||||
}}
|
||||
</Story>
|
||||
</Canvas>
|
|
@ -1,260 +1,298 @@
|
|||
@use 'sass:math';
|
||||
|
||||
@mixin theme {
|
||||
--color-primary-h: 7;
|
||||
--color-primary-s: 100%;
|
||||
--color-primary-l: 68%;
|
||||
--color-primary: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-l)
|
||||
);
|
||||
--color-primary-h: 7;
|
||||
--color-primary-s: 100%;
|
||||
--color-primary-l: 68%;
|
||||
--color-primary: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-l)
|
||||
);
|
||||
|
||||
--color-primary-tint-1-l: 18%;
|
||||
--color-primary-tint-1: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-1-l)
|
||||
);
|
||||
--color-primary-tint-1-l: 18%;
|
||||
--color-primary-tint-1: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-1-l)
|
||||
);
|
||||
|
||||
--color-primary-tint-2-l: 9%;
|
||||
--color-primary-tint-2: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-2-l)
|
||||
);
|
||||
--color-primary-tint-2-l: 9%;
|
||||
--color-primary-tint-2: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-2-l)
|
||||
);
|
||||
|
||||
--color-primary-tint-3-l: 3%;
|
||||
--color-primary-tint-3: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-3-l)
|
||||
);
|
||||
--color-primary-tint-3-l: 3%;
|
||||
--color-primary-tint-3: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-tint-3-l)
|
||||
);
|
||||
|
||||
--color-primary-shade-1-l: 89%;
|
||||
--color-primary-shade-1: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-shade-1-l)
|
||||
);
|
||||
--color-primary-shade-1-l: 89%;
|
||||
--color-primary-shade-1: hsl(
|
||||
var(--color-primary-h),
|
||||
var(--color-primary-s),
|
||||
var(--color-primary-shade-1-l)
|
||||
);
|
||||
|
||||
--color-secondary-h: 247;
|
||||
--color-secondary-s: 100%;
|
||||
--color-secondary-l: 35%;
|
||||
--color-secondary: hsl(
|
||||
var(--color-secondary-h),
|
||||
var(--color-secondary-s),
|
||||
var(--color-secondary-l)
|
||||
);
|
||||
--color-secondary-h: 247;
|
||||
--color-secondary-s: 100%;
|
||||
--color-secondary-l: 35%;
|
||||
--color-secondary: hsl(
|
||||
var(--color-secondary-h),
|
||||
var(--color-secondary-s),
|
||||
var(--color-secondary-l)
|
||||
);
|
||||
|
||||
--color-success-h: 150;
|
||||
--color-success-s: 74%;
|
||||
--color-success-l: 60%;
|
||||
--color-success: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-l)
|
||||
);
|
||||
--color-success-h: 150;
|
||||
--color-success-s: 74%;
|
||||
--color-success-l: 60%;
|
||||
--color-success: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-l)
|
||||
);
|
||||
|
||||
--color-success-tint-1-l: 12%;
|
||||
--color-success-tint-1: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-tint-1-l)
|
||||
);
|
||||
--color-success-tint-1-l: 12%;
|
||||
--color-success-tint-1: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-tint-1-l)
|
||||
);
|
||||
|
||||
--color-success-tint-2-l: 3%;
|
||||
--color-success-tint-2: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-tint-2-l)
|
||||
);
|
||||
--color-success-tint-2-l: 3%;
|
||||
--color-success-tint-2: hsl(
|
||||
var(--color-success-h),
|
||||
var(--color-success-s),
|
||||
var(--color-success-tint-2-l)
|
||||
);
|
||||
|
||||
--color-warning-h: 36;
|
||||
--color-warning-s: 77%;
|
||||
--color-warning-l: 43%;
|
||||
--color-warning: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-l)
|
||||
);
|
||||
--color-warning-h: 36;
|
||||
--color-warning-s: 77%;
|
||||
--color-warning-l: 43%;
|
||||
--color-warning: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-l)
|
||||
);
|
||||
|
||||
--color-warning-tint-1-l: 12%;
|
||||
--color-warning-tint-1: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-tint-1-l)
|
||||
);
|
||||
--color-warning-tint-1-l: 12%;
|
||||
--color-warning-tint-1: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-tint-1-l)
|
||||
);
|
||||
|
||||
--color-warning-tint-2-l: 4%;
|
||||
--color-warning-tint-2: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-tint-2-l)
|
||||
);
|
||||
--color-warning-tint-2-l: 4%;
|
||||
--color-warning-tint-2: hsl(
|
||||
var(--color-warning-h),
|
||||
var(--color-warning-s),
|
||||
var(--color-warning-tint-2-l)
|
||||
);
|
||||
|
||||
--color-danger-h: 0;
|
||||
--color-danger-s: 88%;
|
||||
--color-danger-l: 35%;
|
||||
--color-danger: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-l)
|
||||
);
|
||||
--color-danger-h: 0;
|
||||
--color-danger-s: 88%;
|
||||
--color-danger-l: 35%;
|
||||
--color-danger: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-l)
|
||||
);
|
||||
|
||||
--color-danger-tint-1-l: 8%;
|
||||
--color-danger-tint-1: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-tint-1-l)
|
||||
);
|
||||
--color-danger-tint-1-l: 8%;
|
||||
--color-danger-tint-1: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-tint-1-l)
|
||||
);
|
||||
|
||||
--color-danger-tint-2-l: 3%;
|
||||
--color-danger-tint-2: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-tint-2-l)
|
||||
);
|
||||
--color-danger-tint-2-l: 3%;
|
||||
--color-danger-tint-2: hsl(
|
||||
var(--color-danger-h),
|
||||
var(--color-danger-s),
|
||||
var(--color-danger-tint-2-l)
|
||||
);
|
||||
|
||||
--color-info-h: 220;
|
||||
--color-info-s: 4%;
|
||||
--color-info-l: 42%;
|
||||
--color-info: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-l)
|
||||
);
|
||||
--color-info-h: 220;
|
||||
--color-info-s: 4%;
|
||||
--color-info-l: 42%;
|
||||
--color-info: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-l)
|
||||
);
|
||||
|
||||
--color-info-tint-1-l: 12%;
|
||||
--color-info-tint-1: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-tint-1-l)
|
||||
);
|
||||
--color-info-tint-1-l: 12%;
|
||||
--color-info-tint-1: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-tint-1-l)
|
||||
);
|
||||
|
||||
--color-info-tint-2-l: 4%;
|
||||
--color-info-tint-2: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-tint-2-l)
|
||||
);
|
||||
--color-info-tint-2-l: 4%;
|
||||
--color-info-tint-2: hsl(
|
||||
var(--color-info-h),
|
||||
var(--color-info-s),
|
||||
var(--color-info-tint-2-l)
|
||||
);
|
||||
|
||||
--color-text-dark-h: 0;
|
||||
--color-text-dark-s: 0%;
|
||||
--color-text-dark-l: 100%;
|
||||
--color-text-dark: hsl(
|
||||
var(--color-text-dark-h),
|
||||
var(--color-text-dark-s),
|
||||
var(--color-text-dark-l)
|
||||
);
|
||||
--color-grey-h: 228;
|
||||
--color-grey-s: 10%;
|
||||
--color-grey-l: 80%;
|
||||
--color-grey: hsl(
|
||||
var(--color-grey-h),
|
||||
var(--color-grey-s),
|
||||
var(--color-grey-l)
|
||||
);
|
||||
|
||||
--color-text-base-h: 240;
|
||||
--color-text-base-s: 4%;
|
||||
--color-text-base-l: 49%;
|
||||
--color-text-base: hsl(
|
||||
var(--color-text-base-h),
|
||||
var(--color-text-base-s),
|
||||
var(--color-text-base-l)
|
||||
);
|
||||
--color-light-grey-h: 220;
|
||||
--color-light-grey-s: 20%;
|
||||
--color-light-grey-l: 88%;
|
||||
--color-light-grey: hsl(
|
||||
var(--color-light-grey-h),
|
||||
var(--color-light-grey-s),
|
||||
var(--color-light-grey-l)
|
||||
);
|
||||
|
||||
--color-text-light-h: 220;
|
||||
--color-text-light-s: 4%;
|
||||
--color-text-light-l: 42%;
|
||||
--color-text-light: hsl(
|
||||
var(--color-text-light-h),
|
||||
var(--color-text-light-s),
|
||||
var(--color-text-light-l)
|
||||
);
|
||||
--color-neutral-h: 228;
|
||||
--color-neutral-s: 10%;
|
||||
--color-neutral-l: 50%;
|
||||
--color-neutral: hsl(
|
||||
var(--color-neutral-h),
|
||||
var(--color-neutral-s),
|
||||
var(--color-neutral-l)
|
||||
);
|
||||
|
||||
--color-text-lighter-h: 222;
|
||||
--color-text-lighter-s: 17%;
|
||||
--color-text-lighter-l: 12%;
|
||||
--color-text-lighter: hsl(
|
||||
var(--color-text-lighter-h),
|
||||
var(--color-text-lighter-s),
|
||||
var(--color-text-lighter-l)
|
||||
);
|
||||
--color-text-dark-h: 0;
|
||||
--color-text-dark-s: 0%;
|
||||
--color-text-dark-l: 100%;
|
||||
--color-text-dark: hsl(
|
||||
var(--color-text-dark-h),
|
||||
var(--color-text-dark-s),
|
||||
var(--color-text-dark-l)
|
||||
);
|
||||
|
||||
--color-text-xlight-h: 0;
|
||||
--color-text-xlight-s: 0%;
|
||||
--color-text-xlight-l: 100%;
|
||||
--color-text-xlight: hsl(
|
||||
var(--color-text-xlight-h),
|
||||
var(--color-text-xlight-s),
|
||||
var(--color-text-xlight-l)
|
||||
);
|
||||
--color-text-base-h: 240;
|
||||
--color-text-base-s: 4%;
|
||||
--color-text-base-l: 49%;
|
||||
--color-text-base: hsl(
|
||||
var(--color-text-base-h),
|
||||
var(--color-text-base-s),
|
||||
var(--color-text-base-l)
|
||||
);
|
||||
|
||||
--color-foreground-base-h: 220;
|
||||
--color-foreground-base-s: 20%;
|
||||
--color-foreground-base-l: 12%;
|
||||
--color-foreground-base: hsl(
|
||||
var(--color-foreground-base-h),
|
||||
var(--color-foreground-base-s),
|
||||
var(--color-foreground-base-l)
|
||||
);
|
||||
--color-text-light-h: 220;
|
||||
--color-text-light-s: 4%;
|
||||
--color-text-light-l: 42%;
|
||||
--color-text-light: hsl(
|
||||
var(--color-text-light-h),
|
||||
var(--color-text-light-s),
|
||||
var(--color-text-light-l)
|
||||
);
|
||||
|
||||
--color-foreground-light-h: 0;
|
||||
--color-foreground-light-s: 0%;
|
||||
--color-foreground-light-l: 7%;
|
||||
--color-foreground-light: hsl(
|
||||
var(--color-foreground-light-h),
|
||||
var(--color-foreground-light-s),
|
||||
var(--color-foreground-light-l)
|
||||
);
|
||||
--color-text-lighter-h: 222;
|
||||
--color-text-lighter-s: 17%;
|
||||
--color-text-lighter-l: 12%;
|
||||
--color-text-lighter: hsl(
|
||||
var(--color-text-lighter-h),
|
||||
var(--color-text-lighter-s),
|
||||
var(--color-text-lighter-l)
|
||||
);
|
||||
|
||||
--color-foreground-xlight-h: 0;
|
||||
--color-foreground-xlight-s: 0%;
|
||||
--color-foreground-xlight-l: 0%;
|
||||
--color-foreground-xlight: hsl(
|
||||
var(--color-foreground-xlight-h),
|
||||
var(--color-foreground-xlight-s),
|
||||
var(--color-foreground-xlight-l)
|
||||
);
|
||||
--color-text-xlight-h: 0;
|
||||
--color-text-xlight-s: 0%;
|
||||
--color-text-xlight-l: 100%;
|
||||
--color-text-xlight: hsl(
|
||||
var(--color-text-xlight-h),
|
||||
var(--color-text-xlight-s),
|
||||
var(--color-text-xlight-l)
|
||||
);
|
||||
|
||||
--color-background-dark-h: 0;
|
||||
--color-background-dark-s: 0%;
|
||||
--color-background-dark-l: 100%;
|
||||
--color-background-dark: hsl(
|
||||
var(--color-background-dark-h),
|
||||
var(--color-background-dark-s),
|
||||
var(--color-background-dark-l)
|
||||
);
|
||||
--color-foreground-base-h: 220;
|
||||
--color-foreground-base-s: 20%;
|
||||
--color-foreground-base-l: 12%;
|
||||
--color-foreground-base: hsl(
|
||||
var(--color-foreground-base-h),
|
||||
var(--color-foreground-base-s),
|
||||
var(--color-foreground-base-l)
|
||||
);
|
||||
|
||||
--color-background-base-h: 252;
|
||||
--color-background-base-s: 71%;
|
||||
--color-background-base-l: 99%;
|
||||
--color-background-base: hsl(
|
||||
var(--color-background-base-h),
|
||||
var(--color-background-base-s),
|
||||
var(--color-background-base-l)
|
||||
);
|
||||
--color-foreground-light-h: 0;
|
||||
--color-foreground-light-s: 0%;
|
||||
--color-foreground-light-l: 7%;
|
||||
--color-foreground-light: hsl(
|
||||
var(--color-foreground-light-h),
|
||||
var(--color-foreground-light-s),
|
||||
var(--color-foreground-light-l)
|
||||
);
|
||||
|
||||
--color-background-light-h: 220;
|
||||
--color-background-light-s: 27%;
|
||||
--color-background-light-l: 98%;
|
||||
--color-background-light: hsl(
|
||||
var(--color-background-light-h),
|
||||
var(--color-background-light-s),
|
||||
var(--color-background-light-l)
|
||||
);
|
||||
--color-foreground-xlight-h: 0;
|
||||
--color-foreground-xlight-s: 0%;
|
||||
--color-foreground-xlight-l: 0%;
|
||||
--color-foreground-xlight: hsl(
|
||||
var(--color-foreground-xlight-h),
|
||||
var(--color-foreground-xlight-s),
|
||||
var(--color-foreground-xlight-l)
|
||||
);
|
||||
|
||||
--color-background-lighter-h: 220;
|
||||
--color-background-lighter-s: 30%;
|
||||
--color-background-lighter-l: 96%;
|
||||
--color-background-lighter: hsl(
|
||||
var(--color-background-lighter-h),
|
||||
var(--color-background-lighter-s),
|
||||
var(--color-background-lighter-l)
|
||||
);
|
||||
--color-background-dark-h: 0;
|
||||
--color-background-dark-s: 0%;
|
||||
--color-background-dark-l: 100%;
|
||||
--color-background-dark: hsl(
|
||||
var(--color-background-dark-h),
|
||||
var(--color-background-dark-s),
|
||||
var(--color-background-dark-l)
|
||||
);
|
||||
|
||||
--color-background-xlight-h: 240;
|
||||
--color-background-xlight-s: 4%;
|
||||
--color-background-xlight-l: 19%;
|
||||
--color-background-xlight: hsl(
|
||||
var(--color-background-xlight-h),
|
||||
var(--color-background-xlight-s),
|
||||
var(--color-background-xlight-l)
|
||||
);
|
||||
--color-background-base-h: 252;
|
||||
--color-background-base-s: 71%;
|
||||
--color-background-base-l: 99%;
|
||||
--color-background-base: hsl(
|
||||
var(--color-background-base-h),
|
||||
var(--color-background-base-s),
|
||||
var(--color-background-base-l)
|
||||
);
|
||||
|
||||
--color-background-light-h: 220;
|
||||
--color-background-light-s: 27%;
|
||||
--color-background-light-l: 98%;
|
||||
--color-background-light: hsl(
|
||||
var(--color-background-light-h),
|
||||
var(--color-background-light-s),
|
||||
var(--color-background-light-l)
|
||||
);
|
||||
|
||||
--color-background-lighter-h: 220;
|
||||
--color-background-lighter-s: 30%;
|
||||
--color-background-lighter-l: 96%;
|
||||
--color-background-lighter: hsl(
|
||||
var(--color-background-lighter-h),
|
||||
var(--color-background-lighter-s),
|
||||
var(--color-background-lighter-l)
|
||||
);
|
||||
|
||||
--color-background-xlight-h: 240;
|
||||
--color-background-xlight-s: 4%;
|
||||
--color-background-xlight-l: 19%;
|
||||
--color-background-xlight: hsl(
|
||||
var(--color-background-xlight-h),
|
||||
var(--color-background-xlight-s),
|
||||
var(--color-background-xlight-l)
|
||||
);
|
||||
|
||||
// Generated Color Shades from 50 to 950
|
||||
// Not yet used in design system
|
||||
@each $color in ('neutral', 'success', 'warning', 'danger') {
|
||||
@each $shade in (50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950) {
|
||||
--color-#{$color}-#{$shade}-l: #{math.div($shade, 10)}#{'%'};
|
||||
--color-#{$color}-#{$shade}: hsl(var(--color-#{$color}-h), var(--color-#{$color}-s), var(--color-#{$color}-#{$shade}-l));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.theme-dark {
|
||||
@include theme;
|
||||
@include theme;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'sass:math';
|
||||
|
||||
@mixin theme {
|
||||
--color-primary-h: 6.9;
|
||||
--color-primary-s: 100%;
|
||||
|
@ -86,6 +88,14 @@
|
|||
var(--color-secondary-tint-2-l)
|
||||
);
|
||||
|
||||
--color-secondary-tint-3-h: 247;
|
||||
--color-secondary-tint-3-s: 49%;
|
||||
--color-secondary-tint-3-l: 95%;
|
||||
--color-secondary-tint-3: hsl(
|
||||
var(--color-secondary-tint-3-h),
|
||||
var(--color-secondary-tint-3-s),
|
||||
var(--color-secondary-tint-3-l)
|
||||
);
|
||||
|
||||
--color-success-h: 150.4;
|
||||
--color-success-s: 60%;
|
||||
|
@ -190,6 +200,33 @@
|
|||
var(--color-info-tint-2-l)
|
||||
);
|
||||
|
||||
--color-grey-h: 228;
|
||||
--color-grey-s: 10%;
|
||||
--color-grey-l: 80%;
|
||||
--color-grey: hsl(
|
||||
var(--color-grey-h),
|
||||
var(--color-grey-s),
|
||||
var(--color-grey-l)
|
||||
);
|
||||
|
||||
--color-light-grey-h: 220;
|
||||
--color-light-grey-s: 20%;
|
||||
--color-light-grey-l: 88%;
|
||||
--color-light-grey: hsl(
|
||||
var(--color-light-grey-h),
|
||||
var(--color-light-grey-s),
|
||||
var(--color-light-grey-l)
|
||||
);
|
||||
|
||||
--color-neutral-h: 228;
|
||||
--color-neutral-s: 10%;
|
||||
--color-neutral-l: 50%;
|
||||
--color-neutral: hsl(
|
||||
var(--color-neutral-h),
|
||||
var(--color-neutral-s),
|
||||
var(--color-neutral-l)
|
||||
);
|
||||
|
||||
--color-text-dark-h: 0;
|
||||
--color-text-dark-s: 0%;
|
||||
--color-text-dark-l: 33.3%;
|
||||
|
@ -372,6 +409,19 @@
|
|||
var(--color-sticky-default-border-l)
|
||||
);
|
||||
|
||||
// Generated Color Shades from 50 to 950
|
||||
// Not yet used in design system
|
||||
@each $color in ('neutral', 'success', 'warning', 'danger') {
|
||||
@each $shade in (50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950) {
|
||||
--color-#{$color}-#{$shade}-l: #{math.div($shade, 10)}#{'%'};
|
||||
--color-#{$color}-#{$shade}: hsl(
|
||||
var(--color-#{$color}-h),
|
||||
var(--color-#{$color}-s),
|
||||
var(--color-#{$color}-#{$shade}-l)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
--border-radius-xlarge: 12px;
|
||||
--border-radius-large: 8px;
|
||||
--border-radius-base: 4px;
|
||||
|
@ -418,5 +468,5 @@
|
|||
}
|
||||
|
||||
:root {
|
||||
@include theme;
|
||||
@include theme;
|
||||
}
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
@charset "UTF-8";
|
||||
@use 'mixins/mixins';
|
||||
@use 'mixins/utils';
|
||||
@use 'mixins/function';
|
||||
@use 'common/var';
|
||||
|
||||
$disabled-border-color: var(--color-foreground-base);
|
||||
|
||||
$loading-overlay-background-color: rgba(255, 255, 255, 0.35);
|
||||
|
||||
@include mixins.b(button) {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
border: var(--border-width-base) var.$button-border-color
|
||||
var(--border-style-base);
|
||||
color: var.$button-font-color;
|
||||
background-color: var.$button-background-color;
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-radius: var.$button-border-radius;
|
||||
padding: var.$button-padding-vertical var.$button-padding-horizontal;
|
||||
font-size: var.$button-font-size;
|
||||
|
||||
-webkit-appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.1s;
|
||||
|
||||
@include utils.utils-user-select(none);
|
||||
|
||||
&:active {
|
||||
color: var.$button-active-color;
|
||||
border-color: var.$button-active-border-color;
|
||||
background-color: var.$button-active-background-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@include mixins.when(loading) {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
&:before {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border-radius: inherit;
|
||||
background-color: $loading-overlay-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.when(disabled) {
|
||||
&,
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: not-allowed;
|
||||
background-image: none;
|
||||
color: var.$button-disabled-font-color;
|
||||
background-color: var.$button-disabled-background-color;
|
||||
border-color: var.$button-disabled-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.when(round) {
|
||||
--button-border-radius: 20px;
|
||||
}
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
--button-border-radius: 50%;
|
||||
}
|
||||
|
||||
@include mixins.m(mini) {
|
||||
--button-padding-vertical: var(--spacing-4xs);
|
||||
--button-padding-horizontal: var(--spacing-2xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-4xs);
|
||||
--button-padding-horizontal: var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(small) {
|
||||
--button-padding-vertical: var(--spacing-3xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-3xs);
|
||||
--button-padding-horizontal: var(--spacing-3xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(medium) {
|
||||
--button-padding-vertical: var(--spacing-2xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-2xs);
|
||||
--button-padding-horizontal: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(xlarge) {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-s);
|
||||
--button-font-size: var(--font-size-m);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
@use "mixins/mixins";
|
||||
@use "./common/var";
|
||||
@use "button";
|
||||
@use "button-group";
|
||||
|
||||
@include mixins.b(calendar) {
|
||||
|
|
|
@ -3,380 +3,376 @@
|
|||
@use "./common/var";
|
||||
|
||||
@include mixins.b(color-predefine) {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
width: 280px;
|
||||
|
||||
@include mixins.e(colors) {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
width: 280px;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@include mixins.e(colors) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
@include mixins.e(color-selector) {
|
||||
margin: 0 0 8px 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:nth-child(10n + 1) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@include mixins.e(color-selector) {
|
||||
margin: 0 0 8px 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:nth-child(10n + 1) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0 0 3px 2px var(--color-primary);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@include mixins.when(alpha) {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
&.selected {
|
||||
box-shadow: 0 0 3px 2px var(--color-primary);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@include mixins.when(alpha) {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.b(color-hue-slider) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
height: 12px;
|
||||
background-color: #f00;
|
||||
padding: 0 2px;
|
||||
|
||||
@include mixins.e(bar) {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#f00 0%,
|
||||
#ff0 17%,
|
||||
#0f0 33%,
|
||||
#0ff 50%,
|
||||
#00f 67%,
|
||||
#f0f 83%,
|
||||
#f00 100%
|
||||
);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@include mixins.e(thumb) {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
height: 12px;
|
||||
background-color: #f00;
|
||||
padding: 0 2px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mixins.e(bar) {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#f00 0%,
|
||||
#ff0 17%,
|
||||
#0f0 33%,
|
||||
#0ff 50%,
|
||||
#00f 67%,
|
||||
#f0f 83%,
|
||||
#f00 100%
|
||||
);
|
||||
height: 100%;
|
||||
@include mixins.when(vertical) {
|
||||
width: 12px;
|
||||
height: 180px;
|
||||
padding: 2px 0;
|
||||
|
||||
.el-color-hue-slider__bar {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
#f00 0%,
|
||||
#ff0 17%,
|
||||
#0f0 33%,
|
||||
#0ff 50%,
|
||||
#00f 67%,
|
||||
#f0f 83%,
|
||||
#f00 100%
|
||||
);
|
||||
}
|
||||
|
||||
@include mixins.e(thumb) {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mixins.when(vertical) {
|
||||
width: 12px;
|
||||
height: 180px;
|
||||
padding: 2px 0;
|
||||
|
||||
.el-color-hue-slider__bar {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
#f00 0%,
|
||||
#ff0 17%,
|
||||
#0f0 33%,
|
||||
#0ff 50%,
|
||||
#00f 67%,
|
||||
#f0f 83%,
|
||||
#f00 100%
|
||||
);
|
||||
}
|
||||
|
||||
.el-color-hue-slider__thumb {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
.el-color-hue-slider__thumb {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.b(color-svpanel) {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 180px;
|
||||
|
||||
@include mixins.e(('white', 'black')) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@include mixins.e('white') {
|
||||
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
@include mixins.e('black') {
|
||||
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@include mixins.e(cursor) {
|
||||
position: absolute;
|
||||
|
||||
> div {
|
||||
cursor: head;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, 0.3),
|
||||
0 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
@include mixins.e(('white', 'black')) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@include mixins.e('white') {
|
||||
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
@include mixins.e('black') {
|
||||
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@include mixins.e(cursor) {
|
||||
position: absolute;
|
||||
|
||||
> div {
|
||||
cursor: head;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, 0.3),
|
||||
0 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.b(color-alpha-slider) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
height: 12px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
|
||||
@include mixins.e(bar) {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@include mixins.e(thumb) {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
height: 12px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mixins.e(bar) {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
height: 100%;
|
||||
@include mixins.when(vertical) {
|
||||
width: 20px;
|
||||
height: 180px;
|
||||
|
||||
.el-color-alpha-slider__bar {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
@include mixins.e(thumb) {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mixins.when(vertical) {
|
||||
width: 20px;
|
||||
height: 180px;
|
||||
|
||||
.el-color-alpha-slider__bar {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.el-color-alpha-slider__thumb {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
.el-color-alpha-slider__thumb {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.b(color-dropdown) {
|
||||
width: 300px;
|
||||
width: 300px;
|
||||
|
||||
@include mixins.e(main-wrapper) {
|
||||
margin-bottom: 6px;
|
||||
@include mixins.e(main-wrapper) {
|
||||
margin-bottom: 6px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.e(btns) {
|
||||
margin-top: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@include mixins.e(btns) {
|
||||
margin-top: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@include mixins.e(value) {
|
||||
float: left;
|
||||
line-height: 26px;
|
||||
font-size: 12px;
|
||||
color: var.$color-black;
|
||||
width: 160px;
|
||||
}
|
||||
@include mixins.e(value) {
|
||||
float: left;
|
||||
line-height: 26px;
|
||||
font-size: 12px;
|
||||
color: var.$color-black;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
@include mixins.e(btn) {
|
||||
@include button.button-round();
|
||||
@include button.button-small();
|
||||
@include button.button-just-primary();
|
||||
}
|
||||
@include mixins.e(btn) {
|
||||
@include button.button-small();
|
||||
@include button.button-just-primary();
|
||||
}
|
||||
|
||||
@include mixins.e(link-btn) {
|
||||
@include button.button-round();
|
||||
@include button.button-outline();
|
||||
@include button.button-small();
|
||||
|
||||
margin-right: var(--spacing-2xs);
|
||||
}
|
||||
@include mixins.e(link-btn) {
|
||||
@include button.button-small();
|
||||
margin-right: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.b(color-picker) {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
height: 40px;
|
||||
|
||||
@include mixins.when(disabled) {
|
||||
.el-color-picker__trigger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(medium) {
|
||||
height: 36px;
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(small) {
|
||||
height: 32px;
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.el-color-picker__icon,
|
||||
.el-color-picker__empty {
|
||||
transform: translate3d(-50%, -50%, 0) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(mini) {
|
||||
height: 28px;
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.el-color-picker__icon,
|
||||
.el-color-picker__empty {
|
||||
transform: translate3d(-50%, -50%, 0) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.e(mask) {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
z-index: 1;
|
||||
cursor: not-allowed;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@include mixins.e(trigger) {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
font-size: 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@include mixins.when(disabled) {
|
||||
.el-color-picker__trigger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@include mixins.e(color) {
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #999;
|
||||
border-radius: var(--border-radius-small);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
@include mixins.when(alpha) {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(medium) {
|
||||
height: 36px;
|
||||
@include mixins.e(color-inner) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
@include mixins.e(empty) {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
}
|
||||
@include mixins.e(icon) {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
color: var.$color-white;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@include mixins.m(small) {
|
||||
height: 32px;
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.el-color-picker__icon,
|
||||
.el-color-picker__empty {
|
||||
transform: translate3d(-50%, -50%, 0) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(mini) {
|
||||
height: 28px;
|
||||
|
||||
.el-color-picker__trigger {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.el-color-picker__mask {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.el-color-picker__icon,
|
||||
.el-color-picker__empty {
|
||||
transform: translate3d(-50%, -50%, 0) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.e(mask) {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
z-index: 1;
|
||||
cursor: not-allowed;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@include mixins.e(trigger) {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
font-size: 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@include mixins.e(color) {
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #999;
|
||||
border-radius: var(--border-radius-small);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
@include mixins.when(alpha) {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.e(color-inner) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@include mixins.e(empty) {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
@include mixins.e(icon) {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
color: var.$color-white;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@include mixins.e(panel) {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 6px;
|
||||
box-sizing: content-box;
|
||||
background-color: var.$color-white;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: var.$dropdown-menu-box-shadow;
|
||||
}
|
||||
@include mixins.e(panel) {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 6px;
|
||||
box-sizing: content-box;
|
||||
background-color: var.$color-white;
|
||||
border: 1px solid var(--border-color-light);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: var.$dropdown-menu-box-shadow;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ $all-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|||
$fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
$fade-linear-transition: opacity 200ms linear;
|
||||
$md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
$border-transition-base: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
$color-transition-base: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
|
@ -21,190 +21,190 @@ $color-white: #ffffff;
|
|||
$color-black: #000000;
|
||||
|
||||
$color-primary-light-1: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
4%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
4%
|
||||
);
|
||||
$color-primary-light-2: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
8%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
8%
|
||||
);
|
||||
$color-primary-light-3: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
12%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
12%
|
||||
);
|
||||
$color-primary-light-4: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
16%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
16%
|
||||
);
|
||||
$color-primary-light-5: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
20%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
20%
|
||||
);
|
||||
$color-primary-light-6: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
24%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
24%
|
||||
);
|
||||
$color-primary-light-7: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
28%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
28%
|
||||
);
|
||||
$color-primary-light-8: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
32%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
32%
|
||||
);
|
||||
$color-primary-light-9: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
36%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
36%
|
||||
);
|
||||
$color-primary-light: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
30%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
30%
|
||||
);
|
||||
$color-primary-lighter: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
34%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
34%
|
||||
);
|
||||
|
||||
$color-primary-shade-1: function.lightness(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-(10)
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
-(10)
|
||||
);
|
||||
|
||||
$color-success-light-1: function.lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
4%
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
4%
|
||||
);
|
||||
$color-success-light-3: function.lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
12%
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
12%
|
||||
);
|
||||
$color-success-light-5: function.lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
20%
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
20%
|
||||
);
|
||||
$color-success-light: function.lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
41%
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
41%
|
||||
);
|
||||
$color-success-lighter: function.lightness(
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
48%
|
||||
--color-success-h,
|
||||
--color-success-s,
|
||||
--color-success-l,
|
||||
48%
|
||||
);
|
||||
|
||||
$color-warning-light-1: function.lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
4%
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
4%
|
||||
);
|
||||
$color-warning-light-3: function.lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
12%
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
12%
|
||||
);
|
||||
$color-warning-light-5: function.lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
20%
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
20%
|
||||
);
|
||||
$color-warning-light: function.lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
34%
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
34%
|
||||
);
|
||||
$color-warning-lighter: function.lightness(
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
40%
|
||||
--color-warning-h,
|
||||
--color-warning-s,
|
||||
--color-warning-l,
|
||||
40%
|
||||
);
|
||||
|
||||
$color-danger-light-1: function.lightness(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
4%
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
4%
|
||||
);
|
||||
$color-danger-light-3: function.lightness(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
12%
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
12%
|
||||
);
|
||||
$color-danger-light-5: function.lightness(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
20%
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
20%
|
||||
);
|
||||
$color-danger-light: function.lightness(
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
24%
|
||||
--color-danger-h,
|
||||
--color-danger-s,
|
||||
--color-danger-l,
|
||||
24%
|
||||
);
|
||||
$color-danger-lighter: var(--color-danger-tint-2);
|
||||
|
||||
$color-info-light-1: function.lightness(
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
4%
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
4%
|
||||
);
|
||||
$color-info-light-3: function.lightness(
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
12%
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
12%
|
||||
);
|
||||
$color-info-light-5: function.lightness(
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
20%
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
20%
|
||||
);
|
||||
$color-info-lighter: function.lightness(
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
39%
|
||||
--color-info-h,
|
||||
--color-info-s,
|
||||
--color-info-l,
|
||||
39%
|
||||
);
|
||||
|
||||
// Background
|
||||
|
@ -222,7 +222,12 @@ $border-color-hover: var(--color-text-lighter);
|
|||
/// borderRadius|1|Radius|0
|
||||
$border-radius-circle: 100%;
|
||||
|
||||
// Box-shadow
|
||||
/* Outline
|
||||
-------------------------- */
|
||||
$focus-outline-width: 2px;
|
||||
|
||||
/* Box shadow
|
||||
-------------------------- */
|
||||
/// boxShadow|1|Shadow|1
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
// boxShadow|1|Shadow|1
|
||||
|
@ -666,14 +671,30 @@ $button-mini-padding-horizontal: 15px;
|
|||
|
||||
$button-font-color: var(--button-color, var(--color-text-xlight));
|
||||
$button-background-color: var(--button-background-color, var(--color-primary));
|
||||
|
||||
$button-active-color: var(--button-active-color, var(--color-text-xlight));
|
||||
$button-active-border-color: var(
|
||||
--button-active-border-color,
|
||||
var(--color-primary-shade-1)
|
||||
--button-active-border-color,
|
||||
var(--color-primary-shade-1)
|
||||
);
|
||||
$button-active-background-color: var(
|
||||
--button-active-background-color,
|
||||
var(--color-primary-shade-1)
|
||||
--button-active-background-color,
|
||||
var(--color-primary-shade-1)
|
||||
);
|
||||
|
||||
$button-focus-outline-color: var(
|
||||
--button-focus-outline-color,
|
||||
var(--color-primary-tint-1)
|
||||
);
|
||||
|
||||
$button-hover-color: var(--button-hover-color, var(--color-text-xlight));
|
||||
$button-hover-border-color: var(
|
||||
--button-hover-border-color,
|
||||
var(--color-primary-shade-1)
|
||||
);
|
||||
$button-hover-background-color: var(
|
||||
--button-hover-background-color,
|
||||
var(--color-primary-shade-1)
|
||||
);
|
||||
|
||||
/// color||Color|0
|
||||
|
@ -684,14 +705,17 @@ $button-default-background-color: $color-white;
|
|||
$button-default-border-color: var(--border-color-base);
|
||||
|
||||
/// color||Color|0
|
||||
$button-disabled-font-color: var(--color-text-light);
|
||||
/// color||Color|0
|
||||
$button-disabled-background-color: var(
|
||||
--button-disabled-background-color,
|
||||
var(--color-foreground-base)
|
||||
$button-disabled-font-color: var(
|
||||
--button-disabled-color,
|
||||
var(--color-text-dark)
|
||||
);
|
||||
/// color||Color|0
|
||||
$button-disabled-border-color: var(--button-disabled-border-color, var(--border-color-base));
|
||||
$button-disabled-background-color: var(
|
||||
--button-disabled-background-color,
|
||||
var(--color-light-grey)
|
||||
);
|
||||
/// color||Color|0
|
||||
$button-disabled-border-color: var(--button-disabled-border-color, var(--color-light-grey));
|
||||
|
||||
/// color||Color|0
|
||||
$button-primary-border-color: var(--color-primary);
|
||||
|
@ -799,7 +823,7 @@ $pagination-hover-color: var(--color-primary);
|
|||
/* Popup
|
||||
-------------------------- */
|
||||
/// color||Color|0
|
||||
$popup-modal-background-color: hsla(247,14%, 30%, 0.75);
|
||||
$popup-modal-background-color: hsla(247, 14%, 30%, 0.75);
|
||||
/// opacity||Other|1
|
||||
$popup-modal-opacity: 0.65;
|
||||
|
||||
|
@ -893,10 +917,10 @@ $slider-main-background-color: var(--color-primary);
|
|||
/// color||Color|0
|
||||
$slider-runway-background-color: var(--border-color-base);
|
||||
$slider-button-hover-color: function.saturation(
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
8%
|
||||
--color-primary-h,
|
||||
--color-primary-s,
|
||||
--color-primary-l,
|
||||
8%
|
||||
);
|
||||
$slider-stop-background-color: $color-white;
|
||||
$slider-disable-color: var(--color-text-lighter);
|
||||
|
@ -974,16 +998,16 @@ $loading-fullscreen-spinner-size: 50px;
|
|||
/* Scrollbar
|
||||
--------------------------*/
|
||||
$scrollbar-background-color: hsla(
|
||||
var(#{--color-text-light-h}),
|
||||
var(#{--color-text-light-s}),
|
||||
var(#{--color-text-light-l}),
|
||||
0.3
|
||||
var(#{--color-text-light-h}),
|
||||
var(#{--color-text-light-s}),
|
||||
var(#{--color-text-light-l}),
|
||||
0.3
|
||||
);
|
||||
$scrollbar-hover-background-color: hsla(
|
||||
var(#{--color-text-light-h}),
|
||||
var(#{--color-text-light-s}),
|
||||
var(#{--color-text-light-l}),
|
||||
0.5
|
||||
var(#{--color-text-light-h}),
|
||||
var(#{--color-text-light-s}),
|
||||
var(#{--color-text-light-l}),
|
||||
0.5
|
||||
);
|
||||
|
||||
/* Carousel
|
||||
|
@ -1128,67 +1152,67 @@ $lg: 1200px;
|
|||
$xl: 1920px;
|
||||
|
||||
$breakpoints: (
|
||||
'xs': (
|
||||
max-width: $sm - 1,
|
||||
),
|
||||
'sm': (
|
||||
min-width: $sm,
|
||||
),
|
||||
'md': (
|
||||
min-width: $md,
|
||||
),
|
||||
'lg': (
|
||||
min-width: $lg,
|
||||
),
|
||||
'xl': (
|
||||
min-width: $xl,
|
||||
),
|
||||
'xs': (
|
||||
max-width: $sm - 1,
|
||||
),
|
||||
'sm': (
|
||||
min-width: $sm,
|
||||
),
|
||||
'md': (
|
||||
min-width: $md,
|
||||
),
|
||||
'lg': (
|
||||
min-width: $lg,
|
||||
),
|
||||
'xl': (
|
||||
min-width: $xl,
|
||||
),
|
||||
);
|
||||
|
||||
$breakpoints-spec: (
|
||||
'xs-only': (
|
||||
max-width: $sm - 1,
|
||||
),
|
||||
'sm-and-up': (
|
||||
min-width: $sm,
|
||||
),
|
||||
'sm-only': (
|
||||
'xs-only': (
|
||||
max-width: $sm - 1,
|
||||
),
|
||||
'sm-and-up': (
|
||||
min-width: $sm,
|
||||
),
|
||||
'sm-only': (
|
||||
min-width: #{$sm},
|
||||
)
|
||||
and
|
||||
(
|
||||
max-width: #{$md - 1},
|
||||
),
|
||||
'sm-and-down': (
|
||||
max-width: $md - 1,
|
||||
),
|
||||
'md-and-up': (
|
||||
min-width: $md,
|
||||
),
|
||||
'md-only': (
|
||||
'sm-and-down': (
|
||||
max-width: $md - 1,
|
||||
),
|
||||
'md-and-up': (
|
||||
min-width: $md,
|
||||
),
|
||||
'md-only': (
|
||||
min-width: #{$md},
|
||||
)
|
||||
and
|
||||
(
|
||||
max-width: #{$lg - 1},
|
||||
),
|
||||
'md-and-down': (
|
||||
max-width: $lg - 1,
|
||||
),
|
||||
'lg-and-up': (
|
||||
min-width: $lg,
|
||||
),
|
||||
'lg-only': (
|
||||
'md-and-down': (
|
||||
max-width: $lg - 1,
|
||||
),
|
||||
'lg-and-up': (
|
||||
min-width: $lg,
|
||||
),
|
||||
'lg-only': (
|
||||
min-width: #{$lg},
|
||||
)
|
||||
and
|
||||
(
|
||||
max-width: #{$xl - 1},
|
||||
),
|
||||
'lg-and-down': (
|
||||
max-width: $xl - 1,
|
||||
),
|
||||
'xl-only': (
|
||||
min-width: $xl,
|
||||
),
|
||||
'lg-and-down': (
|
||||
max-width: $xl - 1,
|
||||
),
|
||||
'xl-only': (
|
||||
min-width: $xl,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
@use "mixins/mixins";
|
||||
@use "./common/var";
|
||||
@use "button";
|
||||
@use "./popper";
|
||||
|
||||
@include mixins.b(dropdown) {
|
||||
|
@ -35,7 +34,7 @@
|
|||
background: rgba(var.$color-white, 0.5);
|
||||
}
|
||||
|
||||
&.el-button--default::before {
|
||||
&.button::before {
|
||||
background: hsla(
|
||||
var(#{--button-default-border-color-h}),
|
||||
var(#{--button-default-border-color-s}),
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
@use "./switch.scss";
|
||||
@use "./select.scss";
|
||||
@use "./skeleton.scss";
|
||||
@use "./button.scss";
|
||||
// @use "./button-group.scss";
|
||||
@use "./table.scss";
|
||||
@use "./table-column.scss";
|
||||
|
@ -82,4 +81,4 @@
|
|||
// @use "./avatar.scss";
|
||||
@use "./drawer.scss";
|
||||
// @use "./popconfirm.scss";
|
||||
@use "./utilities.scss";
|
||||
@use "./utilities/index.scss";
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
@use "mixins/button";
|
||||
@use "./common/var";
|
||||
@use "common/popup";
|
||||
@use "button" as buttons;
|
||||
@use "input";
|
||||
|
||||
@include mixins.b(message-box) {
|
||||
|
@ -151,15 +150,13 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
& .btn--confirm {
|
||||
@include button.button-just-primary();
|
||||
@include button.button-medium();
|
||||
}
|
||||
|
||||
& .btn--cancel {
|
||||
@include button.button-outline();
|
||||
@include button.button-medium();
|
||||
}
|
||||
//& .btn--confirm {
|
||||
// @include button.button-just-primary();
|
||||
// @include button.button-medium();
|
||||
//}
|
||||
//
|
||||
//& .btn--cancel {
|
||||
//}
|
||||
}
|
||||
|
||||
@include mixins.e(btns-reverse) {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
@use "mixins/utils";
|
||||
@use "./common/var";
|
||||
@use "input";
|
||||
@use "button";
|
||||
@use "checkbox";
|
||||
@use "checkbox-group";
|
||||
|
||||
|
@ -15,7 +14,7 @@
|
|||
padding: 0 30px;
|
||||
}
|
||||
|
||||
@include mixins.e(button) {
|
||||
.button {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
|
|
7
packages/design-system/theme/src/utilities/_float.scss
Normal file
7
packages/design-system/theme/src/utilities/_float.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
.float-left {
|
||||
float: left !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right !important;
|
||||
}
|
2
packages/design-system/theme/src/utilities/index.scss
Normal file
2
packages/design-system/theme/src/utilities/index.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import 'float';
|
||||
@import 'spacing';
|
|
@ -21,6 +21,7 @@ import {
|
|||
ITelemetrySettings,
|
||||
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||
WorkflowExecuteMode,
|
||||
PinData,
|
||||
PublicInstalledPackage,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -150,6 +151,7 @@ export interface INodeUi extends INode {
|
|||
notes?: string;
|
||||
issues?: INodeIssues;
|
||||
name: string;
|
||||
pinData?: IDataObject;
|
||||
}
|
||||
|
||||
export interface INodeTypesMaxCount {
|
||||
|
@ -216,6 +218,7 @@ export interface IStartRunData {
|
|||
startNodes?: string[];
|
||||
destinationNode?: string;
|
||||
runData?: IRunData;
|
||||
pinData?: PinData;
|
||||
}
|
||||
|
||||
export interface IRunDataUi {
|
||||
|
@ -250,6 +253,7 @@ export interface IWorkflowData {
|
|||
connections: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
tags?: string[];
|
||||
pinData?: PinData;
|
||||
}
|
||||
|
||||
export interface IWorkflowDataUpdate {
|
||||
|
@ -260,6 +264,7 @@ export interface IWorkflowDataUpdate {
|
|||
settings?: IWorkflowSettings;
|
||||
active?: boolean;
|
||||
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
||||
pinData?: PinData;
|
||||
}
|
||||
|
||||
export interface IWorkflowTemplate {
|
||||
|
@ -282,6 +287,7 @@ export interface IWorkflowDb {
|
|||
connections: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
||||
pinData?: PinData;
|
||||
}
|
||||
|
||||
// Identical to cli.Interfaces.ts
|
||||
|
@ -907,6 +913,10 @@ export interface IUiState {
|
|||
};
|
||||
output: {
|
||||
displayMode: IRunDataDisplayMode;
|
||||
editMode: {
|
||||
enabled: boolean;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
focusedMappableInput: string;
|
||||
mappingTelemetry: {[key: string]: string | number | boolean};
|
||||
|
|
|
@ -27,11 +27,10 @@
|
|||
v-if="buttonLabel"
|
||||
:label="buttonLoading && buttonLoadingLabel ? buttonLoadingLabel : buttonLabel"
|
||||
:title="buttonTitle"
|
||||
:theme="theme"
|
||||
:type="theme"
|
||||
:loading="buttonLoading"
|
||||
size="small"
|
||||
type="outline"
|
||||
:transparentBackground="true"
|
||||
outline
|
||||
@click.stop="onClick"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -8,15 +8,12 @@
|
|||
:before-close="closeDialog"
|
||||
>
|
||||
<div class="text-editor-wrapper ignore-key-press">
|
||||
<div ref="code" class="text-editor" @keydown.stop></div>
|
||||
<code-editor :value="value" :autocomplete="loadAutocompleteData" @input="$emit('valueChanged', $event)" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import {DateTime} from 'luxon';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
|
@ -34,106 +31,19 @@ import {
|
|||
import {
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
} from '@/constants';
|
||||
import { CodeEditor } from './forms';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
workflowHelpers,
|
||||
).extend({
|
||||
name: 'CodeEdit',
|
||||
components: {
|
||||
CodeEditor,
|
||||
},
|
||||
props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value'],
|
||||
data() {
|
||||
return {
|
||||
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
|
||||
monacoLibrary: null as monaco.IDisposable | null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(this.loadEditor);
|
||||
},
|
||||
destroyed() {
|
||||
if (this.monacoLibrary) {
|
||||
this.monacoLibrary.dispose();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
|
||||
createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] {
|
||||
if (inputData === null || inputData === undefined) {
|
||||
return inputData;
|
||||
} else if (typeof inputData === 'string') {
|
||||
return '';
|
||||
} else if (typeof inputData === 'boolean') {
|
||||
return true;
|
||||
} else if (typeof inputData === 'number') {
|
||||
return 1;
|
||||
} else if (Array.isArray(inputData)) {
|
||||
return inputData.map(value => this.createSimpleRepresentation(value));
|
||||
} else if (typeof inputData === 'object') {
|
||||
const returnData: { [key: string]: object } = {};
|
||||
Object.keys(inputData).forEach(key => {
|
||||
// @ts-ignore
|
||||
returnData[key] = this.createSimpleRepresentation(inputData[key]);
|
||||
});
|
||||
return returnData;
|
||||
}
|
||||
return inputData;
|
||||
},
|
||||
|
||||
loadEditor() {
|
||||
if (!this.$refs.code) return;
|
||||
|
||||
this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, {
|
||||
automaticLayout: true,
|
||||
value: this.value,
|
||||
language: this.type === 'code' ? 'javascript' : 'json',
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: false,
|
||||
readOnly: this.isReadOnly,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
this.monacoInstance.onDidChangeModelContent(() => {
|
||||
const model = this.monacoInstance!.getModel();
|
||||
if (model) {
|
||||
this.$emit('valueChanged', model.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('n8nCustomTheme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#f5f2f0',
|
||||
},
|
||||
});
|
||||
monaco.editor.setTheme('n8nCustomTheme');
|
||||
|
||||
if (this.type === 'code') {
|
||||
// As wordBasedSuggestions: false does not have any effect does it however seem
|
||||
// to remove all all suggestions from the editor if I do this
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
|
||||
this.loadAutocompleteData();
|
||||
} else if (this.type === 'json') {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadAutocompleteData(): void {
|
||||
loadAutocompleteData(): string[] {
|
||||
if (['function', 'functionItem'].includes(this.codeAutocomplete)) {
|
||||
const itemIndex = 0;
|
||||
const inputName = 'main';
|
||||
|
@ -149,8 +59,6 @@ export default mixins(
|
|||
destinationIndex: 0,
|
||||
};
|
||||
|
||||
const autocompleteData: string[] = [];
|
||||
|
||||
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
|
||||
|
||||
let runExecutionData: IRunExecutionData;
|
||||
|
@ -262,17 +170,44 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
autoCompleteItems.join('\n'),
|
||||
);
|
||||
return autoCompleteItems;
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
closeDialog() {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
|
||||
createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] {
|
||||
if (inputData === null || inputData === undefined) {
|
||||
return inputData;
|
||||
} else if (typeof inputData === 'string') {
|
||||
return '';
|
||||
} else if (typeof inputData === 'boolean') {
|
||||
return true;
|
||||
} else if (typeof inputData === 'number') {
|
||||
return 1;
|
||||
} else if (Array.isArray(inputData)) {
|
||||
return inputData.map(value => this.createSimpleRepresentation(value));
|
||||
} else if (typeof inputData === 'object') {
|
||||
const returnData: { [key: string]: object } = {};
|
||||
Object.keys(inputData).forEach(key => {
|
||||
// @ts-ignore
|
||||
returnData[key] = this.createSimpleRepresentation(inputData[key]);
|
||||
});
|
||||
return returnData;
|
||||
}
|
||||
return inputData;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.text-editor {
|
||||
min-height: 30rem;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<n8n-button
|
||||
v-if="parameter.options.length === 1"
|
||||
type="tertiary"
|
||||
fullWidth
|
||||
block
|
||||
@click="optionSelected(parameter.options[0].name)"
|
||||
:label="getPlaceholderText"
|
||||
/>
|
||||
|
@ -194,6 +194,10 @@ export default mixins(
|
|||
|
||||
.param-options {
|
||||
margin-top: var(--spacing-xs);
|
||||
|
||||
.button {
|
||||
--button-background-color: var(--color-background-base);
|
||||
}
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
<div :class="$style.credActions">
|
||||
<n8n-icon-button
|
||||
v-if="currentCredential"
|
||||
size="small"
|
||||
:title="$locale.baseText('credentialEdit.credentialEdit.delete')"
|
||||
icon="trash"
|
||||
type="text"
|
||||
size="medium"
|
||||
type="tertiary"
|
||||
:disabled="isSaving"
|
||||
:loading="isDeleting"
|
||||
@click="deleteCredential"
|
||||
|
@ -884,6 +884,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
}
|
||||
|
||||
.credActions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-right: var(--spacing-xl);
|
||||
> * {
|
||||
margin-left: var(--spacing-2xs);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<template v-slot:footer="{ close }">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button @click="save" :loading="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.save')" float="right" />
|
||||
<n8n-button type="outline" @click="close" :disabled="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.cancel')" float="right" />
|
||||
<n8n-button type="secondary" @click="close" :disabled="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.cancel')" float="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
|
|
@ -100,16 +100,14 @@
|
|||
</n8n-tooltip>
|
||||
|
||||
<el-dropdown trigger="click" @command="handleRetryClick">
|
||||
<span class="retry-button">
|
||||
<n8n-icon-button
|
||||
v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined && !scope.row.waitTill"
|
||||
type="light"
|
||||
:theme="scope.row.stoppedAt === null ? 'warning': 'danger'"
|
||||
size="mini"
|
||||
:title="$locale.baseText('executionsList.retryExecution')"
|
||||
icon="redo"
|
||||
/>
|
||||
</span>
|
||||
<n8n-icon-button
|
||||
v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined && !scope.row.waitTill"
|
||||
:type="scope.row.stoppedAt === null ? 'warning': 'danger'"
|
||||
class="ml-3xs"
|
||||
size="mini"
|
||||
:title="$locale.baseText('executionsList.retryExecution')"
|
||||
icon="redo"
|
||||
/>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}">
|
||||
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
|
||||
|
@ -763,10 +761,6 @@ export default mixins(
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selection-options {
|
||||
height: 2em;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
<n8n-button
|
||||
v-if="parameter.options.length === 1"
|
||||
type="tertiary"
|
||||
fullWidth
|
||||
block
|
||||
@click="optionSelected(parameter.options[0].name)"
|
||||
:label="getPlaceholderText"
|
||||
/>
|
||||
|
@ -286,6 +286,13 @@ export default mixins(genericHelpers)
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep {
|
||||
.button {
|
||||
--button-background-color: var(--color-background-base);
|
||||
--button-border-color: var(--color-foreground-base);
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-collection-parameter {
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@mouseleave="showTooltip = false"
|
||||
>
|
||||
<div :class="$style.tooltip">
|
||||
<n8n-tooltip placement="top" :manual="true" :value="showTooltip">
|
||||
<n8n-tooltip placement="top" manual :value="showTooltip">
|
||||
<div slot="content" v-text="nodeType.displayName"></div>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('ndv.input.noOutputData.title') }}</n8n-text>
|
||||
<n8n-tooltip v-if="!readOnly" :manual="true" :value="showDraggableHint && showDraggableHintWithDelay">
|
||||
<div slot="content" v-html="$locale.baseText('dataMapping.dragFromPreviousHint', { interpolate: { name: focusedMappableInput } })"></div>
|
||||
<NodeExecuteButton type="outline" :transparent="true" :nodeName="currentNodeName" :label="$locale.baseText('ndv.input.noOutputData.executePrevious')" @execute="onNodeExecute" telemetrySource="inputs" />
|
||||
<NodeExecuteButton type="secondary" :transparent="true" :nodeName="currentNodeName" :label="$locale.baseText('ndv.input.noOutputData.executePrevious')" @execute="onNodeExecute" telemetrySource="inputs" />
|
||||
</n8n-tooltip>
|
||||
<n8n-text v-if="!readOnly" tag="div" size="small">
|
||||
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
|
||||
|
|
|
@ -134,9 +134,10 @@ export default mixins(genericHelpers)
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.duplicate-parameter-item ~.add-item-wrapper {
|
||||
margin-top: var(--spacing-xs);
|
||||
.duplicate-parameter-item {
|
||||
~ .add-item-wrapper {
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
|
@ -154,25 +155,31 @@ export default mixins(genericHelpers)
|
|||
}
|
||||
}
|
||||
|
||||
::v-deep .duplicate-parameter-item {
|
||||
position: relative;
|
||||
::v-deep {
|
||||
.button {
|
||||
--button-background-color: var(--color-background-base);
|
||||
--button-border-color: var(--color-foreground-base);
|
||||
}
|
||||
|
||||
.multi > .delete-item{
|
||||
top: 0.1em;
|
||||
.duplicate-parameter-item {
|
||||
position: relative;
|
||||
|
||||
.multi > .delete-item{
|
||||
top: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.duplicate-parameter-input-item {
|
||||
margin: 0.5em 0 0.25em 2em;
|
||||
}
|
||||
|
||||
.duplicate-parameter-item + .duplicate-parameter-item {
|
||||
.collection-parameter-wrapper {
|
||||
border-top: 1px dashed #999;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .duplicate-parameter-input-item {
|
||||
margin: 0.5em 0 0.25em 2em;
|
||||
}
|
||||
|
||||
::v-deep .duplicate-parameter-item + .duplicate-parameter-item {
|
||||
.collection-parameter-wrapper {
|
||||
border-top: 1px dashed #999;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
<font-awesome-icon icon="clock" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<span v-else-if="hasPinData" class="node-pin-data-icon">
|
||||
<font-awesome-icon icon="thumbtack" />
|
||||
<span v-if="workflowDataItems > 1" class="items-count"> {{ workflowDataItems }}</span>
|
||||
</span>
|
||||
<span v-else-if="workflowDataItems" class="data-count">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span v-if="workflowDataItems > 1" class="items-count"> {{ workflowDataItems }}</span>
|
||||
|
@ -27,10 +31,22 @@
|
|||
</div>
|
||||
|
||||
<div class="node-trigger-tooltip__wrapper">
|
||||
<n8n-tooltip placement="top" :manual="true" :value="showTriggerNodeTooltip" popper-class="node-trigger-tooltip__wrapper--item">
|
||||
<n8n-tooltip placement="top" manual :value="showTriggerNodeTooltip" popper-class="node-trigger-tooltip__wrapper--item">
|
||||
<div slot="content" v-text="getTriggerNodeTooltip"></div>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
<n8n-tooltip
|
||||
v-if="isTriggerNode"
|
||||
placement="top"
|
||||
manual
|
||||
:value="pinDataDiscoveryTooltipVisible"
|
||||
popper-class="node-trigger-tooltip__wrapper--item"
|
||||
>
|
||||
<template #content>
|
||||
{{ $locale.baseText('node.discovery.pinData.canvas') }}
|
||||
</template>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" :disabled="this.data.disabled"/>
|
||||
|
@ -75,11 +91,12 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
import { CUSTOM_API_CALL_KEY, WAIT_TIME_UNLIMITED } from '@/constants';
|
||||
import {CUSTOM_API_CALL_KEY, LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, WAIT_TIME_UNLIMITED} from '@/constants';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { nodeBase } from '@/components/mixins/nodeBase';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { pinData } from '@/components/mixins/pinData';
|
||||
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
|
@ -95,7 +112,13 @@ import { get } from 'lodash';
|
|||
import { getStyleTokenValue, getTriggerNodeServiceName } from './helpers';
|
||||
import { INodeUi, XYPosition } from '@/Interface';
|
||||
|
||||
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
nodeBase,
|
||||
nodeHelpers,
|
||||
workflowHelpers,
|
||||
pinData,
|
||||
).extend({
|
||||
name: 'Node',
|
||||
components: {
|
||||
NodeIcon,
|
||||
|
@ -105,6 +128,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
||||
},
|
||||
hasIssues (): boolean {
|
||||
if (this.hasPinData) return false;
|
||||
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
|
||||
return true;
|
||||
}
|
||||
|
@ -257,11 +281,9 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
else if (!this.isExecuting) {
|
||||
if (this.hasIssues) {
|
||||
borderColor = getStyleTokenValue('--color-danger');
|
||||
}
|
||||
else if (this.waiting) {
|
||||
} else if (this.waiting || this.hasPinData) {
|
||||
borderColor = getStyleTokenValue('--color-secondary');
|
||||
}
|
||||
else if (this.workflowDataItems) {
|
||||
} else if (this.workflowDataItems) {
|
||||
borderColor = getStyleTokenValue('--color-success');
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +308,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
!this.isPollingTypeNode &&
|
||||
!this.isNodeDisabled &&
|
||||
this.workflowRunning &&
|
||||
this.workflowDataItems === 0 &&
|
||||
this.workflowDataItems === 0 &&
|
||||
this.isSingleActiveTriggerNode &&
|
||||
!this.isTriggerNodeTooltipEmpty &&
|
||||
!this.hasIssues &&
|
||||
|
@ -306,6 +328,13 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
this.showTriggerNodeTooltip = this.shouldShowTriggerTooltip;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (this.pinDataDiscoveryTooltipVisible) {
|
||||
this.pinDataDiscoveryTooltipVisible = false;
|
||||
setTimeout(() => {
|
||||
this.pinDataDiscoveryTooltipVisible = true;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
shouldShowTriggerTooltip(shouldShowTriggerTooltip) {
|
||||
if (shouldShowTriggerTooltip) {
|
||||
|
@ -320,6 +349,14 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
this.$emit('run', {name: this.data.name, data: newValue, waiting: !!this.waiting});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const hasSeenPinDataTooltip = localStorage.getItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG);
|
||||
if (!hasSeenPinDataTooltip) {
|
||||
this.unwatchWorkflowDataItems = this.$watch('workflowDataItems', (dataItemsCount: number) => {
|
||||
this.showPinDataDiscoveryTooltip(dataItemsCount);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setSubtitle();
|
||||
setTimeout(() => {
|
||||
|
@ -331,10 +368,22 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
isTouchActive: false,
|
||||
nodeSubtitle: '',
|
||||
showTriggerNodeTooltip: false,
|
||||
pinDataDiscoveryTooltipVisible: false,
|
||||
dragging: false,
|
||||
unwatchWorkflowDataItems: () => {},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showPinDataDiscoveryTooltip(dataItemsCount: number): void {
|
||||
if (!this.isTriggerNode) { return; }
|
||||
|
||||
if (dataItemsCount > 0) {
|
||||
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, 'true');
|
||||
|
||||
this.pinDataDiscoveryTooltipVisible = true;
|
||||
this.unwatchWorkflowDataItems();
|
||||
}
|
||||
},
|
||||
setSubtitle() {
|
||||
const nodeSubtitle = this.getNodeSubtitle(this.data, this.nodeType, this.getWorkflow()) || '';
|
||||
|
||||
|
@ -368,6 +417,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
setNodeActive () {
|
||||
this.$store.commit('setActiveNode', this.data.name);
|
||||
this.pinDataDiscoveryTooltipVisible = false;
|
||||
},
|
||||
touchStart () {
|
||||
if (this.isTouchDevice === true && this.isMacOs === false && this.isTouchActive === false) {
|
||||
|
@ -497,6 +547,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
}
|
||||
}
|
||||
|
||||
.node-pin-data-icon {
|
||||
color: var(--color-secondary);
|
||||
margin-right: 2px;
|
||||
|
||||
svg {
|
||||
height: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
placement="bottom-start"
|
||||
:value="showTriggerWaitingWarning"
|
||||
:disabled="!showTriggerWaitingWarning"
|
||||
:manual="true"
|
||||
manual
|
||||
>
|
||||
<div slot="content" :class="$style.triggerWarning">
|
||||
{{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}
|
||||
|
@ -68,6 +68,7 @@
|
|||
:runIndex="outputRun"
|
||||
:linkedRuns="linked"
|
||||
:sessionId="sessionId"
|
||||
:isReadOnly="readOnly"
|
||||
@linkRun="onLinkRunToOutput"
|
||||
@unlinkRun="() => onUnlinkRun('output')"
|
||||
@runChange="onRunOutputIndexChange"
|
||||
|
@ -123,15 +124,20 @@ import TriggerPanel from './TriggerPanel.vue';
|
|||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
BASE_NODE_SURVEY_URL,
|
||||
CRON_NODE_TYPE,
|
||||
ERROR_TRIGGER_NODE_TYPE,
|
||||
START_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { editor } from 'monaco-editor';
|
||||
import { workflowActivate } from './mixins/workflowActivate';
|
||||
import { pinData } from "@/components/mixins/pinData";
|
||||
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus';
|
||||
|
||||
export default mixins(externalHooks, nodeHelpers, workflowHelpers, workflowActivate).extend({
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
nodeHelpers,
|
||||
workflowHelpers,
|
||||
workflowActivate,
|
||||
pinData,
|
||||
).extend({
|
||||
name: 'NodeDetailsView',
|
||||
components: {
|
||||
NodeSettings,
|
||||
|
@ -158,10 +164,18 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers, workflowActiv
|
|||
triggerWaitingWarningEnabled: false,
|
||||
isDragging: false,
|
||||
mainPanelPosition: 0,
|
||||
pinDataDiscoveryTooltipVisible: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit('ui/setNDVSessionId');
|
||||
|
||||
dataPinningEventBus.$on('data-pinning-discovery', ({ isTooltipVisible }: { isTooltipVisible: boolean }) => {
|
||||
this.pinDataDiscoveryTooltipVisible = isTooltipVisible;
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
dataPinningEventBus.$off('data-pinning-discovery');
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['executionWaitingForWebhook']),
|
||||
|
@ -315,6 +329,9 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers, workflowActiv
|
|||
}
|
||||
return `${BASE_NODE_SURVEY_URL}${this.activeNodeType.name}`;
|
||||
},
|
||||
outputPanelEditMode(): { enabled: boolean; value: string; } {
|
||||
return this.$store.getters['ui/outputPanelEditMode'];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeNode(node, oldNode) {
|
||||
|
@ -331,28 +348,31 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers, workflowActiv
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const outogingConnections = this.$store.getters.outgoingConnectionsByNodeName(
|
||||
this.activeNode.name,
|
||||
) as INodeConnections;
|
||||
if (this.activeNode) {
|
||||
const outogingConnections = this.$store.getters.outgoingConnectionsByNodeName(
|
||||
this.activeNode.name,
|
||||
) as INodeConnections;
|
||||
|
||||
this.$telemetry.track('User opened node modal', {
|
||||
node_type: this.activeNodeType ? this.activeNodeType.name : '',
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
session_id: this.sessionId,
|
||||
parameters_pane_position: this.mainPanelPosition,
|
||||
input_first_connector_runs: this.maxInputRun,
|
||||
output_first_connector_runs: this.maxOutputRun,
|
||||
selected_view_inputs: this.isTriggerNode
|
||||
? 'trigger'
|
||||
: this.$store.getters['ui/inputPanelDispalyMode'],
|
||||
selected_view_outputs: this.$store.getters['ui/outputPanelDispalyMode'],
|
||||
input_connectors: this.parentNodes.length,
|
||||
output_connectors:
|
||||
outogingConnections && outogingConnections.main && outogingConnections.main.length,
|
||||
input_displayed_run_index: this.inputRun,
|
||||
output_displayed_run_index: this.outputRun,
|
||||
});
|
||||
}, 0); // wait for display mode to be set correctly
|
||||
this.$telemetry.track('User opened node modal', {
|
||||
node_type: this.activeNodeType ? this.activeNodeType.name : '',
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
session_id: this.sessionId,
|
||||
parameters_pane_position: this.mainPanelPosition,
|
||||
input_first_connector_runs: this.maxInputRun,
|
||||
output_first_connector_runs: this.maxOutputRun,
|
||||
selected_view_inputs: this.isTriggerNode
|
||||
? 'trigger'
|
||||
: this.$store.getters['ui/inputPanelDispalyMode'],
|
||||
selected_view_outputs: this.$store.getters['ui/outputPanelDispalyMode'],
|
||||
input_connectors: this.parentNodes.length,
|
||||
output_connectors:
|
||||
outogingConnections && outogingConnections.main && outogingConnections.main.length,
|
||||
input_displayed_run_index: this.inputRun,
|
||||
output_displayed_run_index: this.outputRun,
|
||||
data_pinning_tooltip_presented: this.pinDataDiscoveryTooltipVisible,
|
||||
});
|
||||
}
|
||||
}, 2000); // wait for RunData to mount and present pindata discovery tooltip
|
||||
}
|
||||
if (window.top && !this.isActiveStickyNode) {
|
||||
window.top.postMessage(JSON.stringify({ command: node ? 'openNDV' : 'closeNDV' }), '*');
|
||||
|
@ -440,10 +460,43 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers, workflowActiv
|
|||
nodeTypeSelected(nodeTypeName: string) {
|
||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||
},
|
||||
close() {
|
||||
async close() {
|
||||
if (this.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.outputPanelEditMode.enabled) {
|
||||
const shouldPinDataBeforeClosing = await this.confirmMessage(
|
||||
'',
|
||||
this.$locale.baseText('ndv.pinData.beforeClosing.title'),
|
||||
null,
|
||||
this.$locale.baseText('ndv.pinData.beforeClosing.confirm'),
|
||||
this.$locale.baseText('ndv.pinData.beforeClosing.cancel'),
|
||||
);
|
||||
|
||||
if (shouldPinDataBeforeClosing) {
|
||||
const { value } = this.outputPanelEditMode;
|
||||
|
||||
if (!this.isValidPinDataSize(value)) {
|
||||
dataPinningEventBus.$emit(
|
||||
'data-pinning-error', { errorType: 'data-too-large', source: 'on-ndv-close-modal' },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidPinDataJSON(value)) {
|
||||
dataPinningEventBus.$emit(
|
||||
'data-pinning-error', { errorType: 'invalid-json', source: 'on-ndv-close-modal' },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('pinData', { node: this.activeNode, data: JSON.parse(value) });
|
||||
}
|
||||
|
||||
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
|
||||
}
|
||||
|
||||
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
||||
this.$telemetry.track('User closed node modal', {
|
||||
node_type: this.activeNodeType ? this.activeNodeType.name : '',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div>
|
||||
<n8n-button
|
||||
:loading="nodeRunning && !isListeningForEvents"
|
||||
:disabled="!!disabledHint"
|
||||
:disabled="disabled || !!disabledHint"
|
||||
:label="buttonLabel"
|
||||
:type="type"
|
||||
:size="size"
|
||||
|
@ -21,14 +21,21 @@ import { INodeUi } from '@/Interface';
|
|||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { workflowRun } from './mixins/workflowRun';
|
||||
import { pinData } from './mixins/pinData';
|
||||
import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
|
||||
|
||||
export default mixins(
|
||||
workflowRun,
|
||||
pinData,
|
||||
).extend({
|
||||
props: {
|
||||
nodeName: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
|
@ -156,14 +163,31 @@ export default mixins(
|
|||
});
|
||||
},
|
||||
|
||||
onClick() {
|
||||
async onClick() {
|
||||
if (this.isListeningForEvents) {
|
||||
this.stopWaitingForWebhook();
|
||||
}
|
||||
else {
|
||||
this.$telemetry.track('User clicked execute node button', { node_type: this.nodeName, workflow_id: this.$store.getters.workflowId, source: this.telemetrySource });
|
||||
this.runWorkflow(this.nodeName, 'RunData.ExecuteNodeButton');
|
||||
this.$emit('execute');
|
||||
} else {
|
||||
let shouldUnpinAndExecute = false;
|
||||
if (this.hasPinData) {
|
||||
shouldUnpinAndExecute = await this.confirmMessage(
|
||||
this.$locale.baseText('ndv.pinData.unpinAndExecute.description'),
|
||||
this.$locale.baseText('ndv.pinData.unpinAndExecute.title'),
|
||||
null,
|
||||
this.$locale.baseText('ndv.pinData.unpinAndExecute.confirm'),
|
||||
this.$locale.baseText('ndv.pinData.unpinAndExecute.cancel'),
|
||||
);
|
||||
|
||||
if (shouldUnpinAndExecute) {
|
||||
dataPinningEventBus.$emit('data-unpinning', { source: 'unpin-and-execute-modal' });
|
||||
this.$store.commit('unpinData', { node: this.node });
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.hasPinData || shouldUnpinAndExecute) {
|
||||
this.$telemetry.track('User clicked execute node button', { node_type: this.nodeName, workflow_id: this.$store.getters.workflowId, source: this.telemetrySource });
|
||||
this.runWorkflow(this.nodeName, 'RunData.ExecuteNodeButton');
|
||||
this.$emit('execute');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
<div
|
||||
v-if="!isReadOnly"
|
||||
>
|
||||
<NodeExecuteButton v-if="node && nodeValid" :nodeName="node.name" @execute="onNodeExecute" size="small" telemetrySource="parameters"/>
|
||||
<NodeExecuteButton
|
||||
:nodeName="node.name"
|
||||
:disabled="outputPanelEditMode.enabled"
|
||||
size="small"
|
||||
telemetrySource="parameters"
|
||||
@execute="onNodeExecute"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NodeSettingsTabs v-if="node && nodeValid" v-model="openPanel" :nodeType="nodeType" :sessionId="sessionId" />
|
||||
|
@ -198,6 +204,9 @@ export default mixins(
|
|||
|
||||
return this.nodeType.properties;
|
||||
},
|
||||
outputPanelEditMode(): { enabled: boolean; value: string; } {
|
||||
return this.$store.getters['ui/outputPanelEditMode'];
|
||||
},
|
||||
isCommunityNode(): boolean {
|
||||
return isCommunityPackageName(this.node.type);
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
>{{ $locale.baseText('ndv.title.renameNode') }}</n8n-text>
|
||||
<n8n-input ref="input" size="small" v-model="newName" />
|
||||
<div :class="$style.editButtons">
|
||||
<n8n-button type="outline" size="small" @click="editName = false" :label="$locale.baseText('ndv.title.cancel')" />
|
||||
<n8n-button type="secondary" size="small" @click="editName = false" :label="$locale.baseText('ndv.title.cancel')" />
|
||||
<n8n-button type="primary" size="small" @click="onRename" :label="$locale.baseText('ndv.title.rename')" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,15 +9,23 @@
|
|||
:isExecuting="isNodeRunning"
|
||||
:executingMessage="$locale.baseText('ndv.output.executing')"
|
||||
:sessionId="sessionId"
|
||||
:isReadOnly="isReadOnly"
|
||||
paneType="output"
|
||||
@runChange="onRunIndexChange"
|
||||
@linkRun="onLinkRun"
|
||||
@unlinkRun="onUnlinkRun"
|
||||
ref="runData"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div :class="$style.titleSection">
|
||||
<span :class="$style.title">{{ $locale.baseText('ndv.output') }}</span>
|
||||
<RunInfo v-if="runsCount === 1" :taskData="runTaskData" />
|
||||
<span :class="$style.title">
|
||||
{{ $locale.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
|
||||
</span>
|
||||
<RunInfo
|
||||
v-if="!hasPinData && runsCount === 1"
|
||||
v-show="!outputPanelEditMode.enabled"
|
||||
:taskData="runTaskData"
|
||||
/>
|
||||
|
||||
<n8n-info-tip
|
||||
theme="warning"
|
||||
|
@ -26,7 +34,9 @@
|
|||
v-if="hasNodeRun && staleData"
|
||||
>
|
||||
<template>
|
||||
<span v-html="$locale.baseText('ndv.output.staleDataWarning')"></span>
|
||||
<span v-html="$locale.baseText(
|
||||
hasPinData ? 'ndv.output.staleDataWarning.pinData' : 'ndv.output.staleDataWarning.regular'
|
||||
)"></span>
|
||||
</template>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
|
@ -34,7 +44,20 @@
|
|||
|
||||
<template v-slot:node-not-run>
|
||||
<n8n-text v-if="workflowRunning && !isTriggerNode">{{ $locale.baseText('ndv.output.waitingToRun') }}</n8n-text>
|
||||
<n8n-text v-if="!workflowRunning && (isScheduleTrigger || !isTriggerNode)">{{ $locale.baseText('ndv.output.runNodeHint') }}</n8n-text>
|
||||
<n8n-text v-if="!workflowRunning">
|
||||
{{ $locale.baseText('ndv.output.runNodeHint') }}
|
||||
<span @click="insertTestData" v-if="canPinData">
|
||||
<br>
|
||||
{{ $locale.baseText('generic.or') }}
|
||||
<n8n-text
|
||||
tag="a"
|
||||
size="medium"
|
||||
color="primary"
|
||||
>
|
||||
{{ $locale.baseText('ndv.output.insertTestData') }}
|
||||
</n8n-text>
|
||||
</span>
|
||||
</n8n-text>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-output-data>
|
||||
|
@ -46,7 +69,7 @@
|
|||
</n8n-text>
|
||||
</template>
|
||||
|
||||
<template #run-info v-if="runsCount > 1">
|
||||
<template #run-info v-if="!hasPinData && runsCount > 1">
|
||||
<RunInfo :taskData="runTaskData" />
|
||||
</template>
|
||||
</RunData>
|
||||
|
@ -56,16 +79,25 @@
|
|||
import { IExecutionResponse, INodeUi } from '@/Interface';
|
||||
import { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
import RunData from './RunData.vue';
|
||||
import RunData, { EnterEditModeArgs } from './RunData.vue';
|
||||
import RunInfo from './RunInfo.vue';
|
||||
import { pinData } from "@/components/mixins/pinData";
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default Vue.extend({
|
||||
type RunData = Vue & { enterEditMode: (args: EnterEditModeArgs) => void };
|
||||
|
||||
export default mixins(
|
||||
pinData,
|
||||
).extend({
|
||||
name: 'OutputPanel',
|
||||
components: { RunData, RunInfo },
|
||||
props: {
|
||||
runIndex: {
|
||||
type: Number,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
linkedRuns: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
@ -162,8 +194,29 @@ export default Vue.extend({
|
|||
const runAt = this.runTaskData.startTime;
|
||||
return updatedAt > runAt;
|
||||
},
|
||||
outputPanelEditMode(): { enabled: boolean; value: string; } {
|
||||
return this.$store.getters['ui/outputPanelEditMode'];
|
||||
},
|
||||
canPinData(): boolean {
|
||||
return this.isPinDataNodeType && !this.isReadOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
insertTestData() {
|
||||
if (this.$refs.runData) {
|
||||
(this.$refs.runData as RunData).enterEditMode({
|
||||
origin: 'insertTestDataLink',
|
||||
});
|
||||
|
||||
this.$telemetry.track('User clicked ndv link', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
session_id: this.sessionId,
|
||||
node_type: this.node.type,
|
||||
pane: 'output',
|
||||
type: 'insert-test-data',
|
||||
});
|
||||
}
|
||||
},
|
||||
onLinkRun() {
|
||||
this.$emit('linkRun');
|
||||
},
|
||||
|
|
|
@ -1,20 +1,111 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<n8n-panel-callout
|
||||
v-if="canPinData && hasPinData && !editMode.enabled"
|
||||
theme="secondary"
|
||||
icon="thumbtack"
|
||||
:class="$style['pinned-data-callout']"
|
||||
>
|
||||
{{ $locale.baseText('runData.pindata.thisDataIsPinned') }}
|
||||
<span class="ml-4xs" v-if="!isReadOnly">
|
||||
<n8n-link
|
||||
theme="secondary"
|
||||
size="small"
|
||||
underline
|
||||
bold
|
||||
@click="onTogglePinData({ source: 'banner-link' })"
|
||||
>
|
||||
{{ $locale.baseText('runData.pindata.unpin') }}
|
||||
</n8n-link>
|
||||
</span>
|
||||
<template #trailingContent>
|
||||
<n8n-link
|
||||
:to="dataPinningDocsUrl"
|
||||
size="small"
|
||||
theme="secondary"
|
||||
bold
|
||||
underline
|
||||
@click="onClickDataPinningDocsLink"
|
||||
>
|
||||
{{ $locale.baseText('runData.pindata.learnMore') }}
|
||||
</n8n-link>
|
||||
</template>
|
||||
</n8n-panel-callout>
|
||||
|
||||
<BinaryDataDisplay :windowVisible="binaryDataDisplayVisible" :displayData="binaryDataDisplayData" @close="closeBinaryDataDisplay"/>
|
||||
|
||||
<div :class="$style.header">
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div v-show="!hasRunError && hasNodeRun && ((jsonData && jsonData.length > 0) || (binaryData && binaryData.length > 0))" @click.stop :class="$style.displayModes">
|
||||
<div v-show="!hasRunError" @click.stop :class="$style.displayModes">
|
||||
<n8n-radio-buttons
|
||||
v-show="hasNodeRun && ((jsonData && jsonData.length > 0) || (binaryData && binaryData.length > 0)) && !editMode.enabled"
|
||||
:value="displayMode"
|
||||
:options="buttons"
|
||||
@input="onDisplayModeChange"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
v-if="canPinData && !isReadOnly"
|
||||
v-show="!editMode.enabled"
|
||||
:title="$locale.baseText('runData.editOutput')"
|
||||
:circle="false"
|
||||
:disabled="node.disabled"
|
||||
class="ml-2xs"
|
||||
icon="pencil-alt"
|
||||
type="tertiary"
|
||||
@click="enterEditMode({ origin: 'editIconButton' })"
|
||||
/>
|
||||
<n8n-tooltip
|
||||
placement="bottom-end"
|
||||
v-if="canPinData && (jsonData && jsonData.length > 0)"
|
||||
v-show="!editMode.enabled"
|
||||
:value="pinDataDiscoveryTooltipVisible"
|
||||
:manual="isControlledPinDataTooltip"
|
||||
>
|
||||
<template #content v-if="!isControlledPinDataTooltip">
|
||||
<div :class="$style['tooltip-container']">
|
||||
<strong>{{ $locale.baseText('ndv.pinData.pin.title') }}</strong>
|
||||
<n8n-text size="small" tag="p">
|
||||
{{ $locale.baseText('ndv.pinData.pin.description') }}
|
||||
|
||||
<n8n-link :to="dataPinningDocsUrl" size="small">
|
||||
{{ $locale.baseText('ndv.pinData.pin.link') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template #content v-else>
|
||||
<div :class="$style['tooltip-container']">
|
||||
{{ $locale.baseText('node.discovery.pinData.ndv') }}
|
||||
</div>
|
||||
</template>
|
||||
<n8n-icon-button
|
||||
:class="`ml-2xs ${$style['pin-data-button']} ${hasPinData ? $style['pin-data-button-active'] : ''}`"
|
||||
type="tertiary"
|
||||
active
|
||||
icon="thumbtack"
|
||||
:disabled="editMode.enabled || (inputData.length === 0 && !hasPinData) || isReadOnly"
|
||||
@click="onTogglePinData({ source: 'pin-icon-click' })"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
|
||||
<div :class="$style['edit-mode-actions']" v-show="editMode.enabled">
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
:label="$locale.baseText('runData.editor.cancel')"
|
||||
@click="onClickCancelEdit"
|
||||
/>
|
||||
<n8n-button
|
||||
class="ml-2xs"
|
||||
type="primary"
|
||||
:label="$locale.baseText('runData.editor.save')"
|
||||
@click="onClickSaveEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.runSelector" v-if="maxRunIndex > 0" >
|
||||
<div :class="$style.runSelector" v-if="maxRunIndex > 0" v-show="!editMode.enabled">
|
||||
<n8n-select size="small" :value="runIndex" @input="onRunIndexChange" @click.stop>
|
||||
<template slot="prepend">{{ $locale.baseText('ndv.output.run') }}</template>
|
||||
<n8n-option v-for="option in (maxRunIndex + 1)" :label="getRunLabel(option)" :value="option - 1" :key="option"></n8n-option>
|
||||
|
@ -22,8 +113,8 @@
|
|||
|
||||
|
||||
<n8n-tooltip placement="right" v-if="canLinkRuns" :content="$locale.baseText(linkedRuns ? 'runData.unlinking.hint': 'runData.linking.hint')">
|
||||
<n8n-icon-button v-if="linkedRuns" icon="unlink" type="text" size="small" @click="unlinkRun" />
|
||||
<n8n-icon-button v-else icon="link" type="text" size="small" @click="linkRun" />
|
||||
<n8n-icon-button v-if="linkedRuns" icon="unlink" text size="small" @click="unlinkRun" />
|
||||
<n8n-icon-button v-else icon="link" text size="small" @click="linkRun" />
|
||||
</n8n-tooltip>
|
||||
|
||||
<slot name="run-info"></slot>
|
||||
|
@ -33,28 +124,40 @@
|
|||
<n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && dataCount > 0 && maxRunIndex === 0" :class="$style.itemsCount">
|
||||
<div v-else-if="hasNodeRun && dataCount > 0 && maxRunIndex === 0" v-show="!editMode.enabled" :class="$style.itemsCount">
|
||||
<n8n-text>
|
||||
{{ dataCount }} {{ $locale.baseText('ndv.output.items', {adjustToNumber: dataCount}) }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div :class="$style.dataContainer" ref="dataContainer">
|
||||
<div v-if="hasNodeRun && !hasRunError && displayMode === 'json' && state.path !== deselectedPlaceholder" :class="$style.copyButton">
|
||||
<el-dropdown trigger="click" @command="handleCopyClick">
|
||||
<div
|
||||
:class="[$style['data-container'], copyDropdownOpen ? $style['copy-dropdown-open'] : '']"
|
||||
ref="dataContainer"
|
||||
>
|
||||
<div v-if="hasNodeRun && !hasRunError && displayMode === 'json'" v-show="!editMode.enabled" :class="$style['actions-group']">
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="handleCopyClick"
|
||||
@visible-change="copyDropdownOpen = $event"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<n8n-icon-button :title="$locale.baseText('runData.copyToClipboard')" icon="copy" />
|
||||
<n8n-icon-button
|
||||
:title="$locale.baseText('runData.copyToClipboard')"
|
||||
icon="copy"
|
||||
type="tertiary"
|
||||
:circle="false"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'itemPath'}">
|
||||
<el-dropdown-item :command="{command: 'value'}">
|
||||
{{ $locale.baseText('runData.copyValue') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'itemPath'}" divided>
|
||||
{{ $locale.baseText('runData.copyItemPath') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'parameterPath'}">
|
||||
{{ $locale.baseText('runData.copyParameterPath') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'value'}">
|
||||
{{ $locale.baseText('runData.copyValue') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
@ -64,10 +167,38 @@
|
|||
<n8n-text>{{ executingMessage }}</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="editMode.enabled" :class="$style['edit-mode']">
|
||||
<div :class="$style['edit-mode-body']">
|
||||
<code-editor
|
||||
:value="editMode.value"
|
||||
:options="{ scrollBeyondLastLine: false }"
|
||||
type="json"
|
||||
@input="$store.commit('ui/setOutputPanelEditModeValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style['edit-mode-footer']">
|
||||
<n8n-info-tip :bold="false" :class="$style['edit-mode-footer-infotip']">
|
||||
{{ $locale.baseText('runData.editor.copyDataInfo') }}
|
||||
<n8n-link :to="dataPinningDocsUrl" size="small">
|
||||
{{ $locale.baseText('generic.learnMore') }}
|
||||
</n8n-link>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!hasNodeRun" :class="$style.center">
|
||||
<slot name="node-not-run"></slot>
|
||||
</div>
|
||||
|
||||
<div v-else-if="paneType === 'input' && node.disabled" :class="$style.center">
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('ndv.input.disabled', { interpolate: { nodeName: node.name } }) }}
|
||||
<n8n-link @click="enableNode">
|
||||
{{ $locale.baseText('ndv.input.disabled.cta') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && hasRunError" :class="$style.errorDisplay">
|
||||
<NodeErrorView :error="workflowRunData[node.name][runIndex].error" />
|
||||
</div>
|
||||
|
@ -87,7 +218,7 @@
|
|||
<n8n-text align="center" tag="div"><span v-html="$locale.baseText('ndv.output.tooMuchData.message', { interpolate: {size: dataSizeInMB }})"></span></n8n-text>
|
||||
|
||||
<n8n-button
|
||||
type="outline"
|
||||
outline
|
||||
:label="$locale.baseText('ndv.output.tooMuchData.showDataAnyway')"
|
||||
@click="showTooMuchData"
|
||||
/>
|
||||
|
@ -110,7 +241,7 @@
|
|||
<vue-json-pretty
|
||||
:data="jsonData"
|
||||
:deep="10"
|
||||
v-model="state.path"
|
||||
v-model="selectedOutput.path"
|
||||
:showLine="true"
|
||||
:showLength="true"
|
||||
selectableType="single"
|
||||
|
@ -159,7 +290,7 @@
|
|||
|
||||
<div :class="$style.binaryButtonContainer">
|
||||
<n8n-button size="small" :label="$locale.baseText('runData.showBinaryData')" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
|
||||
<n8n-button v-if="isDownloadable(index, key)" size="small" type="outline" :label="$locale.baseText('runData.downloadBinaryData')" class="binary-data-show-data-button" @click="downloadBinaryData(index, key)" />
|
||||
<n8n-button v-if="isDownloadable(index, key)" size="small" type="secondary" :label="$locale.baseText('runData.downloadBinaryData')" class="binary-data-show-data-button" @click="downloadBinaryData(index, key)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -167,7 +298,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.pagination" v-if="hasNodeRun && !hasRunError && dataCount > pageSize">
|
||||
<div :class="$style.pagination" v-if="hasNodeRun && !hasRunError && dataCount > pageSize" v-show="!editMode.enabled">
|
||||
<el-pagination
|
||||
background
|
||||
:hide-on-single-page="true"
|
||||
|
@ -212,7 +343,7 @@ import {
|
|||
INodeTypeDescription,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
PinData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -225,8 +356,12 @@ import {
|
|||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
DATA_PINNING_DOCS_URL,
|
||||
LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG,
|
||||
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
|
||||
MAX_DISPLAY_DATA_SIZE,
|
||||
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
||||
TEST_PIN_DATA,
|
||||
} from '@/constants';
|
||||
|
||||
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||||
|
@ -237,20 +372,29 @@ import { copyPaste } from '@/components/mixins/copyPaste';
|
|||
import { externalHooks } from "@/components/mixins/externalHooks";
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { pinData } from '@/components/mixins/pinData';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import { CodeEditor } from "@/components/forms";
|
||||
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus';
|
||||
import { stringSizeInBytes } from './helpers';
|
||||
import RunDataTable from './RunDataTable.vue';
|
||||
|
||||
// A path that does not exist so that nothing is selected by default
|
||||
const deselectedPlaceholder = '_!^&*';
|
||||
|
||||
export type EnterEditModeArgs = {
|
||||
origin: 'editIconButton' | 'insertTestDataLink',
|
||||
};
|
||||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
pinData,
|
||||
)
|
||||
.extend({
|
||||
name: 'RunData',
|
||||
|
@ -259,6 +403,7 @@ export default mixins(
|
|||
NodeErrorView,
|
||||
VueJsonPretty,
|
||||
WarningTooltip,
|
||||
CodeEditor,
|
||||
RunDataTable,
|
||||
},
|
||||
props: {
|
||||
|
@ -309,7 +454,7 @@ export default mixins(
|
|||
binaryDataPreviewActive: false,
|
||||
dataSize: 0,
|
||||
deselectedPlaceholder,
|
||||
state: {
|
||||
selectedOutput: {
|
||||
value: '' as object | number | string,
|
||||
path: deselectedPlaceholder,
|
||||
},
|
||||
|
@ -323,15 +468,38 @@ export default mixins(
|
|||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 25, 50, 100],
|
||||
copyDropdownOpen: false,
|
||||
eventBus: dataPinningEventBus,
|
||||
|
||||
pinDataDiscoveryTooltipVisible: false,
|
||||
isControlledPinDataTooltip: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
|
||||
if (this.paneType === 'output') {
|
||||
this.eventBus.$on('data-pinning-error', this.onDataPinningError);
|
||||
this.eventBus.$on('data-unpinning', this.onDataUnpinning);
|
||||
|
||||
const hasSeenPinDataTooltip = localStorage.getItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG);
|
||||
if (!hasSeenPinDataTooltip) {
|
||||
this.showPinDataDiscoveryTooltip(this.jsonData);
|
||||
}
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.hidePinDataDiscoveryTooltip();
|
||||
this.eventBus.$off('data-pinning-error', this.onDataPinningError);
|
||||
this.eventBus.$off('data-unpinning', this.onDataUnpinning);
|
||||
},
|
||||
computed: {
|
||||
activeNode(): INodeUi {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
dataPinningDocsUrl(): string {
|
||||
return DATA_PINNING_DOCS_URL;
|
||||
},
|
||||
displayMode(): IRunDataDisplayMode {
|
||||
return this.$store.getters['ui/getPanelDisplayMode'](this.paneType);
|
||||
},
|
||||
|
@ -344,6 +512,14 @@ export default mixins(
|
|||
}
|
||||
return null;
|
||||
},
|
||||
isTriggerNode (): boolean {
|
||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
||||
},
|
||||
canPinData (): boolean {
|
||||
return this.paneType === 'output' &&
|
||||
this.isPinDataNodeType &&
|
||||
!(this.binaryData && this.binaryData.length > 0);
|
||||
},
|
||||
buttons(): Array<{label: string, value: string}> {
|
||||
const defaults = [
|
||||
{ label: this.$locale.baseText('runData.table'), value: 'table'},
|
||||
|
@ -358,7 +534,7 @@ export default mixins(
|
|||
return defaults;
|
||||
},
|
||||
hasNodeRun(): boolean {
|
||||
return Boolean(!this.isExecuting && this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name));
|
||||
return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData));
|
||||
},
|
||||
hasRunError(): boolean {
|
||||
return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error);
|
||||
|
@ -423,12 +599,32 @@ export default mixins(
|
|||
|
||||
return 0;
|
||||
},
|
||||
inputData (): INodeExecutionData[] {
|
||||
let inputData = this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex);
|
||||
rawInputData (): INodeExecutionData[] {
|
||||
let inputData: INodeExecutionData[] = [];
|
||||
|
||||
if (this.node) {
|
||||
inputData = this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex);
|
||||
}
|
||||
|
||||
if (inputData.length === 0 || !Array.isArray(inputData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return inputData;
|
||||
},
|
||||
inputData (): INodeExecutionData[] {
|
||||
let inputData = this.rawInputData;
|
||||
|
||||
if (this.node && this.pinData) {
|
||||
inputData = Array.isArray(this.pinData)
|
||||
? this.pinData.map((value) => ({
|
||||
json: value,
|
||||
}))
|
||||
: [{
|
||||
json: this.pinData,
|
||||
}];
|
||||
}
|
||||
|
||||
const offset = this.pageSize * (this.currentPage - 1);
|
||||
inputData = inputData.slice(offset, offset + this.pageSize);
|
||||
|
||||
|
@ -480,8 +676,201 @@ export default mixins(
|
|||
}
|
||||
return branches;
|
||||
},
|
||||
editMode(): { enabled: boolean; value: string; } {
|
||||
return this.paneType === 'output'
|
||||
? this.$store.getters['ui/outputPanelEditMode']
|
||||
: { enabled: false, value: '' };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickDataPinningDocsLink() {
|
||||
this.$telemetry.track('User clicked ndv link', {
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
session_id: this.sessionId,
|
||||
node_type: this.activeNode.type,
|
||||
pane: 'output',
|
||||
type: 'data-pinning-docs',
|
||||
});
|
||||
},
|
||||
showPinDataDiscoveryTooltip(value: IDataObject[]) {
|
||||
if (!this.isTriggerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && value.length > 0) {
|
||||
this.pinDataDiscoveryComplete();
|
||||
|
||||
setTimeout(() => {
|
||||
this.isControlledPinDataTooltip = true;
|
||||
this.pinDataDiscoveryTooltipVisible = true;
|
||||
this.eventBus.$emit('data-pinning-discovery', { isTooltipVisible: true });
|
||||
}, 500); // Wait for NDV to open
|
||||
}
|
||||
},
|
||||
hidePinDataDiscoveryTooltip() {
|
||||
if (this.pinDataDiscoveryTooltipVisible) {
|
||||
this.isControlledPinDataTooltip = false;
|
||||
this.pinDataDiscoveryTooltipVisible = false;
|
||||
this.eventBus.$emit('data-pinning-discovery', { isTooltipVisible: false });
|
||||
}
|
||||
},
|
||||
pinDataDiscoveryComplete() {
|
||||
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG, 'true');
|
||||
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, 'true');
|
||||
},
|
||||
enterEditMode({ origin }: EnterEditModeArgs) {
|
||||
const inputData = this.pinData ? this.pinData : this.convertToJson(this.rawInputData);
|
||||
const data = inputData.length > 0
|
||||
? inputData
|
||||
: TEST_PIN_DATA;
|
||||
|
||||
this.$store.commit('ui/setOutputPanelEditModeEnabled', true);
|
||||
this.$store.commit('ui/setOutputPanelEditModeValue', JSON.stringify(data, null, 2));
|
||||
|
||||
this.$telemetry.track('User opened ndv edit state', {
|
||||
node_type: this.activeNode.type,
|
||||
click_type: origin === 'editIconButton' ? 'button' : 'link',
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
is_output_present: this.hasNodeRun || this.hasPinData,
|
||||
view: !this.hasNodeRun && !this.hasPinData ? 'undefined' : this.displayMode,
|
||||
is_data_pinned: this.hasPinData,
|
||||
});
|
||||
},
|
||||
onClickCancelEdit() {
|
||||
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
|
||||
this.$store.commit('ui/setOutputPanelEditModeValue', '');
|
||||
this.onExitEditMode({ type: 'cancel' });
|
||||
},
|
||||
onClickSaveEdit() {
|
||||
const { value } = this.editMode;
|
||||
|
||||
this.clearAllStickyNotifications();
|
||||
|
||||
if (!this.isValidPinDataSize(value)) {
|
||||
this.onDataPinningError({ errorType: 'data-too-large', source: 'save-edit' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidPinDataJSON(value)) {
|
||||
this.onDataPinningError({ errorType: 'invalid-json', source: 'save-edit' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
|
||||
this.$store.commit('pinData', { node: this.node, data: this.removeJsonKeys(value) });
|
||||
|
||||
this.onDataPinningSuccess({ source: 'save-edit' });
|
||||
|
||||
this.onExitEditMode({ type: 'save' });
|
||||
},
|
||||
removeJsonKeys(value: string) {
|
||||
const parsed = JSON.parse(value);
|
||||
|
||||
return Array.isArray(parsed)
|
||||
? parsed.map(item => this.isJsonKeyObject(item) ? item.json : item)
|
||||
: parsed;
|
||||
},
|
||||
isJsonKeyObject(item: unknown): item is { json: unknown } {
|
||||
if (!this.isObjectLiteral(item)) return false;
|
||||
|
||||
const keys = Object.keys(item);
|
||||
|
||||
return keys.length === 1 && keys[0] === 'json';
|
||||
},
|
||||
onExitEditMode({ type }: { type: 'save' | 'cancel' }) {
|
||||
this.$telemetry.track('User closed ndv edit state', {
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
view: this.displayMode,
|
||||
type,
|
||||
});
|
||||
},
|
||||
onDataUnpinning(
|
||||
{ source }: { source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal' },
|
||||
) {
|
||||
this.$telemetry.track('User unpinned ndv data', {
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
source,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
});
|
||||
},
|
||||
onDataPinningSuccess({ source }: { source: 'pin-icon-click' | 'save-edit' }) {
|
||||
this.$telemetry.track('Ndv data pinning success', {
|
||||
pinning_source: source,
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
view: this.displayMode,
|
||||
run_index: this.runIndex,
|
||||
});
|
||||
},
|
||||
onDataPinningError(
|
||||
{ errorType, source }: {
|
||||
errorType: 'data-too-large' | 'invalid-json',
|
||||
source: 'on-ndv-close-modal' | 'pin-icon-click' | 'save-edit'
|
||||
},
|
||||
) {
|
||||
this.$telemetry.track('Ndv data pinning failure', {
|
||||
pinning_source: source,
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
view: this.displayMode,
|
||||
run_index: this.runIndex,
|
||||
error_type: errorType,
|
||||
});
|
||||
},
|
||||
async onTogglePinData(
|
||||
{ source }: { source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal' },
|
||||
) {
|
||||
if (source === 'pin-icon-click') {
|
||||
this.$telemetry.track('User clicked pin data icon', {
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
view: !this.hasNodeRun && !this.hasPinData ? 'none' : this.displayMode,
|
||||
});
|
||||
}
|
||||
|
||||
this.updateNodeParameterIssues(this.node);
|
||||
|
||||
if (this.hasPinData) {
|
||||
this.onDataUnpinning({ source });
|
||||
this.$store.commit('unpinData', { node: this.node });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.convertToJson(this.rawInputData);
|
||||
|
||||
if (!this.isValidPinDataSize(data)) {
|
||||
this.onDataPinningError({ errorType: 'data-too-large', source: 'pin-icon-click' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.onDataPinningSuccess({ source: 'save-edit' });
|
||||
|
||||
this.$store.commit('pinData', { node: this.node, data });
|
||||
|
||||
if (this.maxRunIndex > 0) {
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('ndv.pinData.pin.multipleRuns.title', {
|
||||
interpolate: {
|
||||
index: `${this.runIndex}`,
|
||||
},
|
||||
}),
|
||||
message: this.$locale.baseText('ndv.pinData.pin.multipleRuns.description'),
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
this.hidePinDataDiscoveryTooltip();
|
||||
this.pinDataDiscoveryComplete();
|
||||
},
|
||||
switchToBinary() {
|
||||
this.onDisplayModeChange('binary');
|
||||
},
|
||||
|
@ -690,7 +1079,7 @@ export default mixins(
|
|||
this.updateNodesExecutionIssues();
|
||||
},
|
||||
dataItemClicked (path: string, data: object | number | string) {
|
||||
this.state.value = data;
|
||||
this.selectedOutput.value = data;
|
||||
},
|
||||
isDownloadable (index: number, key: string): boolean {
|
||||
const binaryDataItem: IBinaryData = this.binaryData[index][key];
|
||||
|
@ -771,15 +1160,34 @@ export default mixins(
|
|||
return '["' + allParts.join('"]["') + '"]';
|
||||
},
|
||||
handleCopyClick (commandData: { command: string }) {
|
||||
const newPath = this.convertPath(this.state.path);
|
||||
const isNotSelected = this.selectedOutput.path === deselectedPlaceholder;
|
||||
const selectedPath = isNotSelected ? '[""]' : this.selectedOutput.path;
|
||||
|
||||
let selectedValue = this.selectedOutput.value;
|
||||
if (isNotSelected) {
|
||||
if (this.hasPinData) {
|
||||
selectedValue = this.pinData as object;
|
||||
} else {
|
||||
selectedValue = this.convertToJson(this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex));
|
||||
}
|
||||
}
|
||||
|
||||
const newPath = this.convertPath(selectedPath);
|
||||
|
||||
let value: string;
|
||||
if (commandData.command === 'value') {
|
||||
if (typeof this.state.value === 'object') {
|
||||
value = JSON.stringify(this.state.value, null, 2);
|
||||
if (typeof selectedValue === 'object') {
|
||||
value = JSON.stringify(selectedValue, null, 2);
|
||||
} else {
|
||||
value = this.state.value.toString();
|
||||
value = selectedValue.toString();
|
||||
}
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyValue.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else {
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
|
@ -788,9 +1196,23 @@ export default mixins(
|
|||
const index = pathParts[0].slice(1);
|
||||
path = pathParts.slice(1).join(']');
|
||||
startPath = `$item(${index}).$node["${this.node!.name}"].json`;
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyItemPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else if (commandData.command === 'parameterPath') {
|
||||
path = newPath.split(']').slice(1).join(']');
|
||||
startPath = `$node["${this.node!.name}"].json`;
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyParameterPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
||||
path += '.';
|
||||
|
@ -798,6 +1220,23 @@ export default mixins(
|
|||
value = `{{ ${startPath + path} }}`;
|
||||
}
|
||||
|
||||
const copyType = {
|
||||
value: 'selection',
|
||||
itemPath: 'item_path',
|
||||
parameterPath: 'parameter_path',
|
||||
}[commandData.command];
|
||||
|
||||
this.$telemetry.track('User copied ndv data', {
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
view: this.displayMode,
|
||||
copy_type: copyType,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
pane: 'output',
|
||||
in_execution_log: this.isReadOnly,
|
||||
});
|
||||
|
||||
this.copyToClipboard(value);
|
||||
},
|
||||
refreshDataSize () {
|
||||
|
@ -820,13 +1259,30 @@ export default mixins(
|
|||
onRunIndexChange(run: number) {
|
||||
this.$emit('runChange', run);
|
||||
},
|
||||
enableNode() {
|
||||
if (this.node) {
|
||||
const updateInformation = {
|
||||
name: this.node.name,
|
||||
properties: {
|
||||
disabled: !this.node.disabled,
|
||||
},
|
||||
};
|
||||
|
||||
this.$store.commit('updateNodeProperties', updateInformation);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
node() {
|
||||
this.init();
|
||||
},
|
||||
jsonData () {
|
||||
jsonData (value: IDataObject[]) {
|
||||
this.refreshDataSize();
|
||||
|
||||
const hasSeenPinDataTooltip = localStorage.getItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG);
|
||||
if (!hasSeenPinDataTooltip) {
|
||||
this.showPinDataDiscoveryTooltip(value);
|
||||
}
|
||||
},
|
||||
binaryData (newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) {
|
||||
if (newData.length && !prevData.length && this.displayMode !== 'binary') {
|
||||
|
@ -869,22 +1325,36 @@ export default mixins(
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pinned-data-callout {
|
||||
border-radius: inherit;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-s);
|
||||
padding: var(--spacing-s) var(--spacing-s) 0 var(--spacing-s);
|
||||
position: relative;
|
||||
height: 30px;
|
||||
|
||||
> *:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dataContainer {
|
||||
.data-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&:hover,
|
||||
&.copy-dropdown-open {
|
||||
.actions-group {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dataDisplay {
|
||||
|
@ -921,7 +1391,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
.runSelector {
|
||||
max-width: 200px;
|
||||
max-width: 210px;
|
||||
margin-left: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-s);
|
||||
display: flex;
|
||||
|
@ -931,12 +1401,13 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
height: 30px;
|
||||
top: 12px;
|
||||
right: 24px;
|
||||
.actions-group {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 12px;
|
||||
right: var(--spacing-l);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
@ -1021,6 +1492,27 @@ export default mixins(
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.pin-data-button {
|
||||
svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.pin-data-button-active {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-tint-2);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
* {
|
||||
color: var(--color-primary);
|
||||
|
@ -1033,6 +1525,43 @@ export default mixins(
|
|||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.edit-mode {
|
||||
height: calc(100% - var(--spacing-s));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
padding-left: var(--spacing-s);
|
||||
padding-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.edit-mode-body {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edit-mode-footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.edit-mode-footer-infotip {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-mode-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -1075,5 +1604,4 @@ export default mixins(
|
|||
.vjs-tree .vjs-tree__content.has-line {
|
||||
border-left: 1px dotted var(--color-json-line);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -49,6 +49,7 @@ export default Vue.extend({
|
|||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 65px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.saved {
|
||||
|
|
|
@ -44,15 +44,15 @@
|
|||
<template slot-scope="scope">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="ops" v-if="scope.row.create">
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="secondary" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.createTag')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.update">
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="secondary" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.saveChanges')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.delete">
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="secondary" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.deleteTag')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops main" v-else-if="!scope.row.disable">
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<div :class="$style.buttonContainer" v-if="useWorkflowButton">
|
||||
<n8n-button
|
||||
v-if="useWorkflowButton"
|
||||
type="outline"
|
||||
outline
|
||||
label="Use workflow"
|
||||
@click.stop="onUseWorkflowClick"
|
||||
/>
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
|
||||
$locale.baseText('ndv.trigger.webhookNode.listening')
|
||||
}}</n8n-text>
|
||||
<div :class="$style.shake">
|
||||
<n8n-text class="mb-xs">
|
||||
<div :class="[$style.shake, 'mb-xs']">
|
||||
<n8n-text>
|
||||
{{
|
||||
$locale.baseText('ndv.trigger.webhookNode.requestHint', {
|
||||
interpolate: { type: this.webhookHttpMethod },
|
||||
|
@ -33,8 +33,8 @@
|
|||
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
|
||||
$locale.baseText('ndv.trigger.webhookBasedNode.listening')
|
||||
}}</n8n-text>
|
||||
<div :class="$style.shake">
|
||||
<n8n-text class="mb-xs" tag="div">
|
||||
<div :class="[$style.shake, 'mb-xs']">
|
||||
<n8n-text tag="div">
|
||||
{{
|
||||
$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
interpolate: { service: serviceName },
|
||||
|
|
|
@ -20,9 +20,11 @@ import {
|
|||
GenericValue,
|
||||
IContextObject,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
IWorkflowDataProxyAdditionalKeys,
|
||||
PinData,
|
||||
Workflow,
|
||||
WorkflowDataProxy,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -248,19 +250,20 @@ export default mixins(
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns the data the a node does output
|
||||
* Get the node's output using runData
|
||||
*
|
||||
* @param {IRunData} runData The data of the run to get the data of
|
||||
* @param {string} nodeName The name of the node to get the data of
|
||||
* @param {IRunData} runData The data of the run to get the data of
|
||||
* @param {string} filterText Filter text for parameters
|
||||
* @param {number} [itemIndex=0] The index of the item
|
||||
* @param {number} [runIndex=0] The index of the run
|
||||
* @param {string} [inputName='main'] The name of the input
|
||||
* @param {number} [outputIndex=0] The index of the output
|
||||
* @param {boolean} [useShort=false] Use short notation $json vs. $node[NodeName].json
|
||||
* @returns
|
||||
* @memberof Workflow
|
||||
*/
|
||||
getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null {
|
||||
getNodeRunDataOutput(nodeName: string, runData: IRunData, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null {
|
||||
if (!runData.hasOwnProperty(nodeName)) {
|
||||
// No data found for node
|
||||
return null;
|
||||
|
@ -297,6 +300,32 @@ export default mixins(
|
|||
|
||||
const outputData = runData[nodeName][runIndex].data![inputName][outputIndex]![itemIndex];
|
||||
|
||||
return this.getNodeOutput(nodeName, outputData, filterText, useShort);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the node's output using pinData
|
||||
*
|
||||
* @param {string} nodeName The name of the node to get the data of
|
||||
* @param {PinData[string]} pinData The node's pin data
|
||||
* @param {string} filterText Filter text for parameters
|
||||
* @param {boolean} [useShort=false] Use short notation $json vs. $node[NodeName].json
|
||||
*/
|
||||
getNodePinDataOutput(nodeName: string, pinData: PinData[string], filterText: string, useShort = false): IVariableSelectorOption[] | null {
|
||||
const outputData = pinData.map((data) => ({ json: data } as INodeExecutionData))[0];
|
||||
|
||||
return this.getNodeOutput(nodeName, outputData, filterText, useShort);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the node's output data
|
||||
*
|
||||
* @param {string} nodeName The name of the node to get the data of
|
||||
* @param {INodeExecutionData} outputData The data of the run to get the data of
|
||||
* @param {string} filterText Filter text for parameters
|
||||
* @param {boolean} [useShort=false] Use short notation
|
||||
*/
|
||||
getNodeOutput (nodeName: string, outputData: INodeExecutionData, filterText: string, useShort = false): IVariableSelectorOption[] | null {
|
||||
const returnData: IVariableSelectorOption[] = [];
|
||||
|
||||
// Get json data
|
||||
|
@ -491,7 +520,7 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
let tempOutputData;
|
||||
let tempOutputData: IVariableSelectorOption[] | null | undefined;
|
||||
|
||||
if (parentNode.length) {
|
||||
// If the node has an input node add the input data
|
||||
|
@ -502,7 +531,50 @@ export default mixins(
|
|||
const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main');
|
||||
const outputIndex = nodeConnection === undefined ? 0: nodeConnection.sourceIndex;
|
||||
|
||||
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
|
||||
tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
|
||||
|
||||
const pinDataOptions: IVariableSelectorOption[] = [
|
||||
{
|
||||
name: 'JSON',
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
parentNode.forEach((parentNodeName) => {
|
||||
const pinData = this.$store.getters['pinDataByNodeName'](parentNodeName);
|
||||
|
||||
if (pinData) {
|
||||
const output = this.getNodePinDataOutput(parentNodeName, pinData, filterText, true);
|
||||
|
||||
pinDataOptions[0].options = pinDataOptions[0].options!.concat(
|
||||
output && output[0].options ? output[0].options : [],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (pinDataOptions[0].options!.length > 0) {
|
||||
if (tempOutputData) {
|
||||
const jsonTempOutputData = tempOutputData.find((tempData) => tempData.name === 'JSON');
|
||||
|
||||
if (jsonTempOutputData) {
|
||||
if (!jsonTempOutputData.options) {
|
||||
jsonTempOutputData.options = [];
|
||||
}
|
||||
|
||||
(pinDataOptions[0].options || []).forEach((pinDataOption) => {
|
||||
const existingOptionIndex = jsonTempOutputData.options!.findIndex((option) => option.name === pinDataOption.name);
|
||||
if (existingOptionIndex !== -1) {
|
||||
jsonTempOutputData.options![existingOptionIndex] = pinDataOption;
|
||||
} else {
|
||||
jsonTempOutputData.options!.push(pinDataOption);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tempOutputData.push(pinDataOptions[0]);
|
||||
}
|
||||
} else {
|
||||
tempOutputData = pinDataOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (tempOutputData) {
|
||||
if (JSON.stringify(tempOutputData).length < 102400) {
|
||||
|
@ -602,7 +674,11 @@ export default mixins(
|
|||
|
||||
if (upstreamNodes.includes(nodeName)) {
|
||||
// If the node is an upstream node add also the output data which can be referenced
|
||||
tempOutputData = this.getNodeOutputData(runData, nodeName, filterText, itemIndex);
|
||||
const pinData = this.$store.getters['pinDataByNodeName'](nodeName);
|
||||
tempOutputData = pinData
|
||||
? this.getNodePinDataOutput(nodeName, pinData, filterText)
|
||||
: this.getNodeRunDataOutput(nodeName, runData, filterText, itemIndex);
|
||||
|
||||
if (tempOutputData) {
|
||||
nodeOptions.push(
|
||||
{
|
||||
|
|
136
packages/editor-ui/src/components/forms/CodeEditor.vue
Normal file
136
packages/editor-ui/src/components/forms/CodeEditor.vue
Normal file
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div ref="code" class="text-editor" @keydown.stop />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'code',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autocomplete: {
|
||||
type: Function,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
|
||||
monacoLibrary: null as monaco.IDisposable | null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadEditor() {
|
||||
if (!this.$refs.code) return;
|
||||
|
||||
this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, {
|
||||
automaticLayout: true,
|
||||
value: this.value,
|
||||
language: this.type === 'code' ? 'javascript' : 'json',
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: false,
|
||||
readOnly: this.readonly,
|
||||
padding: {
|
||||
top: 16,
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
...this.options,
|
||||
});
|
||||
|
||||
this.monacoInstance.onDidChangeModelContent(() => {
|
||||
const model = this.monacoInstance!.getModel();
|
||||
if (model) {
|
||||
this.$emit('input', model.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('n8nCustomTheme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#f5f2f0',
|
||||
},
|
||||
});
|
||||
monaco.editor.setTheme('n8nCustomTheme');
|
||||
|
||||
if (this.type === 'code') {
|
||||
// As wordBasedSuggestions: false does not have any effect does it however seem
|
||||
// to remove all all suggestions from the editor if I do this
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
|
||||
if (this.autocomplete) {
|
||||
this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
this.autocomplete().join('\n'),
|
||||
);
|
||||
}
|
||||
} else if (this.type === 'json') {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleResize() {
|
||||
if (this.monacoInstance) {
|
||||
// Workaround to force Monaco to recompute its boundaries
|
||||
this.monacoInstance.layout({} as unknown as undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(this.loadEditor);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
destroyed() {
|
||||
if (this.monacoLibrary) {
|
||||
this.monacoLibrary.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.monaco-editor {
|
||||
.slider {
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
&,
|
||||
&-background,
|
||||
.inputarea.ime-input,
|
||||
.margin {
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--color-background-xlight) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
1
packages/editor-ui/src/components/forms/index.ts
Normal file
1
packages/editor-ui/src/components/forms/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as CodeEditor } from './CodeEditor.vue';
|
|
@ -1,7 +1,7 @@
|
|||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER } from '@/constants';
|
||||
import { INodeUi, ITemplatesNode } from '@/Interface';
|
||||
import dateformat from 'dateformat';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import {IDataObject, INodeTypeDescription} from 'n8n-workflow';
|
||||
|
||||
const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
|
||||
const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
|
||||
|
@ -71,6 +71,12 @@ export function isNumber(value: unknown): value is number {
|
|||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function stringSizeInBytes(input: string | IDataObject | IDataObject[] | undefined): number {
|
||||
if (input === undefined) return 0;
|
||||
|
||||
return new Blob([typeof input === 'string' ? input : JSON.stringify(input)]).size;
|
||||
}
|
||||
|
||||
export function isCommunityPackageName(packageName: string): boolean {
|
||||
COMMUNITY_PACKAGE_NAME_REGEX.lastIndex = 0;
|
||||
// Community packages names start with <@username/>n8n-nodes- not followed by word 'base'
|
||||
|
|
|
@ -47,10 +47,14 @@ export const nodeHelpers = mixins(
|
|||
return Object.keys(node.parameters).includes('nodeCredentialType');
|
||||
},
|
||||
|
||||
isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } {
|
||||
return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject);
|
||||
},
|
||||
|
||||
isCustomApiCallSelected (nodeValues: INodeParameters): boolean {
|
||||
const { parameters } = nodeValues;
|
||||
|
||||
if (!isObjectLiteral(parameters)) return false;
|
||||
if (!this.isObjectLiteral(parameters)) return false;
|
||||
|
||||
return (
|
||||
parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY) ||
|
||||
|
@ -73,11 +77,13 @@ export const nodeHelpers = mixins(
|
|||
|
||||
// Returns all the issues of the node
|
||||
getNodeIssues (nodeType: INodeTypeDescription | null, node: INodeUi, ignoreIssues?: string[]): INodeIssues | null {
|
||||
const pinDataNodeNames = Object.keys(this.$store.getters.pinData || {});
|
||||
|
||||
let nodeIssues: INodeIssues | null = null;
|
||||
ignoreIssues = ignoreIssues || [];
|
||||
|
||||
if (node.disabled === true) {
|
||||
// Ignore issues on disabled nodes
|
||||
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
|
||||
// Ignore issues on disabled and pindata nodes
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -510,7 +516,3 @@ declare namespace HttpRequestNode {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } {
|
||||
return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject);
|
||||
}
|
||||
|
|
85
packages/editor-ui/src/components/mixins/pinData.ts
Normal file
85
packages/editor-ui/src/components/mixins/pinData.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import Vue from 'vue';
|
||||
import { INodeUi } from "@/Interface";
|
||||
import {IDataObject, PinData} from "n8n-workflow";
|
||||
import {stringSizeInBytes} from "@/components/helpers";
|
||||
import {MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST} from "@/constants";
|
||||
|
||||
interface PinDataContext {
|
||||
node: INodeUi;
|
||||
$showError(error: Error, title: string): void;
|
||||
}
|
||||
|
||||
export const pinData = (Vue as Vue.VueConstructor<Vue & PinDataContext>).extend({
|
||||
computed: {
|
||||
pinData (): PinData[string] | undefined {
|
||||
return this.node ? this.$store.getters['pinDataByNodeName'](this.node!.name) : undefined;
|
||||
},
|
||||
hasPinData (): boolean {
|
||||
return !!this.node && typeof this.pinData !== 'undefined';
|
||||
},
|
||||
isPinDataNodeType(): boolean {
|
||||
return !!this.node && !PIN_DATA_NODE_TYPES_DENYLIST.includes(this.node.type);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isValidPinDataJSON(data: string): boolean {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
const title = this.$locale.baseText('runData.editOutputInvalid');
|
||||
|
||||
const toRemove = new RegExp(/JSON\.parse:|of the JSON data/, 'g');
|
||||
const message = error.message.replace(toRemove, '').trim();
|
||||
const positionMatchRegEx = /at position (\d+)/;
|
||||
const positionMatch = error.message.match(positionMatchRegEx);
|
||||
|
||||
error.message = message.charAt(0).toUpperCase() + message.slice(1);
|
||||
error.message = error.message.replace(
|
||||
'Unexpected token \' in JSON',
|
||||
this.$locale.baseText('runData.editOutputInvalid.singleQuote'),
|
||||
);
|
||||
|
||||
if (positionMatch) {
|
||||
const position = parseInt(positionMatch[1], 10);
|
||||
const lineBreaksUpToPosition = (data.slice(0, position).match(/\n/g) || []).length;
|
||||
|
||||
error.message = error.message.replace(positionMatchRegEx,
|
||||
this.$locale.baseText('runData.editOutputInvalid.atPosition', {
|
||||
interpolate: {
|
||||
position: `${position}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
error.message = `${
|
||||
this.$locale.baseText('runData.editOutputInvalid.onLine', {
|
||||
interpolate: {
|
||||
line: `${lineBreaksUpToPosition + 1}`,
|
||||
},
|
||||
})
|
||||
} ${error.message}`;
|
||||
}
|
||||
|
||||
this.$showError(error, title);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isValidPinDataSize(data: string | object): boolean {
|
||||
if (typeof data === 'object') data = JSON.stringify(data);
|
||||
|
||||
if (this.$store.getters['pinDataSize'] + stringSizeInBytes(data) > MAX_WORKFLOW_PINNED_DATA_SIZE) {
|
||||
this.$showError(
|
||||
new Error(this.$locale.baseText('ndv.pinData.error.tooLarge.description')),
|
||||
this.$locale.baseText('ndv.pinData.error.tooLarge.title'),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -30,6 +30,7 @@ import {
|
|||
IExecuteData,
|
||||
INodeConnection,
|
||||
IWebhookDescription,
|
||||
PinData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -69,7 +70,7 @@ export const workflowHelpers = mixins(
|
|||
source: null,
|
||||
} as IExecuteData;
|
||||
|
||||
if (parentNode.length) {
|
||||
if (parentNode.length) {
|
||||
// Add the input data to be able to also resolve the short expression format
|
||||
// which does not use the node name
|
||||
const parentNodeName = parentNode[0];
|
||||
|
@ -78,6 +79,7 @@ export const workflowHelpers = mixins(
|
|||
if (workflowRunData === null) {
|
||||
return executeData;
|
||||
}
|
||||
|
||||
if (!workflowRunData[parentNodeName] ||
|
||||
workflowRunData[parentNodeName].length <= runIndex ||
|
||||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
|
||||
|
@ -108,7 +110,7 @@ export const workflowHelpers = mixins(
|
|||
},
|
||||
// Returns connectionInputData to be able to execute an expression.
|
||||
connectionInputData (parentNode: string[], currentNode: string, inputName: string, runIndex: number, nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 }): INodeExecutionData[] | null {
|
||||
let connectionInputData = null;
|
||||
let connectionInputData: INodeExecutionData[] | null = null;
|
||||
const executeData = this.executeData(parentNode, currentNode, inputName, runIndex);
|
||||
if (parentNode.length) {
|
||||
if (!Object.keys(executeData.data).length || executeData.data[inputName].length <= nodeConnection.sourceIndex) {
|
||||
|
@ -131,6 +133,35 @@ export const workflowHelpers = mixins(
|
|||
}
|
||||
}
|
||||
|
||||
const parentPinData = parentNode.reduce((acc: INodeExecutionData[], parentNodeName, index) => {
|
||||
const pinData = this.$store.getters['pinDataByNodeName'](parentNodeName);
|
||||
|
||||
if (pinData) {
|
||||
acc.push({
|
||||
json: pinData[0],
|
||||
pairedItem: {
|
||||
item: index,
|
||||
input: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (parentPinData.length > 0) {
|
||||
if (connectionInputData && connectionInputData.length > 0) {
|
||||
parentPinData.forEach((parentPinDataEntry) => {
|
||||
connectionInputData![0].json = {
|
||||
...connectionInputData![0].json,
|
||||
...parentPinDataEntry.json,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
connectionInputData = parentPinData;
|
||||
}
|
||||
}
|
||||
|
||||
return connectionInputData;
|
||||
},
|
||||
|
||||
|
@ -328,6 +359,7 @@ export const workflowHelpers = mixins(
|
|||
const data: IWorkflowData = {
|
||||
name: this.$store.getters.workflowName,
|
||||
nodes,
|
||||
pinData: this.$store.getters.pinData,
|
||||
connections: workflowConnections,
|
||||
active: this.$store.getters.isActive,
|
||||
settings: this.$store.getters.workflowSettings,
|
||||
|
@ -473,7 +505,10 @@ export const workflowHelpers = mixins(
|
|||
const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null;
|
||||
let runIndexParent = 0;
|
||||
if (workflowRunData !== null && parentNode.length) {
|
||||
runIndexParent = workflowRunData[parentNode[0]].length -1;
|
||||
const firstParentWithWorkflowRunData = parentNode.find((parentNodeName) => workflowRunData[parentNodeName]);
|
||||
if (firstParentWithWorkflowRunData) {
|
||||
runIndexParent = workflowRunData[firstParentWithWorkflowRunData].length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]);
|
||||
|
@ -490,6 +525,34 @@ export const workflowHelpers = mixins(
|
|||
runExecutionData = executionData.data;
|
||||
}
|
||||
|
||||
parentNode.forEach((parentNodeName) => {
|
||||
const pinData: PinData[string] = this.$store.getters['pinDataByNodeName'](parentNodeName);
|
||||
|
||||
if (pinData) {
|
||||
runExecutionData = {
|
||||
...runExecutionData,
|
||||
resultData: {
|
||||
...runExecutionData.resultData,
|
||||
runData: {
|
||||
...runExecutionData.resultData.runData,
|
||||
[parentNodeName]: [
|
||||
{
|
||||
startTime: new Date().valueOf(),
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
main: [
|
||||
pinData.map((data) => ({ json: data })),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (connectionInputData === null) {
|
||||
connectionInputData = [];
|
||||
}
|
||||
|
@ -509,7 +572,6 @@ export const workflowHelpers = mixins(
|
|||
},
|
||||
|
||||
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {
|
||||
|
||||
const parameters = {
|
||||
'__xxxxxxx__': expression,
|
||||
...siblingParameters,
|
||||
|
|
|
@ -188,6 +188,7 @@ export const workflowRun = mixins(
|
|||
const startRunData: IStartRunData = {
|
||||
workflowData,
|
||||
runData: newRunData,
|
||||
pinData: workflowData.pinData,
|
||||
startNodes,
|
||||
};
|
||||
if (nodeName) {
|
||||
|
@ -208,6 +209,7 @@ export const workflowRun = mixins(
|
|||
data: {
|
||||
resultData: {
|
||||
runData: newRunData || {},
|
||||
pinData: workflowData.pinData,
|
||||
startNodes,
|
||||
workflowData,
|
||||
},
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const MAX_WORKFLOW_SIZE = 16777216; // Workflow size limit in bytes
|
||||
export const MAX_WORKFLOW_PINNED_DATA_SIZE = 12582912; // Workflow pinned data size limit in bytes
|
||||
export const MAX_DISPLAY_DATA_SIZE = 204800;
|
||||
export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250;
|
||||
export const NODE_NAME_PREFIX = 'node-';
|
||||
|
@ -55,6 +57,7 @@ export const BREAKPOINT_XL = 1920;
|
|||
|
||||
|
||||
export const N8N_IO_BASE_URL = `https://api.n8n.io/api/`;
|
||||
export const DATA_PINNING_DOCS_URL = 'https://docs.n8n.io/data/data-pinning/';
|
||||
export const NPM_COMMUNITY_NODE_SEARCH_API_URL = `https://api.npms.io/v2/`;
|
||||
export const NPM_PACKAGE_DOCS_BASE_URL = `https://www.npmjs.com/package/`;
|
||||
export const NPM_KEYWORD_SEARCH_URL = `https://www.npmjs.com/search?q=keywords%3An8n-community-node-package`;
|
||||
|
@ -95,6 +98,7 @@ export const SET_NODE_TYPE = 'n8n-nodes-base.set';
|
|||
export const SERVICENOW_NODE_TYPE = 'n8n-nodes-base.serviceNow';
|
||||
export const SLACK_NODE_TYPE = 'n8n-nodes-base.slack';
|
||||
export const SPREADSHEET_FILE_NODE_TYPE = 'n8n-nodes-base.spreadsheetFile';
|
||||
export const SPLIT_IN_BATCHES_NODE_TYPE = 'n8n-nodes-base.splitInBatches';
|
||||
export const START_NODE_TYPE = 'n8n-nodes-base.start';
|
||||
export const SWITCH_NODE_TYPE = 'n8n-nodes-base.switch';
|
||||
export const THE_HIVE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.theHiveTrigger';
|
||||
|
@ -106,6 +110,16 @@ export const XERO_NODE_TYPE = 'n8n-nodes-base.xero';
|
|||
export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
|
||||
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
|
||||
|
||||
export const MULTIPLE_OUTPUT_NODE_TYPES = [
|
||||
IF_NODE_TYPE,
|
||||
SWITCH_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const PIN_DATA_NODE_TYPES_DENYLIST = [
|
||||
...MULTIPLE_OUTPUT_NODE_TYPES,
|
||||
SPLIT_IN_BATCHES_NODE_TYPE,
|
||||
];
|
||||
|
||||
// Node creator
|
||||
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||
|
@ -210,6 +224,8 @@ export const MODAL_CONFIRMED = 'confirmed';
|
|||
|
||||
export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
|
||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
||||
export const LOCAL_STORAGE_MAPPING_FLAG = 'N8N_MAPPING_ONBOARDED';
|
||||
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
||||
|
||||
|
@ -262,4 +278,14 @@ export enum VIEWS {
|
|||
COMMUNITY_NODES = "CommunityNodes",
|
||||
}
|
||||
|
||||
export const TEST_PIN_DATA = [
|
||||
{
|
||||
name: "First item",
|
||||
code: 1,
|
||||
},
|
||||
{
|
||||
name: "Second item",
|
||||
code: 2,
|
||||
},
|
||||
];
|
||||
export const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`, '$parameter'];
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export const dataPinningEventBus = new Vue();
|
|
@ -108,6 +108,10 @@ const module: Module<IUiState, IRootState> = {
|
|||
},
|
||||
output: {
|
||||
displayMode: 'table',
|
||||
editMode: {
|
||||
enabled: false,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
focusedMappableInput: '',
|
||||
mappingTelemetry: {},
|
||||
|
@ -147,6 +151,7 @@ const module: Module<IUiState, IRootState> = {
|
|||
},
|
||||
inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode,
|
||||
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||
outputPanelEditMode: (state: IUiState): IUiState['ndv']['output']['editMode'] => state.ndv.output.editMode,
|
||||
mainPanelPosition: (state: IUiState) => state.mainPanelPosition,
|
||||
focusedMappableInput: (state: IUiState) => state.ndv.focusedMappableInput,
|
||||
isDraggableDragging: (state: IUiState) => state.draggable.isDragging,
|
||||
|
@ -198,6 +203,12 @@ const module: Module<IUiState, IRootState> = {
|
|||
setPanelDisplayMode: (state: IUiState, params: {pane: 'input' | 'output', mode: IRunDataDisplayMode}) => {
|
||||
Vue.set(state.ndv[params.pane], 'displayMode', params.mode);
|
||||
},
|
||||
setOutputPanelEditModeEnabled: (state: IUiState, payload: boolean) => {
|
||||
Vue.set(state.ndv.output.editMode, 'enabled', payload);
|
||||
},
|
||||
setOutputPanelEditModeValue: (state: IUiState, payload: string) => {
|
||||
Vue.set(state.ndv.output.editMode, 'value', payload);
|
||||
},
|
||||
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
|
||||
state.mainPanelPosition = relativePosition;
|
||||
},
|
||||
|
|
|
@ -196,7 +196,7 @@
|
|||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-radius: 20px;
|
||||
border-radius: var(--border-radius-base);
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-background-base);
|
||||
border-color: var(--color-foreground-base);
|
||||
|
|
|
@ -49,6 +49,9 @@ import {
|
|||
N8nAvatar,
|
||||
N8nActionToggle,
|
||||
N8nButton,
|
||||
N8nElButton,
|
||||
N8nCallout,
|
||||
N8nPanelCallout,
|
||||
N8nCard,
|
||||
N8nIcon,
|
||||
N8nIconButton,
|
||||
|
@ -87,7 +90,10 @@ Vue.use(N8nInfoAccordion);
|
|||
Vue.use(N8nActionBox);
|
||||
Vue.use(N8nActionToggle);
|
||||
Vue.use(N8nAvatar);
|
||||
Vue.use(N8nButton);
|
||||
Vue.component('n8n-button', N8nButton);
|
||||
Vue.component('el-button', N8nElButton);
|
||||
Vue.component('n8n-callout', N8nCallout);
|
||||
Vue.component('n8n-panel-callout', N8nPanelCallout);
|
||||
Vue.component('n8n-card', N8nCard);
|
||||
Vue.component('n8n-form-box', N8nFormBox);
|
||||
Vue.component('n8n-form-inputs', N8nFormInputs);
|
||||
|
@ -161,7 +167,6 @@ Vue.prototype.$alert = async (message: string, configOrTitle: string | ElMessage
|
|||
let temp = config || (typeof configOrTitle === 'object' ? configOrTitle : {});
|
||||
temp = {
|
||||
...temp,
|
||||
roundButton: true,
|
||||
cancelButtonClass: 'btn--cancel',
|
||||
confirmButtonClass: 'btn--confirm',
|
||||
};
|
||||
|
@ -176,7 +181,6 @@ Vue.prototype.$confirm = async (message: string, configOrTitle: string | ElMessa
|
|||
let temp = config || (typeof configOrTitle === 'object' ? configOrTitle : {});
|
||||
temp = {
|
||||
...temp,
|
||||
roundButton: true,
|
||||
cancelButtonClass: 'btn--cancel',
|
||||
confirmButtonClass: 'btn--confirm',
|
||||
distinguishCancelAndClose: true,
|
||||
|
@ -194,7 +198,6 @@ Vue.prototype.$prompt = async (message: string, configOrTitle: string | ElMessag
|
|||
let temp = config || (typeof configOrTitle === 'object' ? configOrTitle : {});
|
||||
temp = {
|
||||
...temp,
|
||||
roundButton: true,
|
||||
cancelButtonClass: 'btn--cancel',
|
||||
confirmButtonClass: 'btn--confirm',
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"generic.communityNode.tooltip": "This is a node from our community. It's part of the {packageName} package. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">Learn more</a>",
|
||||
"generic.delete": "Delete",
|
||||
"generic.copy": "Copy",
|
||||
"generic.or": "or",
|
||||
"generic.clickToCopy": "Click to copy",
|
||||
"generic.copiedToClipboard": "Copied to clipboard",
|
||||
"generic.beta": "beta",
|
||||
|
@ -343,7 +344,10 @@
|
|||
"ndv.input.notConnected.title": "Wire me up",
|
||||
"ndv.input.notConnected.message": "This node can only receive input data if you connect it to another node.",
|
||||
"ndv.input.notConnected.learnMore": "Learn more",
|
||||
"ndv.input.disabled": "The '{nodeName}' node is disabled and won’t execute.",
|
||||
"ndv.input.disabled.cta": "Enable it",
|
||||
"ndv.output": "Output",
|
||||
"ndv.output.edit": "Edit Output",
|
||||
"ndv.output.all": "all",
|
||||
"ndv.output.branch": "Branch",
|
||||
"ndv.output.executing": "Executing node...",
|
||||
|
@ -357,7 +361,9 @@
|
|||
"ndv.output.pageSize": "Page Size",
|
||||
"ndv.output.run": "Run",
|
||||
"ndv.output.runNodeHint": "Execute this node to output data",
|
||||
"ndv.output.staleDataWarning": "Node parameters have changed. <br /> Execute node again to refresh output.",
|
||||
"ndv.output.insertTestData": "insert test data",
|
||||
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Execute node again to refresh output.",
|
||||
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
||||
"ndv.output.tooMuchData.message": "The node contains {size} MB of data. Displaying it may cause problems. <br /> If you do decide to display it, avoid the JSON view.",
|
||||
"ndv.output.tooMuchData.showDataAnyway": "Show data anyway",
|
||||
"ndv.output.tooMuchData.title": "Output data is huge!",
|
||||
|
@ -365,6 +371,20 @@
|
|||
"ndv.title.cancel": "Cancel",
|
||||
"ndv.title.rename": "Rename",
|
||||
"ndv.title.renameNode": "Rename node",
|
||||
"ndv.pinData.pin.title": "Pin data",
|
||||
"ndv.pinData.pin.description": "Node will always output this data instead of executing. You can also pin data from previous executions.",
|
||||
"ndv.pinData.pin.link": "More info",
|
||||
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
|
||||
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
||||
"ndv.pinData.unpinAndExecute.title": "Unpin output data?",
|
||||
"ndv.pinData.unpinAndExecute.description": "Executing a node overwrites pinned data.",
|
||||
"ndv.pinData.unpinAndExecute.cancel": "Cancel",
|
||||
"ndv.pinData.unpinAndExecute.confirm": "Unpin and execute",
|
||||
"ndv.pinData.beforeClosing.title": "Save output changes before closing?",
|
||||
"ndv.pinData.beforeClosing.cancel": "Discard",
|
||||
"ndv.pinData.beforeClosing.confirm": "Save",
|
||||
"ndv.pinData.error.tooLarge.title": "Output data is too large to pin",
|
||||
"ndv.pinData.error.tooLarge.description": "You can pin at most 12MB of output per workflow.",
|
||||
"noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?",
|
||||
"noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows",
|
||||
"node.activateDeactivateNode": "Activate/Deactivate Node",
|
||||
|
@ -378,6 +398,8 @@
|
|||
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
|
||||
"node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall": "The node is waiting for an incoming webhook call (indefinitely)",
|
||||
"node.waitingForYouToCreateAnEventIn": "Waiting for you to create an event in {nodeType}",
|
||||
"node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.",
|
||||
"node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.",
|
||||
"nodeBase.clickToAddNodeOrDragToConnect": "Click to add node<br />or drag to connect",
|
||||
"nodeCreator.categoryNames.analytics": "Analytics",
|
||||
"nodeCreator.categoryNames.communication": "Communication",
|
||||
|
@ -644,14 +666,25 @@
|
|||
"runData.unlinking.hint": "Unlink displayed input and output runs",
|
||||
"runData.binary": "Binary",
|
||||
"runData.copyItemPath": "Copy Item Path",
|
||||
"runData.copyItemPath.toast": "Item path copied",
|
||||
"runData.copyParameterPath": "Copy Parameter Path",
|
||||
"runData.copyParameterPath.toast": "Parameter path copied",
|
||||
"runData.copyValue": "Copy Selection",
|
||||
"runData.copyValue.toast": "Output data copied",
|
||||
"runData.copyToClipboard": "Copy to Clipboard",
|
||||
"runData.copyValue": "Copy Value",
|
||||
"runData.copyDisabled": "First click on the output data you want to copy, then click this button.",
|
||||
"runData.editOutput": "Edit Output",
|
||||
"runData.editOutputInvalid": "Problem with output data",
|
||||
"runData.editOutputInvalid.singleQuote": "Unexpected single quote. Please use double quotes (\") instead",
|
||||
"runData.editOutputInvalid.onLine": "On line {line}:",
|
||||
"runData.editOutputInvalid.atPosition": "(at position {position})",
|
||||
"runData.editValue": "Edit Value",
|
||||
"runData.downloadBinaryData": "Download",
|
||||
"runData.executeNode": "Execute Node",
|
||||
"runData.executionTime": "Execution Time",
|
||||
"runData.fileExtension": "File Extension",
|
||||
"runData.fileName": "File Name",
|
||||
"runData.invalidPinnedData": "Invalid pinned data",
|
||||
"runData.items": "Items",
|
||||
"runData.json": "JSON",
|
||||
"runData.mimeType": "Mime Type",
|
||||
|
@ -664,6 +697,12 @@
|
|||
"runData.showBinaryData": "View",
|
||||
"runData.startTime": "Start Time",
|
||||
"runData.table": "Table",
|
||||
"runData.pindata.learnMore": "Learn more",
|
||||
"runData.pindata.thisDataIsPinned": "This data is pinned.",
|
||||
"runData.pindata.unpin": "Unpin",
|
||||
"runData.editor.save": "Save",
|
||||
"runData.editor.cancel": "Cancel",
|
||||
"runData.editor.copyDataInfo": "You can copy data from previous executions and paste it above.",
|
||||
"saveButton.save": "@:_reusableBaseText.save",
|
||||
"saveButton.saved": "Saved",
|
||||
"saveButton.saving": "Saving",
|
||||
|
|
|
@ -92,6 +92,7 @@ import {
|
|||
faTasks,
|
||||
faTerminal,
|
||||
faThLarge,
|
||||
faThumbtack,
|
||||
faTimes,
|
||||
faTimesCircle,
|
||||
faTrash,
|
||||
|
@ -204,6 +205,7 @@ addIcon(faTable);
|
|||
addIcon(faTasks);
|
||||
addIcon(faTerminal);
|
||||
addIcon(faThLarge);
|
||||
addIcon(faThumbtack);
|
||||
addIcon(faTimes);
|
||||
addIcon(faTimesCircle);
|
||||
addIcon(faTrash);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, DEFAULT_NODETYPE_VERSION } from '@/constants';
|
||||
import {
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
DEFAULT_NODETYPE_VERSION,
|
||||
} from '@/constants';
|
||||
|
||||
import {
|
||||
IConnection,
|
||||
|
@ -14,6 +16,7 @@ import {
|
|||
IRunData,
|
||||
ITaskData,
|
||||
IWorkflowSettings,
|
||||
PinData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -40,6 +43,8 @@ import users from './modules/users';
|
|||
import workflows from './modules/workflows';
|
||||
import versions from './modules/versions';
|
||||
import templates from './modules/templates';
|
||||
import {stringSizeInBytes} from "@/components/helpers";
|
||||
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
|
||||
import communityNodes from './modules/communityNodes';
|
||||
import { isCommunityPackageName } from './components/helpers';
|
||||
|
||||
|
@ -90,6 +95,7 @@ const state: IRootState = {
|
|||
nodes: [],
|
||||
settings: {},
|
||||
tags: [],
|
||||
pinData: {},
|
||||
},
|
||||
sidebarMenuItems: [],
|
||||
instanceId: '',
|
||||
|
@ -114,13 +120,13 @@ export const store = new Vuex.Store({
|
|||
state,
|
||||
mutations: {
|
||||
// Active Actions
|
||||
addActiveAction (state, action: string) {
|
||||
addActiveAction(state, action: string) {
|
||||
if (!state.activeActions.includes(action)) {
|
||||
state.activeActions.push(action);
|
||||
}
|
||||
},
|
||||
|
||||
removeActiveAction (state, action: string) {
|
||||
removeActiveAction(state, action: string) {
|
||||
const actionIndex = state.activeActions.indexOf(action);
|
||||
if (actionIndex !== -1) {
|
||||
state.activeActions.splice(actionIndex, 1);
|
||||
|
@ -128,7 +134,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// Active Executions
|
||||
addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) {
|
||||
addActiveExecution(state, newActiveExecution: IExecutionsCurrentSummaryExtended) {
|
||||
// Check if the execution exists already
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.id === newActiveExecution.id;
|
||||
|
@ -144,7 +150,7 @@ export const store = new Vuex.Store({
|
|||
|
||||
state.activeExecutions.unshift(newActiveExecution);
|
||||
},
|
||||
finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) {
|
||||
finishActiveExecution(state, finishedActiveExecution: IPushDataExecutionFinished) {
|
||||
// Find the execution to set to finished
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.id === finishedActiveExecution.executionId;
|
||||
|
@ -162,22 +168,22 @@ export const store = new Vuex.Store({
|
|||
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
||||
Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt);
|
||||
},
|
||||
setActiveExecutions (state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) {
|
||||
setActiveExecutions(state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) {
|
||||
Vue.set(state, 'activeExecutions', newActiveExecutions);
|
||||
},
|
||||
|
||||
// Active Workflows
|
||||
setActiveWorkflows (state, newActiveWorkflows: string[]) {
|
||||
setActiveWorkflows(state, newActiveWorkflows: string[]) {
|
||||
state.activeWorkflows = newActiveWorkflows;
|
||||
},
|
||||
setWorkflowActive (state, workflowId: string) {
|
||||
setWorkflowActive(state, workflowId: string) {
|
||||
state.stateIsDirty = false;
|
||||
const index = state.activeWorkflows.indexOf(workflowId);
|
||||
if (index === -1) {
|
||||
state.activeWorkflows.push(workflowId);
|
||||
}
|
||||
},
|
||||
setWorkflowInactive (state, workflowId: string) {
|
||||
setWorkflowInactive(state, workflowId: string) {
|
||||
const index = state.activeWorkflows.indexOf(workflowId);
|
||||
if (index !== -1) {
|
||||
state.activeWorkflows.splice(index, 1);
|
||||
|
@ -185,15 +191,15 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
// Set state condition dirty or not
|
||||
// ** Dirty: if current workflow state has been synchronized with database AKA has it been saved
|
||||
setStateDirty (state, dirty : boolean) {
|
||||
setStateDirty(state, dirty: boolean) {
|
||||
state.stateIsDirty = dirty;
|
||||
},
|
||||
|
||||
// Selected Nodes
|
||||
addSelectedNode (state, node: INodeUi) {
|
||||
addSelectedNode(state, node: INodeUi) {
|
||||
state.selectedNodes.push(node);
|
||||
},
|
||||
removeNodeFromSelection (state, node: INodeUi) {
|
||||
removeNodeFromSelection(state, node: INodeUi) {
|
||||
let index;
|
||||
for (index in state.selectedNodes) {
|
||||
if (state.selectedNodes[index].name === node.name) {
|
||||
|
@ -202,17 +208,38 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
}
|
||||
},
|
||||
resetSelectedNodes (state) {
|
||||
resetSelectedNodes(state) {
|
||||
Vue.set(state, 'selectedNodes', []);
|
||||
},
|
||||
|
||||
// Pin data
|
||||
pinData(state, payload: { node: INodeUi, data: PinData[string] }) {
|
||||
if (state.workflow.pinData) {
|
||||
Vue.set(state.workflow.pinData, payload.node.name, payload.data);
|
||||
}
|
||||
|
||||
state.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.$emit('pin-data', { [payload.node.name]: payload.data });
|
||||
},
|
||||
unpinData(state, payload: { node: INodeUi }) {
|
||||
if (state.workflow.pinData) {
|
||||
Vue.set(state.workflow.pinData, payload.node.name, undefined);
|
||||
delete state.workflow.pinData[payload.node.name];
|
||||
}
|
||||
|
||||
state.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.$emit('unpin-data', { [payload.node.name]: undefined });
|
||||
},
|
||||
|
||||
// Active
|
||||
setActive (state, newActive: boolean) {
|
||||
setActive(state, newActive: boolean) {
|
||||
state.workflow.active = newActive;
|
||||
},
|
||||
|
||||
// Connections
|
||||
addConnection (state, data) {
|
||||
addConnection(state, data) {
|
||||
if (data.connection.length !== 2) {
|
||||
// All connections need two entries
|
||||
// TODO: Check if there is an error or whatever that is supposed to be returned
|
||||
|
@ -260,7 +287,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
|
||||
},
|
||||
removeConnection (state, data) {
|
||||
removeConnection(state, data) {
|
||||
const sourceData = data.connection[0];
|
||||
const destinationData = data.connection[1];
|
||||
|
||||
|
@ -285,13 +312,13 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
|
||||
},
|
||||
removeAllConnections (state, data) {
|
||||
removeAllConnections(state, data) {
|
||||
if (data && data.setStateDirty === true) {
|
||||
state.stateIsDirty = true;
|
||||
}
|
||||
state.workflow.connections = {};
|
||||
},
|
||||
removeAllNodeConnection (state, node: INodeUi) {
|
||||
removeAllNodeConnection(state, node: INodeUi) {
|
||||
state.stateIsDirty = true;
|
||||
// Remove all source connections
|
||||
if (state.workflow.connections.hasOwnProperty(node.name)) {
|
||||
|
@ -320,7 +347,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
},
|
||||
|
||||
renameNodeSelectedAndExecution (state, nameData) {
|
||||
renameNodeSelectedAndExecution(state, nameData) {
|
||||
state.stateIsDirty = true;
|
||||
// If node has any WorkflowResultData rename also that one that the data
|
||||
// does still get displayed also after node got renamed
|
||||
|
@ -336,9 +363,14 @@ export const store = new Vuex.Store({
|
|||
|
||||
Vue.set(state.nodeMetadata, nameData.new, state.nodeMetadata[nameData.old]);
|
||||
Vue.delete(state.nodeMetadata, nameData.old);
|
||||
|
||||
if (state.workflow.pinData && state.workflow.pinData.hasOwnProperty(nameData.old)) {
|
||||
Vue.set(state.workflow.pinData, nameData.new, state.workflow.pinData[nameData.old]);
|
||||
Vue.delete(state.workflow.pinData, nameData.old);
|
||||
}
|
||||
},
|
||||
|
||||
resetAllNodesIssues (state) {
|
||||
resetAllNodesIssues(state) {
|
||||
state.workflow.nodes.forEach((node) => {
|
||||
node.issues = undefined;
|
||||
});
|
||||
|
@ -346,7 +378,7 @@ export const store = new Vuex.Store({
|
|||
return true;
|
||||
},
|
||||
|
||||
setNodeIssue (state, nodeIssueData: INodeIssueData) {
|
||||
setNodeIssue(state, nodeIssueData: INodeIssueData) {
|
||||
|
||||
const node = state.workflow.nodes.find(node => {
|
||||
return node.name === nodeIssueData.node;
|
||||
|
@ -382,7 +414,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// Name
|
||||
setWorkflowName (state, data) {
|
||||
setWorkflowName(state, data) {
|
||||
if (data.setStateDirty === true) {
|
||||
state.stateIsDirty = true;
|
||||
}
|
||||
|
@ -390,7 +422,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// replace invalid credentials in workflow
|
||||
replaceInvalidWorkflowCredentials(state, {credentials, invalid, type }) {
|
||||
replaceInvalidWorkflowCredentials(state, {credentials, invalid, type}) {
|
||||
state.workflow.nodes.forEach((node) => {
|
||||
if (!node.credentials || !node.credentials[type]) {
|
||||
return;
|
||||
|
@ -403,7 +435,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
|
||||
if (nodeCredentials.id === null) {
|
||||
if (nodeCredentials.name === invalid.name){
|
||||
if (nodeCredentials.name === invalid.name) {
|
||||
node.credentials[type] = credentials;
|
||||
}
|
||||
return;
|
||||
|
@ -416,7 +448,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// Nodes
|
||||
addNode (state, nodeData: INodeUi) {
|
||||
addNode(state, nodeData: INodeUi) {
|
||||
if (!nodeData.hasOwnProperty('name')) {
|
||||
// All nodes have to have a name
|
||||
// TODO: Check if there is an error or whatever that is supposed to be returned
|
||||
|
@ -425,9 +457,13 @@ export const store = new Vuex.Store({
|
|||
|
||||
state.workflow.nodes.push(nodeData);
|
||||
},
|
||||
removeNode (state, node: INodeUi) {
|
||||
removeNode(state, node: INodeUi) {
|
||||
Vue.delete(state.nodeMetadata, node.name);
|
||||
|
||||
if (state.workflow.pinData && state.workflow.pinData.hasOwnProperty(node.name)) {
|
||||
Vue.delete(state.workflow.pinData, node.name);
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.workflow.nodes.length; i++) {
|
||||
if (state.workflow.nodes[i].name === node.name) {
|
||||
state.workflow.nodes.splice(i, 1);
|
||||
|
@ -436,14 +472,19 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
}
|
||||
},
|
||||
removeAllNodes (state, data) {
|
||||
removeAllNodes(state, data) {
|
||||
if (data.setStateDirty === true) {
|
||||
state.stateIsDirty = true;
|
||||
}
|
||||
|
||||
if (data.removePinData) {
|
||||
state.workflow.pinData = {};
|
||||
}
|
||||
|
||||
state.workflow.nodes.splice(0, state.workflow.nodes.length);
|
||||
state.nodeMetadata = {};
|
||||
},
|
||||
updateNodeProperties (state, updateInformation: INodeUpdatePropertiesInformation) {
|
||||
updateNodeProperties(state, updateInformation: INodeUpdatePropertiesInformation) {
|
||||
// Find the node that should be updated
|
||||
const node = state.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
|
@ -456,7 +497,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
}
|
||||
},
|
||||
setNodeValue (state, updateInformation: IUpdateInformation) {
|
||||
setNodeValue(state, updateInformation: IUpdateInformation) {
|
||||
// Find the node that should be updated
|
||||
const node = state.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
|
@ -469,7 +510,7 @@ export const store = new Vuex.Store({
|
|||
state.stateIsDirty = true;
|
||||
Vue.set(node, updateInformation.key, updateInformation.value);
|
||||
},
|
||||
setNodeParameters (state, updateInformation: IUpdateInformation) {
|
||||
setNodeParameters(state, updateInformation: IUpdateInformation) {
|
||||
// Find the node that should be updated
|
||||
const node = state.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
|
@ -489,70 +530,71 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// Node-Index
|
||||
addToNodeIndex (state, nodeName: string) {
|
||||
addToNodeIndex(state, nodeName: string) {
|
||||
state.nodeIndex.push(nodeName);
|
||||
},
|
||||
setNodeIndex (state, newData: { index: number, name: string | null}) {
|
||||
setNodeIndex(state, newData: { index: number, name: string | null }) {
|
||||
state.nodeIndex[newData.index] = newData.name;
|
||||
},
|
||||
resetNodeIndex (state) {
|
||||
resetNodeIndex(state) {
|
||||
Vue.set(state, 'nodeIndex', []);
|
||||
},
|
||||
|
||||
// Node-View
|
||||
setNodeViewMoveInProgress (state, value: boolean) {
|
||||
setNodeViewMoveInProgress(state, value: boolean) {
|
||||
state.nodeViewMoveInProgress = value;
|
||||
},
|
||||
setNodeViewOffsetPosition (state, data) {
|
||||
setNodeViewOffsetPosition(state, data) {
|
||||
state.nodeViewOffsetPosition = data.newOffset;
|
||||
},
|
||||
|
||||
setNodeTypes (state, nodeTypes: INodeTypeDescription[]) {
|
||||
// Node-Types
|
||||
setNodeTypes(state, nodeTypes: INodeTypeDescription[]) {
|
||||
Vue.set(state, 'nodeTypes', nodeTypes);
|
||||
},
|
||||
// Active Execution
|
||||
setExecutingNode (state, executingNode: string) {
|
||||
setExecutingNode(state, executingNode: string) {
|
||||
state.executingNode = executingNode;
|
||||
},
|
||||
setExecutionWaitingForWebhook (state, newWaiting: boolean) {
|
||||
setExecutionWaitingForWebhook(state, newWaiting: boolean) {
|
||||
state.executionWaitingForWebhook = newWaiting;
|
||||
},
|
||||
setActiveExecutionId (state, executionId: string | null) {
|
||||
setActiveExecutionId(state, executionId: string | null) {
|
||||
state.executionId = executionId;
|
||||
},
|
||||
|
||||
// Push Connection
|
||||
setPushConnectionActive (state, newActive: boolean) {
|
||||
setPushConnectionActive(state, newActive: boolean) {
|
||||
state.pushConnectionActive = newActive;
|
||||
},
|
||||
|
||||
// Webhooks
|
||||
setUrlBaseWebhook (state, urlBaseWebhook: string) {
|
||||
setUrlBaseWebhook(state, urlBaseWebhook: string) {
|
||||
Vue.set(state, 'urlBaseWebhook', urlBaseWebhook);
|
||||
},
|
||||
setEndpointWebhook (state, endpointWebhook: string) {
|
||||
setEndpointWebhook(state, endpointWebhook: string) {
|
||||
Vue.set(state, 'endpointWebhook', endpointWebhook);
|
||||
},
|
||||
setEndpointWebhookTest (state, endpointWebhookTest: string) {
|
||||
setEndpointWebhookTest(state, endpointWebhookTest: string) {
|
||||
Vue.set(state, 'endpointWebhookTest', endpointWebhookTest);
|
||||
},
|
||||
|
||||
setSaveDataErrorExecution (state, newValue: string) {
|
||||
setSaveDataErrorExecution(state, newValue: string) {
|
||||
Vue.set(state, 'saveDataErrorExecution', newValue);
|
||||
},
|
||||
setSaveDataSuccessExecution (state, newValue: string) {
|
||||
setSaveDataSuccessExecution(state, newValue: string) {
|
||||
Vue.set(state, 'saveDataSuccessExecution', newValue);
|
||||
},
|
||||
setSaveManualExecutions (state, saveManualExecutions: boolean) {
|
||||
setSaveManualExecutions(state, saveManualExecutions: boolean) {
|
||||
Vue.set(state, 'saveManualExecutions', saveManualExecutions);
|
||||
},
|
||||
setTimezone (state, timezone: string) {
|
||||
setTimezone(state, timezone: string) {
|
||||
Vue.set(state, 'timezone', timezone);
|
||||
},
|
||||
setExecutionTimeout (state, executionTimeout: number) {
|
||||
setExecutionTimeout(state, executionTimeout: number) {
|
||||
Vue.set(state, 'executionTimeout', executionTimeout);
|
||||
},
|
||||
setMaxExecutionTimeout (state, maxExecutionTimeout: number) {
|
||||
setMaxExecutionTimeout(state, maxExecutionTimeout: number) {
|
||||
Vue.set(state, 'maxExecutionTimeout', maxExecutionTimeout);
|
||||
},
|
||||
setVersionCli(state, version: string) {
|
||||
|
@ -570,25 +612,25 @@ export const store = new Vuex.Store({
|
|||
setDefaultLocale(state, locale: string) {
|
||||
Vue.set(state, 'defaultLocale', locale);
|
||||
},
|
||||
setActiveNode (state, nodeName: string) {
|
||||
setActiveNode(state, nodeName: string) {
|
||||
state.activeNode = nodeName;
|
||||
},
|
||||
setActiveCredentialType (state, activeCredentialType: string) {
|
||||
setActiveCredentialType(state, activeCredentialType: string) {
|
||||
state.activeCredentialType = activeCredentialType;
|
||||
},
|
||||
|
||||
setLastSelectedNode (state, nodeName: string) {
|
||||
setLastSelectedNode(state, nodeName: string) {
|
||||
state.lastSelectedNode = nodeName;
|
||||
},
|
||||
|
||||
setLastSelectedNodeOutputIndex (state, outputIndex: number | null) {
|
||||
setLastSelectedNodeOutputIndex(state, outputIndex: number | null) {
|
||||
state.lastSelectedNodeOutputIndex = outputIndex;
|
||||
},
|
||||
|
||||
setWorkflowExecutionData (state, workflowResultData: IExecutionResponse | null) {
|
||||
setWorkflowExecutionData(state, workflowResultData: IExecutionResponse | null) {
|
||||
state.workflowExecutionData = workflowResultData;
|
||||
},
|
||||
addNodeExecutionData (state, pushData: IPushDataNodeExecuteAfter): void {
|
||||
addNodeExecutionData(state, pushData: IPushDataNodeExecuteAfter): void {
|
||||
if (state.workflowExecutionData === null) {
|
||||
throw new Error('The "workflowExecutionData" is not initialized!');
|
||||
}
|
||||
|
@ -597,7 +639,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
|
||||
},
|
||||
clearNodeExecutionData (state, nodeName: string): void {
|
||||
clearNodeExecutionData(state, nodeName: string): void {
|
||||
if (state.workflowExecutionData === null) {
|
||||
return;
|
||||
}
|
||||
|
@ -605,19 +647,25 @@ export const store = new Vuex.Store({
|
|||
Vue.delete(state.workflowExecutionData.data.resultData.runData, nodeName);
|
||||
},
|
||||
|
||||
setWorkflowSettings (state, workflowSettings: IWorkflowSettings) {
|
||||
setWorkflowSettings(state, workflowSettings: IWorkflowSettings) {
|
||||
Vue.set(state.workflow, 'settings', workflowSettings);
|
||||
},
|
||||
|
||||
setWorkflowTagIds (state, tags: string[]) {
|
||||
setWorkflowPinData(state, pinData: PinData) {
|
||||
Vue.set(state.workflow, 'pinData', pinData);
|
||||
|
||||
dataPinningEventBus.$emit('pin-data', pinData);
|
||||
},
|
||||
|
||||
setWorkflowTagIds(state, tags: string[]) {
|
||||
Vue.set(state.workflow, 'tags', tags);
|
||||
},
|
||||
|
||||
addWorkflowTagIds (state, tags: string[]) {
|
||||
addWorkflowTagIds(state, tags: string[]) {
|
||||
Vue.set(state.workflow, 'tags', [...new Set([...(state.workflow.tags || []), ...tags])]);
|
||||
},
|
||||
|
||||
removeWorkflowTagId (state, tagId: string) {
|
||||
removeWorkflowTagId(state, tagId: string) {
|
||||
const tags = state.workflow.tags as string[];
|
||||
const updated = tags.filter((id: string) => id !== tagId);
|
||||
|
||||
|
@ -625,7 +673,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
// Workflow
|
||||
setWorkflow (state, workflow: IWorkflowDb) {
|
||||
setWorkflow(state, workflow: IWorkflowDb) {
|
||||
Vue.set(state, 'workflow', workflow);
|
||||
|
||||
if (!state.workflow.hasOwnProperty('active')) {
|
||||
|
@ -651,7 +699,7 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
},
|
||||
|
||||
updateNodeTypes (state, nodeTypes: INodeTypeDescription[]) {
|
||||
updateNodeTypes(state, nodeTypes: INodeTypeDescription[]) {
|
||||
const oldNodesToKeep = state.nodeTypes.filter(node => !nodeTypes.find(n => n.name === node.name && n.version.toString() === node.version.toString()));
|
||||
const newNodesState = [...oldNodesToKeep, ...nodeTypes];
|
||||
|
||||
|
@ -673,7 +721,7 @@ export const store = new Vuex.Store({
|
|||
},
|
||||
getters: {
|
||||
executedNode: (state): string | undefined => {
|
||||
return state.workflowExecutionData? state.workflowExecutionData.executedNode: undefined;
|
||||
return state.workflowExecutionData ? state.workflowExecutionData.executedNode : undefined;
|
||||
},
|
||||
activeCredentialType: (state): string | null => {
|
||||
return state.activeCredentialType;
|
||||
|
@ -729,7 +777,7 @@ export const store = new Vuex.Store({
|
|||
return `${state.urlBaseWebhook}${state.endpointWebhookTest}`;
|
||||
},
|
||||
|
||||
getStateIsDirty: (state) : boolean => {
|
||||
getStateIsDirty: (state): boolean => {
|
||||
return state.stateIsDirty;
|
||||
},
|
||||
|
||||
|
@ -832,8 +880,8 @@ export const store = new Vuex.Store({
|
|||
allNodes: (state): INodeUi[] => {
|
||||
return state.workflow.nodes;
|
||||
},
|
||||
nodesByName: (state: IRootState): {[name: string]: INodeUi} => {
|
||||
return state.workflow.nodes.reduce((accu: {[name: string]: INodeUi}, node) => {
|
||||
nodesByName: (state: IRootState): { [name: string]: INodeUi } => {
|
||||
return state.workflow.nodes.reduce((accu: { [name: string]: INodeUi }, node) => {
|
||||
accu[node.name] = node;
|
||||
return accu;
|
||||
}, {});
|
||||
|
@ -853,11 +901,32 @@ export const store = new Vuex.Store({
|
|||
allNodeTypes: (state): INodeTypeDescription[] => {
|
||||
return state.nodeTypes;
|
||||
},
|
||||
/**
|
||||
* Pin data
|
||||
*/
|
||||
|
||||
pinData: (state): PinData | undefined => {
|
||||
return state.workflow.pinData;
|
||||
},
|
||||
pinDataByNodeName: (state) => (nodeName: string) => {
|
||||
return state.workflow.pinData && state.workflow.pinData[nodeName];
|
||||
},
|
||||
pinDataSize: (state) => {
|
||||
return state.workflow.nodes
|
||||
.reduce((acc, node) => {
|
||||
if (typeof node.pinData !== 'undefined' && node.name !== state.activeNode) {
|
||||
acc += stringSizeInBytes(node.pinData);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc.
|
||||
*/
|
||||
nativelyNumberSuffixedDefaults: (_, getters): string[] => {
|
||||
const { allNodeTypes } = getters as {
|
||||
const {allNodeTypes} = getters as {
|
||||
allNodeTypes: Array<INodeTypeDescription & { defaults: { name: string } }>;
|
||||
};
|
||||
|
||||
|
|
|
@ -74,7 +74,12 @@
|
|||
:class="['add-sticky-button', showStickyButton ? 'visible-button' : '']"
|
||||
@click="nodeTypeSelected(STICKY_NODE_TYPE)"
|
||||
>
|
||||
<n8n-icon-button size="large" :icon="['far', 'note-sticky']" type="outline" :title="$locale.baseText('nodeView.addSticky')"/>
|
||||
<n8n-icon-button
|
||||
size="medium"
|
||||
type="secondary"
|
||||
:icon="['far', 'note-sticky']"
|
||||
:title="$locale.baseText('nodeView.addSticky')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,33 +89,27 @@
|
|||
@closeNodeCreator="closeNodeCreator"
|
||||
/>
|
||||
<div :class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
||||
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
||||
<font-awesome-icon icon="expand"/>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="button-white" :title="$locale.baseText('nodeView.zoomIn')">
|
||||
<font-awesome-icon icon="search-plus"/>
|
||||
</button>
|
||||
<button @click="zoomOut()" class="button-white" :title="$locale.baseText('nodeView.zoomOut')">
|
||||
<font-awesome-icon icon="search-minus"/>
|
||||
</button>
|
||||
<button
|
||||
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')" icon="expand" />
|
||||
<n8n-icon-button @click="zoomIn" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomIn')" icon="search-plus" />
|
||||
<n8n-icon-button @click="zoomOut" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomOut')" icon="search-minus" />
|
||||
<n8n-icon-button
|
||||
v-if="nodeViewScale !== 1 && !isDemo"
|
||||
@click="resetZoom()"
|
||||
class="button-white"
|
||||
@click="resetZoom"
|
||||
type="tertiary"
|
||||
size="large"
|
||||
:title="$locale.baseText('nodeView.resetZoom')"
|
||||
>
|
||||
<font-awesome-icon icon="undo" :title="$locale.baseText('nodeView.resetZoom')"/>
|
||||
</button>
|
||||
icon="undo"
|
||||
/>
|
||||
</div>
|
||||
<div class="workflow-execute-wrapper" v-if="!isReadOnly">
|
||||
<n8n-button
|
||||
@click.stop="onRunWorkflow"
|
||||
:loading="workflowRunning"
|
||||
:label="runButtonText"
|
||||
:title="$locale.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')"
|
||||
size="large"
|
||||
icon="play-circle"
|
||||
:title="$locale.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')"
|
||||
:type="workflowRunning ? 'light' : 'primary'"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
<n8n-icon-button
|
||||
|
@ -118,7 +117,7 @@
|
|||
icon="stop"
|
||||
size="large"
|
||||
class="stop-execution"
|
||||
type="light"
|
||||
type="secondary"
|
||||
:title="stopExecutionInProgress
|
||||
? $locale.baseText('nodeView.stoppingCurrentExecution')
|
||||
: $locale.baseText('nodeView.stopCurrentExecution')
|
||||
|
@ -133,7 +132,7 @@
|
|||
icon="stop"
|
||||
size="large"
|
||||
:title="$locale.baseText('nodeView.stopWaitingForWebhookCall')"
|
||||
type="light"
|
||||
type="secondary"
|
||||
@click.stop="stopWaitingForWebhook()"
|
||||
/>
|
||||
|
||||
|
@ -196,6 +195,7 @@ import {
|
|||
TelemetryHelpers,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase,
|
||||
PinData,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ICredentialsResponse,
|
||||
|
@ -220,6 +220,7 @@ import {
|
|||
|
||||
import '../plugins/N8nCustomConnectorType';
|
||||
import '../plugins/PlusEndpointType';
|
||||
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
|
@ -549,6 +550,7 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||
|
||||
this.$store.commit('setWorkflowExecutionData', data);
|
||||
this.$store.commit('setWorkflowPinData', data.workflowData.pinData);
|
||||
|
||||
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections)));
|
||||
this.$nextTick(() => {
|
||||
|
@ -556,6 +558,7 @@ export default mixins(
|
|||
this.$store.commit('setStateDirty', false);
|
||||
});
|
||||
|
||||
|
||||
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
||||
this.$telemetry.track('User opened read-only execution', { workflow_id: data.workflowData.id, execution_mode: data.mode, execution_finished: data.finished });
|
||||
|
||||
|
@ -610,6 +613,11 @@ export default mixins(
|
|||
this.resetWorkspace();
|
||||
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
|
||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||
|
||||
if (data.workflow.pinData) {
|
||||
this.$store.commit('setWorkflowPinData', data.workflow.pinData);
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.zoomToFit();
|
||||
});
|
||||
|
@ -678,6 +686,7 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowId', workflowId);
|
||||
this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false});
|
||||
this.$store.commit('setWorkflowSettings', data.settings || {});
|
||||
this.$store.commit('setWorkflowPinData', data.pinData || {});
|
||||
|
||||
const tags = (data.tags || []) as ITag[];
|
||||
this.$store.commit('tags/upsertTags', tags);
|
||||
|
@ -1312,6 +1321,10 @@ export default mixins(
|
|||
});
|
||||
});
|
||||
|
||||
if (workflowData.pinData) {
|
||||
this.$store.commit('setWorkflowPinData', workflowData.pinData);
|
||||
}
|
||||
|
||||
const tagsEnabled = this.$store.getters['settings/areTagsEnabled'];
|
||||
if (importTags && tagsEnabled && Array.isArray(workflowData.tags)) {
|
||||
const allTags: ITag[] = await this.$store.dispatch('tags/fetchAll');
|
||||
|
@ -2077,6 +2090,10 @@ export default mixins(
|
|||
// so if we do not connect we have to save the connection manually
|
||||
this.$store.commit('addConnection', connectionProperties);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.addPinDataConnections(this.$store.getters.pinData);
|
||||
});
|
||||
},
|
||||
__removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) {
|
||||
if (removeVisualConnection === true) {
|
||||
|
@ -2168,6 +2185,14 @@ export default mixins(
|
|||
|
||||
await this.addNodes([newNodeData]);
|
||||
|
||||
const pinData = this.$store.getters['pinDataByNodeName'](nodeName);
|
||||
if (pinData) {
|
||||
this.$store.commit('pinData', {
|
||||
node: newNodeData,
|
||||
data: pinData,
|
||||
});
|
||||
}
|
||||
|
||||
this.$store.commit('setStateDirty', true);
|
||||
|
||||
// Automatically deselect all nodes and select the current one and also active
|
||||
|
@ -2271,6 +2296,7 @@ export default mixins(
|
|||
|
||||
if (connection) {
|
||||
const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex];
|
||||
|
||||
if (!output || !output.total) {
|
||||
CanvasHelpers.resetConnection(connection);
|
||||
}
|
||||
|
@ -2832,7 +2858,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$store.commit('removeAllConnections', {setStateDirty: false});
|
||||
this.$store.commit('removeAllNodes', {setStateDirty: false});
|
||||
this.$store.commit('removeAllNodes', { setStateDirty: false, removePinData: true });
|
||||
|
||||
// Reset workflow execution data
|
||||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
|
@ -2940,6 +2966,31 @@ export default mixins(
|
|||
await this.importWorkflowData(workflowData);
|
||||
}
|
||||
},
|
||||
addPinDataConnections(pinData: PinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName),
|
||||
}) as Connection[];
|
||||
|
||||
connections.forEach((connection) => {
|
||||
CanvasHelpers.addConnectionOutputSuccess(connection, {
|
||||
total: pinData[nodeName].length,
|
||||
iterations: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
removePinDataConnections(pinData: PinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName),
|
||||
}) as Connection[];
|
||||
|
||||
connections.forEach(CanvasHelpers.resetConnection);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
|
@ -2989,10 +3040,14 @@ export default mixins(
|
|||
setTimeout(() => {
|
||||
this.$store.dispatch('users/showPersonalizationSurvey');
|
||||
this.checkForNewVersions();
|
||||
this.addPinDataConnections(this.$store.getters.pinData);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
this.$externalHooks().run('nodeView.mount');
|
||||
|
||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
|
@ -3002,6 +3057,9 @@ export default mixins(
|
|||
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
|
||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -3013,8 +3071,8 @@ export default mixins(
|
|||
|
||||
position: fixed;
|
||||
left: $--sidebar-width + $--zoom-menu-margin;
|
||||
width: 200px;
|
||||
bottom: 45px;
|
||||
width: 210px;
|
||||
bottom: 44px;
|
||||
line-height: 25px;
|
||||
color: #444;
|
||||
padding-right: 5px;
|
||||
|
@ -3026,6 +3084,16 @@ export default mixins(
|
|||
button {
|
||||
border: var(--border-base);
|
||||
}
|
||||
|
||||
> * {
|
||||
+ * {
|
||||
margin-left: var(--spacing-3xs);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.regular-zoom-menu {
|
||||
|
@ -3198,23 +3266,6 @@ export default mixins(
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.button-white {
|
||||
border: none;
|
||||
padding: 0.3em;
|
||||
margin: 0 0.1em;
|
||||
border-radius: 3px;
|
||||
font-size: 1.2em;
|
||||
background: #fff;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
&:hover {
|
||||
display: block !important;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const path = require('path');
|
||||
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
|
@ -27,6 +28,12 @@ module.exports = {
|
|||
plugins: [
|
||||
new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'element-ui/packages/button': path.resolve(__dirname, '..', 'design-system/src/components/N8nButton/overrides/ElButton.vue'),
|
||||
'element-ui/lib/button': path.resolve(__dirname, '..', 'design-system/src/components/N8nButton/overrides/ElButton.vue'),
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
loaderOptions: {
|
||||
|
|
|
@ -839,6 +839,11 @@ export interface INode {
|
|||
parameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
webhookId?: string;
|
||||
pinData?: IDataObject;
|
||||
}
|
||||
|
||||
export interface PinData {
|
||||
[nodeName: string]: IDataObject[];
|
||||
}
|
||||
|
||||
export interface INodes {
|
||||
|
@ -1323,6 +1328,7 @@ export interface IRunExecutionData {
|
|||
resultData: {
|
||||
error?: ExecutionError;
|
||||
runData: IRunData;
|
||||
pinData?: PinData;
|
||||
lastNodeExecuted?: string;
|
||||
};
|
||||
executionData?: {
|
||||
|
@ -1396,6 +1402,7 @@ export interface IWorkflowBase {
|
|||
connections: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
staticData?: IDataObject;
|
||||
pinData?: PinData;
|
||||
}
|
||||
|
||||
export interface IWorkflowCredentials {
|
||||
|
@ -1516,6 +1523,7 @@ export interface INodesGraph {
|
|||
node_connections: IDataObject[];
|
||||
nodes: INodesGraphNode;
|
||||
notes: INotesGraphNode;
|
||||
is_pinned: boolean;
|
||||
}
|
||||
|
||||
export interface INodesGraphNode {
|
||||
|
|
|
@ -1060,12 +1060,13 @@ export function getNodeWebhookUrl(
|
|||
export function getNodeParametersIssues(
|
||||
nodePropertiesArray: INodeProperties[],
|
||||
node: INode,
|
||||
pinDataNodeNames?: string[],
|
||||
): INodeIssues | null {
|
||||
const foundIssues: INodeIssues = {};
|
||||
let propertyIssues: INodeIssues;
|
||||
|
||||
if (node.disabled === true) {
|
||||
// Ignore issues on disabled nodes
|
||||
if (node.disabled === true || pinDataNodeNames?.includes(node.name)) {
|
||||
// Ignore issues on disabled and pindata nodes
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -120,6 +120,7 @@ export function generateNodesGraph(
|
|||
node_connections: [],
|
||||
nodes: {},
|
||||
notes: {},
|
||||
is_pinned: Object.keys(workflow.pinData ?? {}).length > 0,
|
||||
};
|
||||
const nodeNameAndIndex: INodeNameIndex = {};
|
||||
const webhookNodeNames: string[] = [];
|
||||
|
|
|
@ -252,6 +252,7 @@ export class Workflow {
|
|||
checkReadyForExecution(inputData: {
|
||||
startNode?: string;
|
||||
destinationNode?: string;
|
||||
pinDataNodeNames?: string[];
|
||||
}): IWorfklowIssues | null {
|
||||
let node: INode;
|
||||
let nodeType: INodeType | undefined;
|
||||
|
@ -287,7 +288,11 @@ export class Workflow {
|
|||
typeUnknown: true,
|
||||
};
|
||||
} else {
|
||||
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node);
|
||||
nodeIssues = NodeHelpers.getNodeParametersIssues(
|
||||
nodeType.description.properties,
|
||||
node,
|
||||
inputData.pinDataNodeNames,
|
||||
);
|
||||
}
|
||||
|
||||
if (nodeIssues !== null) {
|
||||
|
|
Loading…
Reference in a new issue