feat: add resource locator parameter (#3932)
* ✨ Added resource locator interfaces to `n8n-workflow` package * ✅ Updating Trello node to use resource locator property type * ✨ Added resource locator prop to Delete Board` Trello operation * ✔️ Fiixing linting errors in Trello node * ✨ Added list mode to Trello test node * ⚡ Updating resource locator modes interface * ⚡ Updating Trello test node validation messages and placeholders * N8N-4175 resource locator component (#3812) * ✨ Implemented initial version of resource locator component * ✨ Implemented front-end validation for resource locator component. Improved responsiveness. Minor refactoring. * ⚡ Setting resource locator default state to list. Updating hover states and expand icon. * 🔨 Moving resource locator component to `ParameterInput` from `ParameterInputFull * 🔨 Moving `ResourceLocator` to a separate Vue component * 🔨 Implementing expression and drag'n'drop support in ResourceLocator` component * 🔨 Cleaning up `ResourceLocator` component code * ✨ Implemented resource locator selected mode persistance * 💄 Minor refactoring and fixes in `ResourceLocator` * 🔨 Updating `ResourceLocator` front-end validation logic * ⚡ Saving resource locator mode in node parameters * 💄 Updating the `ResourceLocator` component based on the design review * 🐛 Fixing resource locator mode parameters handling when loading node parameter values on front-end * 💄 Removing leftover unused CSS * ⚡ Updating interfaces to support resource locator value types * ⚡ Updating `ResourceLocator` component to work with object parameter values * 🔨 Cleaning up `ResourceLocator` and related components code * ⚡ Preventing `DraggableTarget` to be sticky if disabled * 🐛 Fixing a bug with resource locator value parameter * 👌 Adding new type alias for all possible node parameter value types * 👌 Updating `ResourceLocator` and related components based on PR review feedback * ⚡ Adding disabled mode to `ResourceLocator` component, fixing expression handling, minor refactoring. * 💄 Updating disabled state styling in `ResourceLocator` component * ⚡ Setting correct default value for test node and removing unnecessary logic * 💄 Added regex URL validation to Trello test node * ✨ Updating Trello test node with another (list mode only) test case * ✔️ Fixing linting error in Trello node * 🔨 Removing hardcoded custom modes and modes order * Add value extractor to routing node (#3777) * ✨ add value extractor to routing node * ✨ add value extractor to property modes * 🔊 improve error logging for value extractor * 🔥 remove old extractValue methods from RoutingNode * ⚡ extractValue inside getNodeParameter * 🔥 remove extract value test from RoutingNode * ✨ make value extraction optional * 🥅 move extract value so proper error messages are sent * 🚨 readd accidentally removed eslint-disable * ✨ add resource locator support extractValue * 🚨 remove unused import * 🐛 fix getting value of resource locator * 💄 Updating resource locator component styling and handling reset value action * ✨ create v2 of Trello node for resource locator * 💄 Updating ResourceLocator droppable & activeDrop classes and removing input padding-right * ⚡ Updating Trello test node with single-mode test case * ⚡ Updating field names in Trello node to avoid name clash * 💄 Updating test Trello node mode order and board:update parameter name * 💄 Updating test node parameter names and display options * List mode search endpoint (#3936) * 🚧 super basic version of the search endpoint This version is built using a hacked up version of the Google Drive node. I need to properly create a v2 for it but it's does work. * 🚧 fixed up type errors and return urls * ✨ add v3 of Google Drive node with RLC * ✨ add RLC to Google Drive Shared Drive operations * ♻️ address some small changes requested in review * 🐛 move list search out of /nodes/ and add check for required param * ✨ google drive folder search * ✨ google drive search sort by name * ✨ add searchable flag for RLC * ✏️ fix google drive wording for v3 * Trello and Airtable search backend (#3974) * ✨ add search to Trello boards * ✨ add RLC to Trello cards * ♻️ use new versioning system for Trello v2 * 🐛 move list search out of /nodes/ and add check for required param * ✨ re-add trello search methods * 🥅 throw error if RLC search isn't sent a method name This will likely be removed when the declarative style of search has been added. * ✨ add requires filter field to RLC search * ✨ add searchable flag to Trello searches * ✨ add RLC for cardId and boardId on all operations * ✨ add ID and URL RLC to Airtable * N8 n 4179 resource locator list mode (#3933) * ✨ Implemented initial version of list mode dropdown * ✨ Handling mode switching and expression support in list mode * 🔨 Removing `sortedModes` references * ⚡ Fixing list mode UI after latest mege * 💄 Updating padding-right for input fields with suffix slots * ✨ Minor fixes to validation, mode switching logic and styling * update error * 2 or more regex * update regex to be more strict * remove expr colors * update hint * 🚧 super basic version of the search endpoint This version is built using a hacked up version of the Google Drive node. I need to properly create a v2 for it but it's does work. * 🚧 fixed up type errors and return urls * begin list impl * ✨ add v3 of Google Drive node with RLC * fix ts issue * introduce dropdown * add more behavior * update design * show search * add filtering * push up selected * add keyboard nav * add loading * add caching * remove console * fix build issues * add debounce * fix click * keep event on focus * fix input size bug * add resource locator type * update type * update interface * update resource locator types * ✨ add search to Trello boards * ✨ add RLC to Google Drive Shared Drive operations * update * update name * add package * use stringify pckg * handle long vals * fix bug in url id modes * remove console log * add lazy loading * add lazy loading on filtering * clean up * make search clearable * add error state * ✨ add RLC to Trello cards * ♻️ address some small changes requested in review * ♻️ use new versioning system for Trello v2 * refactor a bit * fix how loading happens * clear after blur * update api * comment out test code * update api * relaod in case of error * update endpoint * 🐛 move list search out of /nodes/ and add check for required param * 🐛 move list search out of /nodes/ and add check for required param * update req handling * update endpoint * ✨ re-add trello search methods * 🥅 throw error if RLC search isn't sent a method name This will likely be removed when the declarative style of search has been added. * get api to work * update scroll handling * ✨ google drive folder search * ✨ add requires filter field to RLC search * ✨ google drive search sort by name * remove console * ✨ add searchable flag for RLC * ✨ add searchable flag to Trello searches * update searchable * ✨ add RLC for cardId and boardId on all operations * ✨ add ID and URL RLC to Airtable * fix up search * remove extra padding * add link button * update popper pos * format * fix formating * update mode change * add name urls * update regex and errors * upate error * update errors * update airtable regex * update trello regex rules * udpate param name * update * update param * update param * update drive node * update params * add keyboard nav * fix bug * update airtable default mode * fix default value issue * hide long selected value * update duplicate reqs * update node * clean up impl * dedupe resources * fix up nv * resort params * update icon * set placeholders * default to id mode * add telemetry * add refresh opt * clean up tmp val * revert test change * make placeholder optional * update validation * remove description as param hint * support more general values * fix links on long names * update resource item styles * update pos * update icon color * update link alt * check if required * move validation to workflow * update naming * only show warning at param level * show right border on focus * fix hover on all item * fix long names bug * fix expr bug * add expr * update legacy mode * fix up impl * clean up node types * clean up types * remove unnessary type * clean up types * clean up types * clean up types * clea n up localizaiton * remove unused key * clean up helpers * clean up paraminput * clean up paraminputfull * refactor into one loop * update component * update class names * update prop types * update name cases * update casing * clean up classes * clean up resource locator * update drop handling * update mode * add url for link mode * clear value by default * add placeholder * remove legacy hint * handle expr in legacy * fix typos * revert padding change * fix up spacing * update to link component * support urls for id * fix replacement * build Co-authored-by: Milorad Filipovic <milorad@n8n.io> Co-authored-by: Valya Bullions <valya@n8n.io> * refactor: Resource locator review changes (#4109) * ✨ Implemented initial version of list mode dropdown * ✨ Handling mode switching and expression support in list mode * 🔨 Removing `sortedModes` references * ⚡ Fixing list mode UI after latest mege * 💄 Updating padding-right for input fields with suffix slots * ✨ Minor fixes to validation, mode switching logic and styling * update error * 2 or more regex * update regex to be more strict * remove expr colors * update hint * 🚧 super basic version of the search endpoint This version is built using a hacked up version of the Google Drive node. I need to properly create a v2 for it but it's does work. * 🚧 fixed up type errors and return urls * begin list impl * ✨ add v3 of Google Drive node with RLC * fix ts issue * introduce dropdown * add more behavior * update design * show search * add filtering * push up selected * add keyboard nav * add loading * add caching * remove console * fix build issues * add debounce * fix click * keep event on focus * fix input size bug * add resource locator type * update type * update interface * update resource locator types * ✨ add search to Trello boards * ✨ add RLC to Google Drive Shared Drive operations * update * update name * add package * use stringify pckg * handle long vals * fix bug in url id modes * remove console log * add lazy loading * add lazy loading on filtering * clean up * make search clearable * add error state * ✨ add RLC to Trello cards * ♻️ address some small changes requested in review * ♻️ use new versioning system for Trello v2 * refactor a bit * fix how loading happens * clear after blur * update api * comment out test code * update api * relaod in case of error * update endpoint * 🐛 move list search out of /nodes/ and add check for required param * 🐛 move list search out of /nodes/ and add check for required param * update req handling * update endpoint * ✨ re-add trello search methods * 🥅 throw error if RLC search isn't sent a method name This will likely be removed when the declarative style of search has been added. * get api to work * update scroll handling * ✨ google drive folder search * ✨ add requires filter field to RLC search * ✨ google drive search sort by name * remove console * ✨ add searchable flag for RLC * ✨ add searchable flag to Trello searches * update searchable * ✨ add RLC for cardId and boardId on all operations * ✨ add ID and URL RLC to Airtable * fix up search * remove extra padding * add link button * update popper pos * format * fix formating * update mode change * add name urls * update regex and errors * upate error * update errors * update airtable regex * update trello regex rules * udpate param name * update * update param * update param * update drive node * update params * add keyboard nav * fix bug * update airtable default mode * fix default value issue * hide long selected value * update duplicate reqs * update node * clean up impl * dedupe resources * fix up nv * resort params * update icon * set placeholders * default to id mode * add telemetry * add refresh opt * clean up tmp val * revert test change * make placeholder optional * update validation * remove description as param hint * support more general values * fix links on long names * update resource item styles * update pos * update icon color * update link alt * check if required * move validation to workflow * update naming * only show warning at param level * show right border on focus * fix hover on all item * fix long names bug * ♻️ refactor extractValue to allow multiple props with same name * ♻️ use correct import for displayParameterPath * fix expr bug * add expr * update legacy mode * fix up impl * clean up node types * clean up types * ♻️ remove new version of google drive node * ♻️ removed versioned Trello node for RLC * remove unnessary type * ♻️ remove versioned Airtable not for RLC * clean up types * clean up types * clean up types * clea n up localizaiton * remove unused key * clean up helpers * clean up paraminput * clean up paraminputfull * refactor into one loop * update component * update class names * update prop types * update name cases * update casing * clean up classes * 💬 updated RLC URL regex error wording * clean up resource locator * update drop handling * update mode * 💬 reword value extractor errors * 🚨 remove unneeded eslint ignores for RLC modes * 💬 update Trello 400 error message * 🚨 re-add removed types in editor-ui Also ts-ignore something that was clean up in another commit. I've added a comment to fix after someone else can look at it. * 💬 remove hints from Google Drive RLCs * 🥅 rethrow correct errors in Trello node * ✨ add url for id mode on Google Drive * 🔥 remove unused Google Drive file * 🔊 change console.error to use logger instead * 🔀 fix bad merges * ♻️ small changes from review * ♻️ remove ts-ignore Co-authored-by: Milorad Filipovic <milorad@n8n.io> Co-authored-by: Mutasem <mutdmour@gmail.com> * fix build * update tests * fix bug with credential card * update popover component * fix expressions url * fix type issue * format * update alt * fix lint issues * fix eslint issues Co-authored-by: Milorad Filipovic <milorad@n8n.io> Co-authored-by: Milorad FIlipović <miloradfilipovic19@gmail.com> Co-authored-by: Valya <68596159+valya@users.noreply.github.com> Co-authored-by: Valya Bullions <valya@n8n.io>
This commit is contained in:
@@ -23,8 +23,9 @@ import {
|
||||
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { genericHelpers } from "@/components/mixins/genericHelpers";
|
||||
import { debounceHelper } from "./mixins/debounce";
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
export default mixins(genericHelpers, debounceHelper).extend({
|
||||
name: "BreakpointsObserver",
|
||||
props: [
|
||||
"valueXS",
|
||||
@@ -98,4 +99,4 @@ export default mixins(genericHelpers).extend({
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default mixins(
|
||||
},
|
||||
methods: {
|
||||
async onClick() {
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id: this.data.id});
|
||||
this.$store.dispatch('ui/openExistingCredential', { id: this.data.id});
|
||||
},
|
||||
async onAction(action: string) {
|
||||
if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) {
|
||||
|
||||
@@ -107,7 +107,7 @@ export default mixins(
|
||||
},
|
||||
|
||||
editCredential (credential: ICredentialsResponse) {
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id: credential.id});
|
||||
this.$store.dispatch('ui/openExistingCredential', { id: credential.id});
|
||||
this.$telemetry.track('User opened Credential modal', { credential_type: credential.type, source: 'primary_menu', new_credential: false, workflow_id: this.$store.getters.workflowId });
|
||||
},
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export default Vue.extend({
|
||||
|
||||
this.hovering = e.clientX >= dim.left && e.clientX <= dim.right && e.clientY >= dim.top && e.clientY <= dim.bottom;
|
||||
|
||||
if (this.sticky && this.hovering) {
|
||||
if (!this.disabled && this.sticky && this.hovering) {
|
||||
this.$store.commit('ui/setDraggableStickyPos', [dim.left + this.stickyOffset, dim.top + this.stickyOffset]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,12 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
import { debounceHelper } from './mixins/debounce';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
debounceHelper,
|
||||
).extend({
|
||||
name: 'ExpressionEdit',
|
||||
props: [
|
||||
|
||||
@@ -636,7 +636,7 @@ export default mixins(
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
color: $--custom-dialog-text-color;
|
||||
--menu-item-hover-fill: var(--color-primary-tint-3);
|
||||
--menu-item-hover-fill: var(--color-background-base);
|
||||
|
||||
.item-title {
|
||||
position: absolute;
|
||||
@@ -656,7 +656,7 @@ export default mixins(
|
||||
.el-menu {
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
--menu-item-hover-fill: var(--color-primary-tint-3);
|
||||
--menu-item-hover-fill: var(--color-background-base);
|
||||
|
||||
.el-menu--collapse {
|
||||
width: 75px;
|
||||
|
||||
@@ -314,7 +314,7 @@ export default mixins(
|
||||
|
||||
editCredential(credentialType: string): void {
|
||||
const { id } = this.node.credentials[credentialType];
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id });
|
||||
this.$store.dispatch('ui/openExistingCredential', { id });
|
||||
|
||||
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: false, workflow_id: this.$store.getters.workflowId });
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div @keydown.stop :class="parameterInputClasses">
|
||||
<expression-edit
|
||||
:dialogVisible="expressionEditDialogVisible"
|
||||
:value="value"
|
||||
:value="isResourceLocatorParameter ? (value ? value.value : '') : value"
|
||||
:parameter="parameter"
|
||||
:path="path"
|
||||
:eventSource="eventSource || 'ndv'"
|
||||
@@ -14,8 +14,26 @@
|
||||
:style="parameterInputWrapperStyle"
|
||||
@click="openExpressionEdit"
|
||||
>
|
||||
<resource-locator
|
||||
v-if="isResourceLocatorParameter"
|
||||
ref="resourceLocator"
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:displayTitle="displayTitle"
|
||||
:expressionDisplayValue="expressionDisplayValue"
|
||||
:isValueExpression="isValueExpression"
|
||||
:isReadOnly="isReadOnly"
|
||||
:parameterIssues="getIssues"
|
||||
:droppable="droppable"
|
||||
:node="node"
|
||||
:path="path"
|
||||
@input="valueChanged"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@drop="onResourceLocatorDrop"
|
||||
/>
|
||||
<n8n-input
|
||||
v-if="isValueExpression || droppable || forceShowExpression"
|
||||
v-else-if="isValueExpression || droppable || forceShowExpression"
|
||||
:size="inputSize"
|
||||
:type="getStringInputType"
|
||||
:rows="getArgument('rows')"
|
||||
@@ -23,7 +41,6 @@
|
||||
:title="displayTitle"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="
|
||||
['json', 'string'].includes(parameter.type) ||
|
||||
@@ -80,7 +97,7 @@
|
||||
<div slot="suffix" class="expand-input-icon-container">
|
||||
<font-awesome-icon
|
||||
v-if="!isReadOnly"
|
||||
icon="external-link-alt"
|
||||
icon="expand-alt"
|
||||
class="edit-window-button clickable"
|
||||
:title="$locale.baseText('parameterInput.openEditWindow')"
|
||||
@click="displayEditDialog()"
|
||||
@@ -260,7 +277,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<parameter-issues v-if="parameter.type !== 'credentialsSelect'" :issues="getIssues" />
|
||||
<parameter-issues v-if="parameter.type !== 'credentialsSelect' && !isResourceLocatorParameter" :issues="getIssues" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -274,7 +291,6 @@ import {
|
||||
import {
|
||||
NodeHelpers,
|
||||
NodeParameterValue,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptions,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
@@ -288,6 +304,7 @@ import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
import ScopesNotice from '@/components/ScopesNotice.vue';
|
||||
import ParameterOptions from '@/components/ParameterOptions.vue';
|
||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
|
||||
// @ts-ignore
|
||||
import PrismEditor from 'vue-prism-editor';
|
||||
import TextEdit from '@/components/TextEdit.vue';
|
||||
@@ -299,7 +316,8 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
import { hasExpressionMapping, isValueExpression } from './helpers';
|
||||
import { isResourceLocatorValue } from '@/typeGuards';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
@@ -308,7 +326,7 @@ export default mixins(
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'ParameterInput',
|
||||
name: 'parameter-input',
|
||||
components: {
|
||||
CodeEdit,
|
||||
ExpressionEdit,
|
||||
@@ -318,6 +336,7 @@ export default mixins(
|
||||
ScopesNotice,
|
||||
ParameterOptions,
|
||||
ParameterIssues,
|
||||
ResourceLocator,
|
||||
TextEdit,
|
||||
},
|
||||
props: [
|
||||
@@ -395,6 +414,9 @@ export default mixins(
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentialTypes']),
|
||||
isValueExpression(): boolean {
|
||||
return isValueExpression(this.parameter, this.value);
|
||||
},
|
||||
areExpressionsDisabled(): boolean {
|
||||
return this.$store.getters['ui/areExpressionsDisabled'];
|
||||
},
|
||||
@@ -459,7 +481,7 @@ export default mixins(
|
||||
|
||||
let returnValue;
|
||||
if (this.isValueExpression === false) {
|
||||
returnValue = this.value;
|
||||
returnValue = this.isResourceLocatorParameter ? (this.value ? this.value.value: '') : this.value;
|
||||
} else {
|
||||
returnValue = this.expressionValueComputed;
|
||||
}
|
||||
@@ -518,7 +540,7 @@ export default mixins(
|
||||
let computedValue: NodeParameterValue;
|
||||
|
||||
try {
|
||||
computedValue = this.resolveExpression(this.value) as NodeParameterValue;
|
||||
computedValue = this.resolveExpression(this.value.value || this.value) as NodeParameterValue;
|
||||
} catch (error) {
|
||||
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
|
||||
}
|
||||
@@ -610,15 +632,6 @@ export default mixins(
|
||||
isEditor (): boolean {
|
||||
return ['code', 'json'].includes(this.editorType);
|
||||
},
|
||||
isValueExpression () {
|
||||
if (this.parameter.noDataExpression === true) {
|
||||
return false;
|
||||
}
|
||||
if (typeof this.value === 'string' && this.value.charAt(0) === '=') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
editorType (): string {
|
||||
return this.getArgument('editor') as string;
|
||||
},
|
||||
@@ -659,7 +672,7 @@ export default mixins(
|
||||
const styles = {
|
||||
width: '100%',
|
||||
};
|
||||
if (this.parameter.type === 'credentialsSelect') {
|
||||
if (this.parameter.type === 'credentialsSelect' || this.isResourceLocatorParameter) {
|
||||
return styles;
|
||||
}
|
||||
if (this.getIssues.length) {
|
||||
@@ -683,6 +696,9 @@ export default mixins(
|
||||
workflow (): Workflow {
|
||||
return this.getCurrentWorkflow();
|
||||
},
|
||||
isResourceLocatorParameter (): boolean {
|
||||
return this.parameter.type === 'resourceLocator';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isRemoteParameterOption(option: INodePropertyOptions) {
|
||||
@@ -730,9 +746,9 @@ export default mixins(
|
||||
this.remoteParameterOptions.length = 0;
|
||||
|
||||
// Get the resolved parameter values of the current node
|
||||
const currentNodeParameters = this.$store.getters.activeNode.parameters;
|
||||
|
||||
try {
|
||||
const currentNodeParameters = (this.$store.getters.activeNode as INodeUi).parameters;
|
||||
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
|
||||
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
|
||||
const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined;
|
||||
@@ -803,7 +819,8 @@ export default mixins(
|
||||
return this.parameter.typeOptions[argumentName];
|
||||
},
|
||||
expressionUpdated (value: string) {
|
||||
this.valueChanged(value);
|
||||
const val = this.isResourceLocatorParameter ? { value, mode: this.value.mode } : value;
|
||||
this.valueChanged(val);
|
||||
},
|
||||
openExpressionEdit() {
|
||||
if (this.areExpressionsDisabled) {
|
||||
@@ -819,6 +836,9 @@ export default mixins(
|
||||
onBlur () {
|
||||
this.$emit('blur');
|
||||
},
|
||||
onResourceLocatorDrop(data: string) {
|
||||
this.$emit('drop', data);
|
||||
},
|
||||
setFocus () {
|
||||
if (this.isValueExpression) {
|
||||
this.expressionEditDialogVisible = true;
|
||||
@@ -844,7 +864,7 @@ export default mixins(
|
||||
// Set focus on field
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
if (this.$refs.inputField) {
|
||||
if (this.$refs.inputField && this.$refs.inputField.$el) {
|
||||
// @ts-ignore
|
||||
this.$refs.inputField.focus();
|
||||
}
|
||||
@@ -871,7 +891,7 @@ export default mixins(
|
||||
|
||||
this.$emit('textInput', parameterData);
|
||||
},
|
||||
valueChanged (value: string[] | string | number | boolean | Date | null) {
|
||||
valueChanged (value: string[] | string | number | boolean | Date | {} | null) {
|
||||
if (this.parameter.name === 'nodeCredentialType') {
|
||||
this.activeCredentialType = value as string;
|
||||
}
|
||||
@@ -916,9 +936,14 @@ export default mixins(
|
||||
this.expressionEditDialogVisible = true;
|
||||
} else if (command === 'addExpression') {
|
||||
if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
||||
this.valueChanged(`={{${this.value}}}`);
|
||||
}
|
||||
else {
|
||||
this.valueChanged({ value: `={{${this.value}}}`, mode: this.value.mode });
|
||||
} else if (this.isResourceLocatorParameter) {
|
||||
if (isResourceLocatorValue(this.value)) {
|
||||
this.valueChanged({ value: `=${this.value.value}`, mode: this.value.mode });
|
||||
} else {
|
||||
this.valueChanged({ value: `=${this.value}`, mode: '' });
|
||||
}
|
||||
} else {
|
||||
this.valueChanged(`=${this.value}`);
|
||||
}
|
||||
|
||||
@@ -934,8 +959,18 @@ export default mixins(
|
||||
.filter((value) => (this.parameterOptions || []).find((option) => option.value === value));
|
||||
}
|
||||
|
||||
this.valueChanged(typeof value !== 'undefined' ? value : null);
|
||||
if (this.isResourceLocatorParameter) {
|
||||
this.valueChanged({ value, mode: this.value.mode });
|
||||
} else {
|
||||
this.valueChanged(typeof value !== 'undefined' ? value : null);
|
||||
}
|
||||
} else if (command === 'refreshOptions') {
|
||||
if (this.isResourceLocatorParameter) {
|
||||
const resourceLocator = this.$refs.resourceLocator;
|
||||
if (resourceLocator) {
|
||||
(resourceLocator as Vue).$emit('refreshList');
|
||||
}
|
||||
}
|
||||
this.loadRemoteParameterOptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
:value="value"
|
||||
:isReadOnly="false"
|
||||
:showOptions="true"
|
||||
:isValueExpression="isValueExpression"
|
||||
@optionSelected="optionSelected"
|
||||
@menu-expanded="onMenuExpanded"
|
||||
/>
|
||||
@@ -29,6 +30,7 @@
|
||||
:errorHighlight="showRequiredErrors"
|
||||
:isForCredential="true"
|
||||
:eventSource="eventSource"
|
||||
:isValueExpression="isValueExpression"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@textInput="valueChanged"
|
||||
@@ -53,6 +55,8 @@ import ParameterInput from './ParameterInput.vue';
|
||||
import ParameterOptions from './ParameterOptions.vue';
|
||||
import InputHint from './ParameterInputHint.vue';
|
||||
import Vue from 'vue';
|
||||
import { isValueExpression } from './helpers';
|
||||
import { INodeParameterResourceLocator, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ParameterInputExpanded',
|
||||
@@ -63,6 +67,7 @@ export default Vue.extend({
|
||||
},
|
||||
props: {
|
||||
parameter: {
|
||||
type: Object as () => INodeProperties,
|
||||
},
|
||||
value: {
|
||||
},
|
||||
@@ -101,6 +106,9 @@ export default Vue.extend({
|
||||
|
||||
return false;
|
||||
},
|
||||
isValueExpression (): boolean {
|
||||
return isValueExpression(this.parameter, this.value as string | INodeParameterResourceLocator);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFocus() {
|
||||
|
||||
@@ -13,12 +13,19 @@
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
:showOptions="displayOptions"
|
||||
:showExpressionSelector="showExpressionSelector"
|
||||
@optionSelected="optionSelected"
|
||||
@menu-expanded="onMenuExpanded"
|
||||
/>
|
||||
</template>
|
||||
<template>
|
||||
<DraggableTarget type="mapping" :disabled="parameter.noDataExpression || isReadOnly" :sticky="true" :stickyOffset="4" @drop="onDrop">
|
||||
<draggable-target
|
||||
type="mapping"
|
||||
:disabled="isDropDisabled"
|
||||
:sticky="true"
|
||||
:stickyOffset="4"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<template v-slot="{ droppable, activeDrop }">
|
||||
<parameter-input
|
||||
ref="param"
|
||||
@@ -33,9 +40,10 @@
|
||||
@valueChanged="valueChanged"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@drop="onDrop"
|
||||
inputSize="small" />
|
||||
</template>
|
||||
</DraggableTarget>
|
||||
</draggable-target>
|
||||
<input-hint :class="$style.hint" :hint="$locale.nodeText().hint(parameter, path)" />
|
||||
</template>
|
||||
</n8n-input-label>
|
||||
@@ -57,12 +65,15 @@ import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from './mixins/showMessage';
|
||||
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
import { hasOnlyListMode } from './ResourceLocator/helpers';
|
||||
import { INodePropertyMode } from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from '@/typeGuards';
|
||||
|
||||
export default mixins(
|
||||
showMessage,
|
||||
)
|
||||
.extend({
|
||||
name: 'ParameterInputFull',
|
||||
name: 'parameter-input-full',
|
||||
components: {
|
||||
ParameterInput,
|
||||
InputHint,
|
||||
@@ -88,6 +99,15 @@ export default mixins(
|
||||
node (): INodeUi | null {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
isResourceLocator (): boolean {
|
||||
return this.parameter.type === 'resourceLocator';
|
||||
},
|
||||
isDropDisabled (): boolean {
|
||||
return this.parameter.noDataExpression || this.isReadOnly || this.isResourceLocator;
|
||||
},
|
||||
showExpressionSelector (): boolean {
|
||||
return this.isResourceLocator ? !hasOnlyListMode(this.parameter): true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFocus() {
|
||||
@@ -117,7 +137,7 @@ export default mixins(
|
||||
this.forceShowExpression = true;
|
||||
setTimeout(() => {
|
||||
if (this.node) {
|
||||
const prevValue = this.value;
|
||||
const prevValue = this.isResourceLocator ? this.value.value : this.value;
|
||||
let updatedValue: string;
|
||||
if (typeof prevValue === 'string' && prevValue.startsWith('=') && prevValue.length > 1) {
|
||||
updatedValue = `${prevValue} ${data}`;
|
||||
@@ -126,11 +146,43 @@ export default mixins(
|
||||
updatedValue = `=${data}`;
|
||||
}
|
||||
|
||||
const parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: updatedValue,
|
||||
};
|
||||
|
||||
let parameterData;
|
||||
if (this.isResourceLocator) {
|
||||
if (!isResourceLocatorValue(this.value)) {
|
||||
parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: { value: updatedValue, mode: '' },
|
||||
};
|
||||
}
|
||||
else if (this.value.mode === 'list' && this.parameter.modes && this.parameter.modes.length > 1) {
|
||||
let mode = this.parameter.modes.find((mode: INodePropertyMode) => mode.name === 'id') || null;
|
||||
if (!mode) {
|
||||
mode = this.parameter.modes.filter((mode: INodePropertyMode) => mode.name !== 'list')[0];
|
||||
}
|
||||
|
||||
parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: { value: updatedValue, mode: mode ? mode.name : '' },
|
||||
};
|
||||
}
|
||||
else {
|
||||
parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: { value: updatedValue, mode: this.value.mode },
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: updatedValue,
|
||||
};
|
||||
}
|
||||
|
||||
this.$emit('valueChanged', parameterData);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@visible-change="onMenuToggle"
|
||||
/>
|
||||
<n8n-radio-buttons
|
||||
v-if="parameter.noDataExpression !== true"
|
||||
v-if="parameter.noDataExpression !== true && showExpressionSelector"
|
||||
size="small"
|
||||
:value="selectedView"
|
||||
:disabled="isReadOnly"
|
||||
@@ -25,20 +25,39 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { isResourceLocatorValue } from '@/typeGuards';
|
||||
import { NodeParameterValueType } from 'n8n-workflow';
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { isValueExpression } from './helpers';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ParameterOptions',
|
||||
props: [
|
||||
'parameter',
|
||||
'isReadOnly',
|
||||
'value',
|
||||
'showOptions',
|
||||
],
|
||||
name: 'parameter-options',
|
||||
props: {
|
||||
parameter: {
|
||||
type: Object,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String, Number, Boolean] as PropType<NodeParameterValueType>,
|
||||
},
|
||||
showOptions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showExpressionSelector: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isDefault (): boolean {
|
||||
return this.parameter.default === this.value;
|
||||
},
|
||||
isValueExpression(): boolean {
|
||||
return isValueExpression(this.parameter, this.value);
|
||||
},
|
||||
shouldShowOptions (): boolean {
|
||||
if (this.isReadOnly === true) {
|
||||
return false;
|
||||
@@ -61,15 +80,6 @@ export default Vue.extend({
|
||||
|
||||
return 'fixed';
|
||||
},
|
||||
isValueExpression () {
|
||||
if (this.parameter.noDataExpression === true) {
|
||||
return false;
|
||||
}
|
||||
if (typeof this.value === 'string' && this.value.charAt(0) === '=') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasRemoteMethod (): boolean {
|
||||
return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
|
||||
},
|
||||
@@ -82,7 +92,7 @@ export default Vue.extend({
|
||||
},
|
||||
];
|
||||
|
||||
if (this.hasRemoteMethod) {
|
||||
if (this.hasRemoteMethod || (this.parameter.type === 'resourceLocator' && isResourceLocatorValue(this.value) && this.value.mode === 'list')) {
|
||||
return [
|
||||
{
|
||||
label: this.$locale.baseText('parameterInput.refreshList'),
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
<template>
|
||||
<div class="resource-locator">
|
||||
<resource-locator-dropdown
|
||||
:value="value ? value.value: ''"
|
||||
:show="showResourceDropdown"
|
||||
:filterable="isSearchable"
|
||||
:filterRequired="requiresSearchFilter"
|
||||
:resources="currentQueryResults"
|
||||
:loading="currentQueryLoading"
|
||||
:filter="searchFilter"
|
||||
:hasMore="currentQueryHasMore"
|
||||
:errorView="currentQueryError"
|
||||
@input="onListItemSelected"
|
||||
@hide="onDropdownHide"
|
||||
@filter="onSearchFilter"
|
||||
@loadMore="loadResourcesDebounced"
|
||||
ref="dropdown"
|
||||
>
|
||||
<template #error>
|
||||
<div :class="$style.error">
|
||||
<n8n-text color="text-dark" align="center" tag="div">
|
||||
{{ $locale.baseText('resourceLocator.mode.list.error.title') }}
|
||||
</n8n-text>
|
||||
<n8n-text size="small" color="text-base" v-if="hasCredential">
|
||||
{{ $locale.baseText('resourceLocator.mode.list.error.description.part1') }}
|
||||
<a @click="openCredential">{{
|
||||
$locale.baseText('resourceLocator.mode.list.error.description.part2')
|
||||
}}</a>
|
||||
{{ $locale.baseText('resourceLocator.mode.list.error.description.part3') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
:class="{
|
||||
[$style.resourceLocator]: true,
|
||||
[$style.multipleModes]: hasMultipleModes,
|
||||
}"
|
||||
>
|
||||
<div v-if="hasMultipleModes" :class="$style.modeSelector">
|
||||
<n8n-select
|
||||
:value="selectedMode"
|
||||
filterable
|
||||
:size="inputSize"
|
||||
:disabled="isReadOnly"
|
||||
@change="onModeSelected"
|
||||
:placeholder="$locale.baseText('resourceLocator.modeSelector.placeholder')"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="mode in parameter.modes"
|
||||
:key="mode.name"
|
||||
:label="$locale.baseText(getModeLabel(mode.name)) || mode.displayName"
|
||||
:value="mode.name"
|
||||
:disabled="isValueExpression && mode.name === 'list'"
|
||||
:title="
|
||||
isValueExpression && mode.name === 'list'
|
||||
? $locale.baseText('resourceLocator.mode.list.disabled.title')
|
||||
: ''
|
||||
"
|
||||
>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</div>
|
||||
|
||||
<div :class="$style.inputContainer">
|
||||
<draggable-target
|
||||
type="mapping"
|
||||
:disabled="hasOnlyListMode"
|
||||
:sticky="true"
|
||||
:stickyOffset="4"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<template v-slot="{ droppable, activeDrop }">
|
||||
<div
|
||||
:class="{
|
||||
[$style.listModeInputContainer]: isListMode,
|
||||
[$style.droppable]: droppable,
|
||||
[$style.activeDrop]: activeDrop,
|
||||
}"
|
||||
@keydown.stop="onKeyDown"
|
||||
>
|
||||
<n8n-input
|
||||
v-if="isValueExpression || droppable || forceShowExpression"
|
||||
type="text"
|
||||
:size="inputSize"
|
||||
:value="activeDrop || forceShowExpression ? '' : expressionDisplayValue"
|
||||
:title="displayTitle"
|
||||
@keydown.stop
|
||||
ref="input"
|
||||
/>
|
||||
<n8n-input
|
||||
v-else
|
||||
:class="{[$style.selectInput]: isListMode}"
|
||||
:size="inputSize"
|
||||
:value="valueToDisplay"
|
||||
:disabled="isReadOnly"
|
||||
:readonly="isListMode"
|
||||
:title="displayTitle"
|
||||
:placeholder="inputPlaceholder"
|
||||
type="text"
|
||||
ref="input"
|
||||
@input="onInputChange"
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
>
|
||||
<div
|
||||
v-if="isListMode"
|
||||
slot="suffix"
|
||||
>
|
||||
<i
|
||||
:class="{
|
||||
['el-input__icon']: true,
|
||||
['el-icon-arrow-down']: true,
|
||||
[$style.selectIcon]: true,
|
||||
[$style.isReverse]: showResourceDropdown,
|
||||
}"
|
||||
></i>
|
||||
</div>
|
||||
</n8n-input>
|
||||
</div>
|
||||
</template>
|
||||
</draggable-target>
|
||||
<parameter-issues
|
||||
v-if="parameterIssues && parameterIssues.length"
|
||||
:issues="parameterIssues"
|
||||
/>
|
||||
<div v-else-if="urlValue" :class="$style.openResourceLink">
|
||||
<n8n-link
|
||||
theme="text"
|
||||
@click.stop="openResource(urlValue)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="external-link-alt"
|
||||
:title="getLinkAlt(valueToDisplay)"
|
||||
/>
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</resource-locator-dropdown>
|
||||
<parameter-input-hint v-if="infoText" class="mt-4xs" :hint="infoText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import {
|
||||
ILoadOptions,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
INodeParameterResourceLocator,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodePropertyMode,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
hasOnlyListMode,
|
||||
} from './helpers';
|
||||
|
||||
import DraggableTarget from '@/components/DraggableTarget.vue';
|
||||
import ExpressionEdit from '@/components/ExpressionEdit.vue';
|
||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
import ParameterInputHint from '@/components/ParameterInputHint.vue';
|
||||
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { INodeUi, IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface';
|
||||
import { debounceHelper } from '../mixins/debounce';
|
||||
import stringify from 'fast-json-stable-stringify';
|
||||
import { workflowHelpers } from '../mixins/workflowHelpers';
|
||||
import { nodeHelpers } from '../mixins/nodeHelpers';
|
||||
import { getAppNameFromNodeName } from '../helpers';
|
||||
import { type } from 'os';
|
||||
|
||||
interface IResourceLocatorQuery {
|
||||
results: INodeListSearchItems[];
|
||||
nextPageToken: unknown;
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
|
||||
name: 'resource-locator',
|
||||
components: {
|
||||
DraggableTarget,
|
||||
ExpressionEdit,
|
||||
ParameterIssues,
|
||||
ParameterInputHint,
|
||||
ResourceLocatorDropdown,
|
||||
},
|
||||
props: {
|
||||
parameter: {
|
||||
type: Object as PropType<INodeProperties>,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String] as PropType<INodeParameterResourceLocator | NodeParameterValue | undefined>,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputSize: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
validator: (size) => {
|
||||
return ['mini', 'small', 'medium', 'large', 'xlarge'].includes(size);
|
||||
},
|
||||
},
|
||||
parameterIssues: {
|
||||
type: Array as PropType<string[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
displayTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
expressionDisplayValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
forceShowExpression: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isValueExpression: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expressionEditDialogVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
node: {
|
||||
type: Object as PropType<INode>,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
},
|
||||
loadOptionsMethod: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showResourceDropdown: false,
|
||||
searchFilter: '',
|
||||
cachedResponses: {} as { [key: string]: IResourceLocatorQuery },
|
||||
hasCompletedASearch: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
appName(): string {
|
||||
if (!this.node) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const nodeType = this.$store.getters['nodeTypes/getNodeType'](this.node.type);
|
||||
return getAppNameFromNodeName(nodeType.displayName);
|
||||
},
|
||||
selectedMode(): string {
|
||||
if (typeof this.value !== 'object') { // legacy mode
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
return this.parameter.modes? this.parameter.modes[0].name : '';
|
||||
}
|
||||
|
||||
return this.value.mode;
|
||||
},
|
||||
isListMode(): boolean {
|
||||
return this.selectedMode === 'list';
|
||||
},
|
||||
hasCredential(): boolean {
|
||||
const node = this.$store.getters.activeNode as INodeUi | null;
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return !!(node && node.credentials && Object.keys(node.credentials).length === 1);
|
||||
},
|
||||
inputPlaceholder(): string {
|
||||
if (this.currentMode.placeholder) {
|
||||
return this.currentMode.placeholder;
|
||||
}
|
||||
const defaults: { [key: string]: string } = {
|
||||
list: this.$locale.baseText('resourceLocator.mode.list.placeholder'),
|
||||
id: this.$locale.baseText('resourceLocator.id.placeholder'),
|
||||
url: this.$locale.baseText('resourceLocator.url.placeholder'),
|
||||
};
|
||||
|
||||
return defaults[this.selectedMode] || '';
|
||||
},
|
||||
infoText(): string {
|
||||
return this.currentMode.hint ? this.currentMode.hint : '';
|
||||
},
|
||||
currentMode(): INodePropertyMode {
|
||||
return this.findModeByName(this.selectedMode) || ({} as INodePropertyMode);
|
||||
},
|
||||
hasMultipleModes(): boolean {
|
||||
return this.parameter.modes && this.parameter.modes.length > 1 ? true : false;
|
||||
},
|
||||
hasOnlyListMode(): boolean {
|
||||
return hasOnlyListMode(this.parameter);
|
||||
},
|
||||
valueToDisplay(): NodeParameterValue {
|
||||
if (typeof this.value !== 'object') {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
if (this.isListMode) {
|
||||
return this.value? (this.value.cachedResultName || this.value.value) : '';
|
||||
}
|
||||
|
||||
return this.value ? this.value.value : '';
|
||||
},
|
||||
urlValue(): string | null {
|
||||
if (this.isListMode && typeof this.value === 'object') {
|
||||
return (this.value && this.value.cachedResultUrl) || null;
|
||||
}
|
||||
|
||||
if (this.selectedMode === 'url') {
|
||||
if (this.isValueExpression && typeof this.expressionDisplayValue === 'string' && this.expressionDisplayValue.startsWith('http')) {
|
||||
return this.expressionDisplayValue;
|
||||
}
|
||||
|
||||
if (typeof this.valueToDisplay === 'string' && this.valueToDisplay.startsWith('http')) {
|
||||
return this.valueToDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentMode.url) {
|
||||
const value = this.isValueExpression? this.expressionDisplayValue : this.valueToDisplay;
|
||||
if (typeof value === 'string') {
|
||||
const expression = this.currentMode.url.replace(/\{\{\$value\}\}/g, value);
|
||||
const resolved = this.resolveExpression(expression);
|
||||
|
||||
return typeof resolved === 'string' ? resolved : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
currentRequestParams(): {
|
||||
parameters: INodeParameters;
|
||||
credentials: INodeCredentials | undefined;
|
||||
filter: string;
|
||||
} {
|
||||
return {
|
||||
parameters: this.node.parameters,
|
||||
credentials: this.node.credentials,
|
||||
filter: this.searchFilter,
|
||||
};
|
||||
},
|
||||
currentRequestKey(): string {
|
||||
const cacheKeys = {...this.currentRequestParams};
|
||||
cacheKeys.parameters = Object.keys(this.node ? this.node.parameters : {}).reduce((accu: INodeParameters, param) => {
|
||||
if (param !== this.parameter.name && this.node && this.node.parameters) {
|
||||
accu[param] = this.node.parameters[param];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {});
|
||||
return stringify(cacheKeys);
|
||||
},
|
||||
currentResponse(): IResourceLocatorQuery | null {
|
||||
return this.cachedResponses[this.currentRequestKey] || null;
|
||||
},
|
||||
currentQueryResults(): IResourceLocatorResultExpanded[] {
|
||||
const results = this.currentResponse ? this.currentResponse.results : [];
|
||||
|
||||
return results.map((result: INodeListSearchItems): IResourceLocatorResultExpanded => ({
|
||||
...result,
|
||||
...(
|
||||
(result.name && result.url)? { linkAlt: this.getLinkAlt(result.name) } : {}
|
||||
),
|
||||
}));
|
||||
},
|
||||
currentQueryHasMore(): boolean {
|
||||
return !!(this.currentResponse && this.currentResponse.nextPageToken);
|
||||
},
|
||||
currentQueryLoading(): boolean {
|
||||
if (this.requiresSearchFilter && this.searchFilter === '') {
|
||||
return false;
|
||||
}
|
||||
if (!this.currentResponse) {
|
||||
return true;
|
||||
}
|
||||
return !!(this.currentResponse && this.currentResponse.loading);
|
||||
},
|
||||
currentQueryError(): boolean {
|
||||
return !!(this.currentResponse && this.currentResponse.error);
|
||||
},
|
||||
isSearchable(): boolean {
|
||||
return !!this.getPropertyArgument(this.currentMode, 'searchable');
|
||||
},
|
||||
requiresSearchFilter(): boolean {
|
||||
return !!this.getPropertyArgument(this.currentMode, 'searchFilterRequired');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentQueryError(curr: boolean, prev: boolean) {
|
||||
if (this.showResourceDropdown && curr && !prev) {
|
||||
const input = this.$refs.input;
|
||||
if (input) {
|
||||
(input as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
isValueExpression(newValue: boolean) {
|
||||
if (newValue === true) {
|
||||
this.switchFromListMode();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$on('refreshList', this.refreshList);
|
||||
},
|
||||
methods: {
|
||||
getLinkAlt(entity: string) {
|
||||
if (this.selectedMode === 'list' && entity) {
|
||||
return this.$locale.baseText('resourceLocator.openSpecificResource', { interpolate: { entity, appName: this.appName } });
|
||||
}
|
||||
return this.$locale.baseText('resourceLocator.openResource', { interpolate: { appName: this.appName } });
|
||||
},
|
||||
refreshList() {
|
||||
this.cachedResponses = {};
|
||||
this.trackEvent('User refreshed resource locator list');
|
||||
},
|
||||
onKeyDown(e: MouseEvent) {
|
||||
const dropdown = this.$refs.dropdown;
|
||||
if (dropdown && this.showResourceDropdown && !this.isSearchable) {
|
||||
(dropdown as Vue).$emit('keyDown', e);
|
||||
}
|
||||
},
|
||||
openResource(url: string) {
|
||||
window.open(url, '_blank');
|
||||
this.trackEvent('User clicked resource locator link');
|
||||
},
|
||||
getPropertyArgument(
|
||||
parameter: INodePropertyMode,
|
||||
argumentName: string,
|
||||
): string | number | boolean | undefined {
|
||||
if (parameter.typeOptions === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (parameter.typeOptions[argumentName] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return parameter.typeOptions[argumentName];
|
||||
},
|
||||
openCredential(): void {
|
||||
const node = this.$store.getters.activeNode as INodeUi | null;
|
||||
if (!node || !node.credentials) {
|
||||
return;
|
||||
}
|
||||
const credentialKey = Object.keys(node.credentials)[0];
|
||||
if (!credentialKey) {
|
||||
return;
|
||||
}
|
||||
const id = node.credentials[credentialKey].id;
|
||||
this.$store.dispatch('ui/openExistingCredential', { id });
|
||||
},
|
||||
findModeByName(name: string): INodePropertyMode | null {
|
||||
if (this.parameter.modes) {
|
||||
return this.parameter.modes.find((mode: INodePropertyMode) => mode.name === name) || null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getModeLabel(name: string): string | null {
|
||||
if (name === 'id' || name === 'url' || name === 'list') {
|
||||
return this.$locale.baseText(`resourceLocator.mode.${name}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
onInputChange(value: string): void {
|
||||
const params: INodeParameterResourceLocator = { value, mode: this.selectedMode };
|
||||
if (this.isListMode) {
|
||||
const resource = this.currentQueryResults.find((resource) => resource.value === value);
|
||||
if (resource && resource.name) {
|
||||
params.cachedResultName = resource.name;
|
||||
}
|
||||
|
||||
if (resource && resource.url) {
|
||||
params.cachedResultUrl = resource.url;
|
||||
}
|
||||
}
|
||||
this.$emit('input', params);
|
||||
},
|
||||
onModeSelected(value: string): void {
|
||||
if (typeof this.value !== 'object') {
|
||||
this.$emit('input', { value: this.value, mode: value });
|
||||
} else if (value === 'url' && this.value && this.value.cachedResultUrl) {
|
||||
this.$emit('input', { mode: value, value: this.value.cachedResultUrl });
|
||||
} else if (value === 'id' && this.selectedMode === 'list' && this.value && this.value.value) {
|
||||
this.$emit('input', { mode: value, value: this.value.value });
|
||||
} else {
|
||||
this.$emit('input', { mode: value, value: '' });
|
||||
}
|
||||
|
||||
this.trackEvent('User changed resource locator mode', { mode: value });
|
||||
},
|
||||
trackEvent(event: string, params?: {[key: string]: string}): void {
|
||||
this.$telemetry.track(event, {
|
||||
instance_id: this.$store.getters.instanceId,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
node_type: this.node && this.node.type,
|
||||
resource: this.node && this.node.parameters && this.node.parameters.resource,
|
||||
operation: this.node && this.node.parameters && this.node.parameters.operation,
|
||||
field_name: this.parameter.name,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
onDrop(data: string) {
|
||||
this.switchFromListMode();
|
||||
this.$emit('drop', data);
|
||||
},
|
||||
onSearchFilter(filter: string) {
|
||||
this.searchFilter = filter;
|
||||
this.loadResourcesDebounced();
|
||||
},
|
||||
async loadInitialResources(): Promise<void> {
|
||||
if (!this.currentResponse || (this.currentResponse && this.currentResponse.error)) {
|
||||
this.searchFilter = '';
|
||||
this.loadResources();
|
||||
}
|
||||
},
|
||||
loadResourcesDebounced() {
|
||||
this.callDebounced('loadResources', { debounceTime: 1000, trailing: true });
|
||||
},
|
||||
setResponse(paramsKey: string, props: Partial<IResourceLocatorQuery>) {
|
||||
this.cachedResponses = {
|
||||
...this.cachedResponses,
|
||||
[paramsKey]: { ...this.cachedResponses[paramsKey], ...props },
|
||||
};
|
||||
},
|
||||
async loadResources() {
|
||||
const params = this.currentRequestParams;
|
||||
const paramsKey = this.currentRequestKey;
|
||||
const cachedResponse = this.cachedResponses[paramsKey];
|
||||
|
||||
if (this.requiresSearchFilter && !params.filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
let paginationToken: unknown = null;
|
||||
|
||||
try {
|
||||
if (cachedResponse) {
|
||||
const nextPageToken = cachedResponse.nextPageToken;
|
||||
if (nextPageToken) {
|
||||
paginationToken = nextPageToken;
|
||||
this.setResponse(paramsKey, { loading: true });
|
||||
} else if (cachedResponse.error) {
|
||||
this.setResponse(paramsKey, { error: false, loading: true });
|
||||
} else {
|
||||
return; // end of results
|
||||
}
|
||||
} else {
|
||||
this.setResponse(paramsKey, {
|
||||
loading: true,
|
||||
error: false,
|
||||
results: [],
|
||||
nextPageToken: null,
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedNodeParameters = this.resolveParameter(params.parameters) as INodeParameters;
|
||||
const loadOptionsMethod = this.getPropertyArgument(this.currentMode, 'searchListMethod') as
|
||||
| string
|
||||
| undefined;
|
||||
const searchList = this.getPropertyArgument(this.currentMode, 'searchList') as
|
||||
| ILoadOptions
|
||||
| undefined;
|
||||
|
||||
const requestParams: IResourceLocatorReqParams = {
|
||||
nodeTypeAndVersion: {
|
||||
name: this.node.type,
|
||||
version: this.node.typeVersion,
|
||||
},
|
||||
path: this.path,
|
||||
methodName: loadOptionsMethod,
|
||||
searchList,
|
||||
currentNodeParameters: resolvedNodeParameters,
|
||||
credentials: this.node.credentials,
|
||||
...(params.filter ? { filter: params.filter } : {}),
|
||||
...(paginationToken ? { paginationToken } : {}),
|
||||
};
|
||||
|
||||
const response: INodeListSearchResult = await this.$store.dispatch(
|
||||
'nodeTypes/getResourceLocatorResults',
|
||||
requestParams,
|
||||
);
|
||||
|
||||
this.setResponse(paramsKey, {
|
||||
results: (cachedResponse ? cachedResponse.results : []).concat(response.results),
|
||||
nextPageToken: response.paginationToken || null,
|
||||
loading: false,
|
||||
error: false,
|
||||
});
|
||||
|
||||
if (params.filter && !this.hasCompletedASearch) {
|
||||
this.hasCompletedASearch = true;
|
||||
this.trackEvent('User searched resource locator list');
|
||||
}
|
||||
} catch (e) {
|
||||
this.setResponse(paramsKey, {
|
||||
loading: false,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
onInputFocus(): void {
|
||||
if (!this.isListMode || this.showResourceDropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadInitialResources();
|
||||
this.showResourceDropdown = true;
|
||||
},
|
||||
switchFromListMode(): void {
|
||||
if (this.isListMode && this.parameter.modes && this.parameter.modes.length > 1) {
|
||||
let mode = this.findModeByName('id');
|
||||
if (!mode) {
|
||||
mode = this.parameter.modes.filter((mode) => mode.name !== 'list')[0];
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
this.$emit('input', { value: ((this.value && typeof this.value === 'object')? this.value.value: ''), mode: mode.name });
|
||||
}
|
||||
}
|
||||
},
|
||||
onDropdownHide() {
|
||||
if (!this.currentQueryError) {
|
||||
this.showResourceDropdown = false;
|
||||
}
|
||||
},
|
||||
onListItemSelected(value: string) {
|
||||
this.onInputChange(value);
|
||||
this.showResourceDropdown = false;
|
||||
},
|
||||
onInputBlur() {
|
||||
if (!this.isSearchable || this.currentQueryError) {
|
||||
this.showResourceDropdown = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$--mode-selector-width: 92px;
|
||||
|
||||
.modeSelector {
|
||||
--input-background-color: initial;
|
||||
--input-font-color: initial;
|
||||
--input-border-color: initial;
|
||||
flex-basis: $--mode-selector-width;
|
||||
|
||||
input {
|
||||
border-radius: var(--border-radius-base) 0 0 var(--border-radius-base);
|
||||
border-right: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
border-right: var(--border-base);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resourceLocator {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
div:first-child {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.multipleModes {
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-basis: calc(100% - $--mode-selector-width);
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.droppable {
|
||||
--input-border-color: var(--color-secondary-tint-1);
|
||||
--input-background-color: var(--color-secondary-tint-2);
|
||||
--input-border-style: dashed;
|
||||
}
|
||||
|
||||
.activeDrop {
|
||||
--input-border-color: var(--color-success);
|
||||
--input-background-color: var(--color-success-tint-2);
|
||||
--input-border-style: solid;
|
||||
|
||||
textarea,
|
||||
input {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
}
|
||||
|
||||
.selectInput input {
|
||||
padding-right: 30px !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.selectIcon {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s, -webkit-transform 0.3s;
|
||||
-webkit-transform: rotateZ(0);
|
||||
transform: rotateZ(0);
|
||||
|
||||
&.isReverse {
|
||||
-webkit-transform: rotateZ(180deg);
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.listModeInputContainer * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error {
|
||||
max-width: 170px;
|
||||
word-break: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.openResourceLink {
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<n8n-popover
|
||||
placement="bottom"
|
||||
width="318"
|
||||
:popper-class="$style.popover"
|
||||
:value="show"
|
||||
trigger="manual"
|
||||
>
|
||||
<div :class="$style.messageContainer" v-if="errorView">
|
||||
<slot name="error"></slot>
|
||||
</div>
|
||||
<div :class="$style.searchInput" v-if="filterable && !errorView" @keydown="onKeyDown">
|
||||
<n8n-input size="medium" :value="filter" :clearable="true" @input="onFilterInput" @blur="onSearchBlur" ref="search" :placeholder="$locale.baseText('resourceLocator.search.placeholder')">
|
||||
<font-awesome-icon :class="$style.searchIcon" icon="search" slot="prefix" />
|
||||
</n8n-input>
|
||||
</div>
|
||||
<div v-if="filterRequired && !filter && !errorView && !loading" :class="$style.searchRequired">
|
||||
{{ $locale.baseText('resourceLocator.mode.list.searchRequired') }}
|
||||
</div>
|
||||
<div :class="$style.messageContainer" v-else-if="!errorView && sortedResources.length === 0 && !loading">
|
||||
{{ $locale.baseText('resourceLocator.mode.list.noResults') }}
|
||||
</div>
|
||||
<div v-else-if="!errorView" ref="resultsContainer" :class="{[$style.container]: true, [$style.pushDownResults]: filterable}" @scroll="onResultsEnd">
|
||||
<div
|
||||
v-for="(result, i) in sortedResources"
|
||||
:key="result.value"
|
||||
:class="{ [$style.resourceItem]: true, [$style.selected]: result.value === value, [$style.hovering]: hoverIndex === i }"
|
||||
@click="() => onItemClick(result.value)"
|
||||
@mouseenter="() => onItemHover(i)"
|
||||
@mouseleave="() => onItemHoverLeave()"
|
||||
:ref="`item-${i}`"
|
||||
>
|
||||
<div :class="$style.resourceNameContainer">
|
||||
<span>{{ result.name }}</span>
|
||||
</div>
|
||||
<div :class="$style.urlLink">
|
||||
<font-awesome-icon
|
||||
v-if="showHoverUrl && result.url && hoverIndex === i"
|
||||
icon="external-link-alt"
|
||||
:title="result.linkAlt || $locale.baseText('resourceLocator.mode.list.openUrl')"
|
||||
@click="openUrl($event, result.url)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading && !errorView">
|
||||
<div v-for="(_, i) in 3" :key="i" :class="$style.loadingItem">
|
||||
<n8n-loading :class="$style.loader" variant="p" :rows="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot slot="reference" />
|
||||
</n8n-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IResourceLocatorResultExpanded } from '@/Interface';
|
||||
import Vue, { PropType } from 'vue';
|
||||
|
||||
const SEARCH_BAR_HEIGHT_PX = 40;
|
||||
const SCROLL_MARGIN_PX = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'resource-locator-dropdown',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
resources: {
|
||||
type: Array as PropType<IResourceLocatorResultExpanded[]>,
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
},
|
||||
hasMore: {
|
||||
type: Boolean,
|
||||
},
|
||||
errorView: {
|
||||
type: Boolean,
|
||||
},
|
||||
filterRequired: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hoverIndex: 0,
|
||||
showHoverUrl: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$on('keyDown', this.onKeyDown);
|
||||
},
|
||||
computed: {
|
||||
sortedResources(): IResourceLocatorResultExpanded[] {
|
||||
const seen = new Set();
|
||||
const { selected, notSelected } = this.resources.reduce((acc, item: IResourceLocatorResultExpanded) => {
|
||||
if (seen.has(item.value)) {
|
||||
return acc;
|
||||
}
|
||||
seen.add(item.value);
|
||||
|
||||
if (this.value && item.value === this.value) {
|
||||
acc.selected = item;
|
||||
} else {
|
||||
acc.notSelected.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { selected: null as IResourceLocatorResultExpanded | null, notSelected: [] as IResourceLocatorResultExpanded[] });
|
||||
|
||||
if (selected) {
|
||||
return [
|
||||
selected,
|
||||
...notSelected,
|
||||
];
|
||||
}
|
||||
|
||||
return notSelected;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openUrl(event: MouseEvent ,url: string) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
const container = this.$refs.resultsContainer as HTMLElement;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (this.hoverIndex < this.sortedResources.length - 1) {
|
||||
this.hoverIndex++;
|
||||
|
||||
const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[];
|
||||
if (container && Array.isArray(items) && items.length === 1) {
|
||||
const item = items[0];
|
||||
if ((item.offsetTop + item.clientHeight) > (container.scrollTop + container.offsetHeight)) {
|
||||
const top = item.offsetTop - container.offsetHeight + item.clientHeight;
|
||||
container.scrollTo({ top });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
if (this.hoverIndex > 0) {
|
||||
this.hoverIndex--;
|
||||
|
||||
const searchOffset = this.filterable ? SEARCH_BAR_HEIGHT_PX : 0;
|
||||
const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[];
|
||||
if (container && Array.isArray(items) && items.length === 1) {
|
||||
const item = items[0];
|
||||
if (item.offsetTop <= container.scrollTop + searchOffset) {
|
||||
container.scrollTo({ top: item.offsetTop - searchOffset });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.key === 'Enter') {
|
||||
this.$emit('input', this.sortedResources[this.hoverIndex].value);
|
||||
}
|
||||
|
||||
},
|
||||
onFilterInput(value: string) {
|
||||
this.$emit('filter', value);
|
||||
},
|
||||
onSearchBlur() {
|
||||
this.$emit('hide');
|
||||
},
|
||||
onItemClick(selected: string) {
|
||||
this.$emit('input', selected);
|
||||
},
|
||||
onItemHover(index: number) {
|
||||
this.hoverIndex = index;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.hoverIndex === index) {
|
||||
this.showHoverUrl = true;
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
onItemHoverLeave() {
|
||||
this.showHoverUrl = false;
|
||||
},
|
||||
onResultsEnd() {
|
||||
if (this.loading || !this.hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.$refs.resultsContainer as HTMLElement;
|
||||
if (container) {
|
||||
const diff = container.offsetHeight - (container.scrollHeight - container.scrollTop);
|
||||
if (diff > -(SCROLL_MARGIN_PX) && diff < SCROLL_MARGIN_PX) {
|
||||
this.$emit('loadMore');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show(toShow) {
|
||||
if (toShow) {
|
||||
this.hoverIndex = 0;
|
||||
this.showHoverUrl = false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (toShow && this.filterable && this.$refs.search) {
|
||||
(this.$refs.search as HTMLElement).focus();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
loading() {
|
||||
setTimeout(this.onResultsEnd, 0); // in case of filtering
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.popover {
|
||||
padding: 0;
|
||||
border: var(--border-base);
|
||||
}
|
||||
|
||||
.pushDownResults {
|
||||
padding-top: 36px;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
max-height: 236px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
height: 236px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
border-bottom: var(--border-base);
|
||||
--input-border-color: none;
|
||||
--input-font-size: var(--font-size-2xs);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 316px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.resourceItem {
|
||||
display: flex;
|
||||
padding: 0 var(--spacing-xs);
|
||||
white-space: nowrap;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
}
|
||||
|
||||
.loadingItem {
|
||||
padding: 10px var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loader {
|
||||
max-width: 120px;
|
||||
|
||||
* {
|
||||
margin-top: 0 !important;
|
||||
max-height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.hovering {
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
|
||||
.searchRequired {
|
||||
height: 50px;
|
||||
margin-top: 40px;
|
||||
padding-left: var(--spacing-xs);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.urlLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-3xs);
|
||||
color: var(--color-text-base);
|
||||
margin-left: var(--spacing-2xs);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.resourceNameContainer {
|
||||
font-size: var(--font-size-2xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const hasOnlyListMode = (parameter: INodeProperties) : boolean => {
|
||||
return parameter.modes !== undefined && parameter.modes.length === 1 && parameter.modes[0].name === 'list';
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER } from '@/constants';
|
||||
import { INodeUi, ITemplatesNode } from '@/Interface';
|
||||
import { isResourceLocatorValue } from '@/typeGuards';
|
||||
import dateformat from 'dateformat';
|
||||
import {IDataObject, INodeTypeDescription} from 'n8n-workflow';
|
||||
import {IDataObject, INodeProperties, INodeTypeDescription, NodeParameterValueType} from 'n8n-workflow';
|
||||
|
||||
const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
|
||||
const CRED_KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
|
||||
const NODE_KEYWORDS_TO_FILTER = ['Trigger'];
|
||||
const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
|
||||
|
||||
const COMMUNITY_PACKAGE_NAME_REGEX = /(@\w+\/)?n8n-nodes-(?!base\b)\b\w+/g;
|
||||
@@ -29,7 +31,11 @@ export function convertToHumanReadableDate (epochTime: number) {
|
||||
}
|
||||
|
||||
export function getAppNameFromCredType(name: string) {
|
||||
return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
||||
return name.split(' ').filter((word) => !CRED_KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
||||
}
|
||||
|
||||
export function getAppNameFromNodeName(name: string) {
|
||||
return name.split(' ').filter((word) => !NODE_KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
||||
}
|
||||
|
||||
export function getStyleTokenValue(name: string): string {
|
||||
@@ -99,3 +105,16 @@ export function shorten(s: string, limit: number, keep: number) {
|
||||
export function hasExpressionMapping(value: unknown) {
|
||||
return typeof value === 'string' && !!MAPPING_PARAMS.find((param) => value.includes(param));
|
||||
}
|
||||
|
||||
export function isValueExpression (parameter: INodeProperties, paramValue: NodeParameterValueType): boolean {
|
||||
if (parameter.noDataExpression === true) {
|
||||
return false;
|
||||
}
|
||||
if (typeof paramValue === 'string' && paramValue.charAt(0) === '=') {
|
||||
return true;
|
||||
}
|
||||
if (isResourceLocatorValue(paramValue) && paramValue.value && paramValue.value.toString().charAt(0) === '=') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
24
packages/editor-ui/src/components/mixins/debounce.ts
Normal file
24
packages/editor-ui/src/components/mixins/debounce.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { debounce } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
|
||||
export const debounceHelper = Vue.extend({
|
||||
data () {
|
||||
return {
|
||||
debouncedFunctions: [] as any[], // tslint:disable-line:no-any
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async callDebounced (...inputParameters: any[]): Promise<void> { // tslint:disable-line:no-any
|
||||
const functionName = inputParameters.shift() as string;
|
||||
const { trailing, debounceTime } = inputParameters.shift();
|
||||
|
||||
// @ts-ignore
|
||||
if (this.debouncedFunctions[functionName] === undefined) {
|
||||
// @ts-ignore
|
||||
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing } : { leading: true } );
|
||||
}
|
||||
// @ts-ignore
|
||||
await this.debouncedFunctions[functionName].apply(this, inputParameters);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
@@ -8,7 +7,6 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||
data () {
|
||||
return {
|
||||
loadingService: null as any | null, // tslint:disable-line:no-any
|
||||
debouncedFunctions: [] as any[], // tslint:disable-line:no-any
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -71,18 +69,5 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||
this.loadingService = null;
|
||||
}
|
||||
},
|
||||
|
||||
async callDebounced (...inputParameters: any[]): Promise<void> { // tslint:disable-line:no-any
|
||||
const functionName = inputParameters.shift() as string;
|
||||
const { trailing, debounceTime } = inputParameters.shift();
|
||||
|
||||
// @ts-ignore
|
||||
if (this.debouncedFunctions[functionName] === undefined) {
|
||||
// @ts-ignore
|
||||
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing } : { leading: true } );
|
||||
}
|
||||
// @ts-ignore
|
||||
await this.debouncedFunctions[functionName].apply(this, inputParameters);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user