n8n/packages/editor-ui/src/components/WorkflowCard.vue
Omar Ajoue 25e9f0817a
refactor: Workflow sharing bug bash fixes (#4888)
* fix: Prevent workflows with only manual trigger from being activated

* fix: Fix workflow id when sharing from workflows list

* fix: Update sharing modal translations

* fix: Allow sharees to disable workflows and fix issue with unique key when removing a user

* refactor: Improve error messages and change logging level to be less verbose

* fix: Broken user removal transfer issue

* feat: Implement workflow sharing BE telemetry

* chore: temporarily add sharing env vars

* feat: Implement BE telemetry for workflow sharing

* fix: Prevent issues with possibly missing workflow id

* feat: Replace WorkflowSharing flag references (no-changelog) (#4918)

* ci: Block all external network calls in tests (no-changelog) (#4930)

* setup nock to prevent tests from making any external requests

* mock all calls to posthog sdk

* feat: Replace WorkflowSharing flag references (no-changelog)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>

* refactor: Remove temporary feature flag for workflow sharing

* refactor: add sharing_role to both manual and node executions

* refactor: Allow changing name, position and disabled of read only nodes

* feat: Overhaul dynamic translations for local and cloud (#4943)

* feat: Overhaul dynamic translations for local and cloud

* fix: remove type casting

* chore: remove unused translations

* fix: fix workflow sharing translation

* test: Fix broken test

* refactor: remove unnecessary import

* refactor: Minor code improvements

* refactor: rename dynamicTranslations to contextBasedTranslationKeys

* fix: fix type imports

* refactor: Consolidate sharing feature check

* feat: update cred sharing unavailable translations

* feat: update upgrade message when user management not available

* fix: rename plan names to Pro and Power

* feat: update translations to no longer contain plan names

* wip: subworkflow permissions

* feat: add workflowsFromSameOwner caller policy

* feat: Fix subworkflow permissions

* shared entites should check for role when deleting users

* refactor: remove circular dependency

* role filter shouldn't be an array

* fixed role issue

* fix: Corrected behavior when removing users

* feat: show instance owner credential sharing message only if isnt sharee

* feat: update workflow caller policy caller ids labels

* feat: update upgrade plan links to contain instance ids

* fix: show check errors below creds message only to owner

* fix(editor): Hide usage page on cloud

* fix: update credential validation error message for sharee

* fix(core): Remove duplicate import

* fix(editor): Extending deployment types

* feat: Overhaul contextual translations (#4992)

feat: update how contextual translations work

* refactor: improve messageing for subworkflow permissions

* test: Fix issue with user deletion and transfer

* fix: Explicitly throw error message so it can be displayed in UI

Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
Co-authored-by: freyamade <freya@n8n.io>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
2022-12-21 16:42:07 +01:00

280 lines
6.9 KiB
Vue

<template>
<n8n-card :class="$style.cardLink" @click="onClick">
<template #header>
<n8n-heading
tag="h2"
bold
class="ph-no-capture"
:class="$style.cardHeading"
data-test-id="workflow-card-name"
>
{{ data.name }}
</n8n-heading>
</template>
<div :class="$style.cardDescription">
<n8n-text color="text-light" size="small">
<span v-show="data"
>{{ $locale.baseText('workflows.item.updated') }} <time-ago :date="data.updatedAt" /> |
</span>
<span v-show="data" class="mr-2xs"
>{{ $locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
</span>
<span
v-if="settingsStore.areTagsEnabled && data.tags && data.tags.length > 0"
v-show="data"
>
<n8n-tags
:tags="data.tags"
:truncateAt="3"
truncate
@click="onClickTag"
data-test-id="workflow-card-tags"
/>
</span>
</n8n-text>
</div>
<template #append>
<div :class="$style.cardActions">
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
<n8n-badge v-if="workflowPermissions.isOwner" class="mr-xs" theme="tertiary" bold>
{{ $locale.baseText('workflows.item.owner') }}
</n8n-badge>
</enterprise-edition>
<workflow-activator
class="mr-s"
:workflow-active="data.active"
:workflow-id="data.id"
ref="activator"
data-test-id="workflow-card-activator"
/>
<n8n-action-toggle
:actions="actions"
theme="dark"
@action="onAction"
data-test-id="workflow-card-actions"
/>
</div>
</template>
</n8n-card>
</template>
<script lang="ts">
import mixins from 'vue-typed-mixins';
import { IWorkflowDb, IUser, ITag } from '@/Interface';
import {
DUPLICATE_MODAL_KEY,
EnterpriseEditionFeature,
VIEWS,
WORKFLOW_SHARE_MODAL_KEY,
} from '@/constants';
import { showMessage } from '@/mixins/showMessage';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import dateformat from 'dateformat';
import { restApi } from '@/mixins/restApi';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import Vue from 'vue';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
import { useWorkflowsStore } from '@/stores/workflows';
export const WORKFLOW_LIST_ITEM_ACTIONS = {
OPEN: 'open',
SHARE: 'share',
DUPLICATE: 'duplicate',
DELETE: 'delete',
};
export default mixins(showMessage, restApi).extend({
data() {
return {
EnterpriseEditionFeature,
};
},
components: {
WorkflowActivator,
},
props: {
data: {
type: Object,
required: true,
default: (): IWorkflowDb => ({
id: '',
createdAt: '',
updatedAt: '',
active: false,
connections: {},
nodes: [],
name: '',
sharedWith: [],
ownedBy: {} as IUser,
versionId: '',
}),
},
readonly: {
type: Boolean,
default: false,
},
},
computed: {
...mapStores(useSettingsStore, useUIStore, useUsersStore, useWorkflowsStore),
currentUser(): IUser {
return this.usersStore.currentUser || ({} as IUser);
},
workflowPermissions(): IPermissions {
return getWorkflowPermissions(this.currentUser, this.data);
},
actions(): Array<{ label: string; value: string }> {
return [
{
label: this.$locale.baseText('workflows.item.open'),
value: WORKFLOW_LIST_ITEM_ACTIONS.OPEN,
},
{
label: this.$locale.baseText('workflows.item.share'),
value: WORKFLOW_LIST_ITEM_ACTIONS.SHARE,
},
{
label: this.$locale.baseText('workflows.item.duplicate'),
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
},
].concat(
this.workflowPermissions.delete
? [
{
label: this.$locale.baseText('workflows.item.delete'),
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
},
]
: [],
);
},
formattedCreatedAtDate(): string {
const currentYear = new Date().getFullYear();
return dateformat(
this.data.createdAt,
`d mmmm${this.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`,
);
},
},
methods: {
async onClick(event?: PointerEvent) {
if (event) {
if ((this.$refs.activator as Vue)?.$el.contains(event.target as HTMLElement)) {
return;
}
if (event.metaKey || event.ctrlKey) {
const route = this.$router.resolve({
name: VIEWS.WORKFLOW,
params: { name: this.data.id },
});
window.open(route.href, '_blank');
return;
}
}
this.$router.push({
name: VIEWS.WORKFLOW,
params: { name: this.data.id },
});
},
onClickTag(tagId: string, event: PointerEvent) {
event.stopPropagation();
this.$emit('click:tag', tagId, event);
},
async onAction(action: string) {
if (action === WORKFLOW_LIST_ITEM_ACTIONS.OPEN) {
await this.onClick();
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE) {
this.uiStore.openModalWithData({
name: DUPLICATE_MODAL_KEY,
data: {
id: this.data.id,
name: this.data.name,
tags: (this.data.tags || []).map((tag: ITag) => tag.id),
},
});
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.SHARE) {
this.uiStore.openModalWithData({
name: WORKFLOW_SHARE_MODAL_KEY,
data: { id: this.data.id },
});
this.$telemetry.track('User opened sharing modal', {
workflow_id: this.data.id,
user_id_sharer: this.currentUser.id,
sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor',
});
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) {
const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
interpolate: { workflowName: this.data.name },
}),
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
'warning',
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'),
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.cancelButtonText'),
);
if (deleteConfirmed === false) {
return;
}
try {
await this.restApi().deleteWorkflow(this.data.id);
this.workflowsStore.deleteWorkflow(this.data.id);
} catch (error) {
this.$showError(
error,
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
);
return;
}
// Reset tab title since workflow is deleted.
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
type: 'success',
});
}
},
},
});
</script>
<style lang="scss" module>
.cardLink {
transition: box-shadow 0.3s ease;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(#441c17, 0.1);
}
}
.cardHeading {
font-size: var(--font-size-s);
word-break: break-word;
}
.cardDescription {
min-height: 19px;
display: flex;
align-items: center;
}
.cardActions {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>