fix(editor): Tweak hover area of workflow / cred cards (#7108)

Context

When a user is attempting to interact with a foreground action inside an
entity card (workflow, credential, community node, logging destination),
they might accidentally open that entity instead of interacting with a
foreground action.

For these card components, actions are always placed on right side. 

A/C

Area around right "column" of entity cards (workflow, cred, community
node, logging destination) should not be a hoverable area (that opens
that entity when clicked). This area is roughly highlighted in screen
shot below in orange.


![image](https://github.com/n8n-io/n8n/assets/5410822/0916bcd5-e972-4367-a862-41d2086a2334)
This commit is contained in:
Csaba Tuncsik 2023-09-13 12:21:26 +02:00 committed by GitHub
parent 67092c0a1b
commit 217de21605
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 82 additions and 47 deletions

View file

@ -14,7 +14,7 @@
<slot name="footer" /> <slot name="footer" />
</div> </div>
</div> </div>
<div v-if="$slots.append"> <div v-if="$slots.append" :class="$style.append">
<slot name="append" /> <slot name="append" />
</div> </div>
</div> </div>
@ -82,7 +82,6 @@ export default defineComponent({
.icon { .icon {
width: 24px; width: 24px;
height: 24px;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -101,4 +100,10 @@ export default defineComponent({
border-color: var(--color-primary); border-color: var(--color-primary);
} }
} }
.append {
display: flex;
align-items: center;
cursor: default;
}
</style> </style>

View file

@ -1,24 +1,26 @@
<template> <template>
<n8n-card :class="$style['card-link']" @click="onClick"> <n8n-card :class="$style.cardLink" @click="onClick">
<template #prepend> <template #prepend>
<credential-icon :credential-type-name="credentialType ? credentialType.name : ''" /> <credential-icon :credential-type-name="credentialType ? credentialType.name : ''" />
</template> </template>
<template #header> <template #header>
<n8n-heading tag="h2" bold :class="$style['card-heading']"> <n8n-heading tag="h2" bold :class="$style.cardHeading">
{{ data.name }} {{ data.name }}
</n8n-heading> </n8n-heading>
</template> </template>
<n8n-text color="text-light" size="small"> <div :class="$style.cardDescription">
<span v-if="credentialType">{{ credentialType.displayName }} | </span> <n8n-text color="text-light" size="small">
<span v-show="data" <span v-if="credentialType">{{ credentialType.displayName }} | </span>
>{{ $locale.baseText('credentials.item.updated') }} <time-ago :date="data.updatedAt" /> | <span v-show="data"
</span> >{{ $locale.baseText('credentials.item.updated') }} <time-ago :date="data.updatedAt" /> |
<span v-show="data" </span>
>{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }} <span v-show="data"
</span> >{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
</n8n-text> </span>
</n8n-text>
</div>
<template #append> <template #append>
<div :class="$style['card-actions']"> <div :class="$style.cardActions" ref="cardActions">
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]"> <enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
<n8n-badge v-if="credentialPermissions.isOwner" class="mr-xs" theme="tertiary" bold> <n8n-badge v-if="credentialPermissions.isOwner" class="mr-xs" theme="tertiary" bold>
{{ $locale.baseText('credentials.item.owner') }} {{ $locale.baseText('credentials.item.owner') }}
@ -128,7 +130,14 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
async onClick() { async onClick(event: Event) {
if (
this.$refs.cardActions === event.target ||
this.$refs.cardActions?.contains(event.target)
) {
return;
}
this.uiStore.openExistingCredential(this.data.id); this.uiStore.openExistingCredential(this.data.id);
}, },
async onAction(action: string) { async onAction(action: string) {
@ -162,23 +171,36 @@ export default defineComponent({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.card-link { .cardLink {
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
cursor: pointer; cursor: pointer;
padding: 0 0 0 var(--spacing-s);
align-items: stretch;
&:hover { &:hover {
box-shadow: 0 2px 8px rgba(#441c17, 0.1); box-shadow: 0 2px 8px rgba(#441c17, 0.1);
} }
} }
.card-heading { .cardHeading {
font-size: var(--font-size-s); font-size: var(--font-size-s);
padding: var(--spacing-s) 0 0;
} }
.card-actions { .cardDescription {
min-height: 19px;
display: flex;
align-items: center;
padding: 0 0 var(--spacing-s);
}
.cardActions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
align-self: stretch;
padding: 0 var(--spacing-s) 0 0;
cursor: default;
} }
</style> </style>

View file

@ -13,7 +13,7 @@
</div> </div>
</template> </template>
<template #append> <template #append>
<div :class="$style.cardActions"> <div :class="$style.cardActions" ref="cardActions">
<div :class="$style.activeStatusText" data-test-id="destination-activator-status"> <div :class="$style.activeStatusText" data-test-id="destination-activator-status">
<n8n-text v-if="nodeParameters.enabled" :color="'success'" size="small" bold> <n8n-text v-if="nodeParameters.enabled" :color="'success'" size="small" bold>
{{ $locale.baseText('workflowActivator.active') }} {{ $locale.baseText('workflowActivator.active') }}
@ -83,9 +83,7 @@ export default defineComponent({
destination: { destination: {
type: Object, type: Object,
required: true, required: true,
default: deepCopy( default: deepCopy(defaultMessageEventBusDestinationOptions),
defaultMessageEventBusDestinationOptions,
) as MessageEventBusDestinationOptions,
}, },
isInstanceOwner: Boolean, isInstanceOwner: Boolean,
}, },
@ -130,17 +128,16 @@ export default defineComponent({
); );
} }
}, },
async onClick(event?: PointerEvent) { async onClick(event: Event) {
if ( if (
event && this.$refs.cardActions === event.target ||
event.target && this.$refs.cardActions?.contains(event.target) ||
'className' in event.target && event.target?.contains(this.$refs.cardActions)
event.target['className'] === 'el-switch__core'
) { ) {
event.stopPropagation(); return;
} else {
this.$emit('edit', this.destination.id);
} }
this.$emit('edit', this.destination.id);
}, },
onEnabledSwitched(state: boolean, destinationId: string) { onEnabledSwitched(state: boolean, destinationId: string) {
this.nodeParameters.enabled = state; this.nodeParameters.enabled = state;
@ -184,6 +181,8 @@ export default defineComponent({
.cardLink { .cardLink {
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
cursor: pointer; cursor: pointer;
padding: 0 0 0 var(--spacing-s);
align-items: stretch;
&:hover { &:hover {
box-shadow: 0 2px 8px rgba(#441c17, 0.1); box-shadow: 0 2px 8px rgba(#441c17, 0.1);
@ -201,12 +200,14 @@ export default defineComponent({
.cardHeading { .cardHeading {
font-size: var(--font-size-s); font-size: var(--font-size-s);
word-break: break-word; word-break: break-word;
padding: var(--spacing-s) 0 0 var(--spacing-s);
} }
.cardDescription { .cardDescription {
min-height: 19px; min-height: 19px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 0 var(--spacing-s) var(--spacing-s);
} }
.cardActions { .cardActions {
@ -214,5 +215,7 @@ export default defineComponent({
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 0 var(--spacing-s) 0 0;
cursor: default;
} }
</style> </style>

View file

@ -29,7 +29,7 @@
</n8n-text> </n8n-text>
</div> </div>
<template #append> <template #append>
<div :class="$style.cardActions"> <div :class="$style.cardActions" ref="cardActions">
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]"> <enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
<n8n-badge v-if="workflowPermissions.isOwner" class="mr-xs" theme="tertiary" bold> <n8n-badge v-if="workflowPermissions.isOwner" class="mr-xs" theme="tertiary" bold>
{{ $locale.baseText('workflows.item.owner') }} {{ $locale.baseText('workflows.item.owner') }}
@ -40,7 +40,6 @@
class="mr-s" class="mr-s"
:workflow-active="data.active" :workflow-active="data.active"
:workflow-id="data.id" :workflow-id="data.id"
ref="activator"
data-test-id="workflow-card-activator" data-test-id="workflow-card-activator"
/> />
@ -78,8 +77,6 @@ import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import TimeAgo from '@/components/TimeAgo.vue'; import TimeAgo from '@/components/TimeAgo.vue';
type ActivatorRef = InstanceType<typeof WorkflowActivator>;
export const WORKFLOW_LIST_ITEM_ACTIONS = { export const WORKFLOW_LIST_ITEM_ACTIONS = {
OPEN: 'open', OPEN: 'open',
SHARE: 'share', SHARE: 'share',
@ -171,21 +168,22 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
async onClick(event?: PointerEvent) { async onClick(event: Event) {
if (event) { if (
if ((this.$refs.activator as ActivatorRef)?.$el.contains(event.target as HTMLElement)) { this.$refs.cardActions === event.target ||
return; this.$refs.cardActions?.contains(event.target)
} ) {
return;
}
if (event.metaKey || event.ctrlKey) { if (event.metaKey || event.ctrlKey) {
const route = this.$router.resolve({ const route = this.$router.resolve({
name: VIEWS.WORKFLOW, name: VIEWS.WORKFLOW,
params: { name: this.data.id }, params: { name: this.data.id },
}); });
window.open(route.href, '_blank'); window.open(route.href, '_blank');
return; return;
}
} }
await this.$router.push({ await this.$router.push({
@ -267,6 +265,8 @@ export default defineComponent({
.cardLink { .cardLink {
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
cursor: pointer; cursor: pointer;
padding: 0;
align-items: stretch;
&:hover { &:hover {
box-shadow: 0 2px 8px rgba(#441c17, 0.1); box-shadow: 0 2px 8px rgba(#441c17, 0.1);
@ -276,12 +276,14 @@ export default defineComponent({
.cardHeading { .cardHeading {
font-size: var(--font-size-s); font-size: var(--font-size-s);
word-break: break-word; word-break: break-word;
padding: var(--spacing-s) 0 0 var(--spacing-s);
} }
.cardDescription { .cardDescription {
min-height: 19px; min-height: 19px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 0 var(--spacing-s) var(--spacing-s);
} }
.cardActions { .cardActions {
@ -289,5 +291,8 @@ export default defineComponent({
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
align-self: stretch;
padding: 0 var(--spacing-s) 0 0;
cursor: default;
} }
</style> </style>