mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00: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 in bdb84130d6
* 🧪 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 in bdb84130d6
* 🧪 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user