Files
n8n-enterprise-unlocked/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
कारतोफ्फेलस्क्रिप्ट™ 7f8857f69b refactor(editor): Upgrade frontend typing (no-changelog) (#9915)
2024-07-03 14:19:24 +02:00

416 lines
11 KiB
Vue

<template>
<Modal
width="460px"
max-height="75%"
:title="modalTitle"
:event-bus="modalBus"
:name="WORKFLOW_SHARE_MODAL_KEY"
:center="true"
:before-close="onCloseModal"
>
<template #content>
<div v-if="!isSharingEnabled" :class="$style.container">
<n8n-text>
{{
$locale.baseText(
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.description.modal,
)
}}
</n8n-text>
</div>
<div v-else :class="$style.container">
<n8n-info-tip
v-if="!workflowPermissions.share && !isHomeTeamProject"
:bold="false"
class="mb-s"
>
{{
$locale.baseText('workflows.shareModal.info.sharee', {
interpolate: { workflowOwnerName },
})
}}
</n8n-info-tip>
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]" :class="$style.content">
<div>
<ProjectSharing
v-model="sharedWithProjects"
:home-project="workflow.homeProject"
:projects="projects"
:roles="workflowRoles"
:readonly="!workflowPermissions.share"
:static="isHomeTeamProject || !workflowPermissions.share"
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
@project-added="onProjectAdded"
@project-removed="onProjectRemoved"
/>
<n8n-info-tip v-if="isHomeTeamProject" :bold="false" class="mt-s">
<i18n-t keypath="workflows.shareModal.info.members" tag="span">
<template #projectName>
{{ workflow.homeProject?.name }}
</template>
<template #members>
<strong>
{{
$locale.baseText('workflows.shareModal.info.members.number', {
interpolate: {
number: String(numberOfMembersInHomeTeamProject),
},
adjustToNumber: numberOfMembersInHomeTeamProject,
})
}}
</strong>
</template>
</i18n-t>
</n8n-info-tip>
</div>
<template #fallback>
<n8n-text>
<i18n-t
:keypath="
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.description
.tooltip
"
tag="span"
>
<template #action />
</i18n-t>
</n8n-text>
</template>
</enterprise-edition>
</div>
</template>
<template #footer>
<div v-if="!isSharingEnabled" :class="$style.actionButtons">
<n8n-button @click="goToUpgrade">
{{
$locale.baseText(
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.button,
)
}}
</n8n-button>
</div>
<enterprise-edition
v-else
:features="[EnterpriseEditionFeature.Sharing]"
:class="$style.actionButtons"
>
<n8n-text v-show="isDirty" color="text-light" size="small" class="mr-xs">
{{ $locale.baseText('workflows.shareModal.changesHint') }}
</n8n-text>
<n8n-button v-if="isHomeTeamProject" type="secondary" @click="modalBus.emit('close')">
{{ $locale.baseText('generic.close') }}
</n8n-button>
<n8n-button
v-else
v-show="workflowPermissions.share"
:loading="loading"
:disabled="!isDirty"
data-test-id="workflow-sharing-modal-save-button"
@click="onSave"
>
{{ $locale.baseText('workflows.shareModal.save') }}
</n8n-button>
</enterprise-edition>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import {
EnterpriseEditionFeature,
MODAL_CONFIRM,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
VIEWS,
WORKFLOW_SHARE_MODAL_KEY,
} from '@/constants';
import type { IUser, IWorkflowDb } from '@/Interface';
import type { PermissionsMap } from '@/permissions';
import type { WorkflowScope } from '@n8n/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { nodeViewEventBus } from '@/event-bus';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import type { ITelemetryTrackProperties } from 'n8n-workflow';
import type { BaseTextKey } from '@/plugins/i18n';
import { isNavigationFailure } from 'vue-router';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectListItem, ProjectSharingData, Project } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
export default defineComponent({
name: 'WorkflowShareModal',
components: {
Modal,
ProjectSharing,
},
props: {
data: {
type: Object,
default: () => ({}),
},
},
setup() {
return {
...useToast(),
...useMessage(),
};
},
data() {
const workflowsStore = useWorkflowsStore();
const workflow =
this.data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID
? workflowsStore.workflow
: workflowsStore.workflowsById[this.data.id];
return {
WORKFLOW_SHARE_MODAL_KEY,
loading: true,
isDirty: false,
modalBus: createEventBus(),
sharedWithProjects: [...(workflow.sharedWithProjects || [])] as ProjectSharingData[],
EnterpriseEditionFeature,
teamProject: null as Project | null,
};
},
computed: {
...mapStores(
useSettingsStore,
useUIStore,
useUsersStore,
useWorkflowsStore,
useWorkflowsEEStore,
useProjectsStore,
useRolesStore,
),
isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
},
modalTitle(): string {
if (this.isHomeTeamProject) {
return this.$locale.baseText('workflows.shareModal.title.static', {
interpolate: { projectName: this.workflow.homeProject?.name ?? '' },
});
}
return this.$locale.baseText(
this.isSharingEnabled
? (this.uiStore.contextBasedTranslationKeys.workflows.sharing.title as BaseTextKey)
: (this.uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable
.title as BaseTextKey),
{
interpolate: { name: this.workflow.name },
},
);
},
workflow(): IWorkflowDb {
return this.data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID
? this.workflowsStore.workflow
: this.workflowsStore.workflowsById[this.data.id];
},
currentUser(): IUser | null {
return this.usersStore.currentUser;
},
workflowPermissions(): PermissionsMap<WorkflowScope> {
return getWorkflowPermissions(this.workflow);
},
workflowOwnerName(): string {
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflow.id}`);
},
projects(): ProjectListItem[] {
return this.projectsStore.personalProjects.filter(
(project) => project.id !== this.workflow.homeProject?.id,
);
},
isHomeTeamProject(): boolean {
return this.workflow.homeProject?.type === ProjectTypes.Team;
},
numberOfMembersInHomeTeamProject(): number {
return this.teamProject?.relations.length ?? 0;
},
workflowRoleTranslations(): Record<string, string> {
return {
'workflow:editor': this.$locale.baseText('workflows.shareModal.role.editor'),
};
},
workflowRoles(): RoleMap['workflow'] {
return this.rolesStore.processedWorkflowRoles.map(({ role, scopes, licensed }) => ({
role,
name: this.workflowRoleTranslations[role],
scopes,
licensed,
}));
},
},
watch: {
sharedWithProjects: {
handler() {
this.isDirty = true;
},
deep: true,
},
},
async mounted() {
await this.initialize();
},
methods: {
onProjectAdded(project: ProjectSharingData) {
this.trackTelemetry('User selected sharee to add', {
project_id_sharer: this.workflow.homeProject?.id,
project_id_sharee: project.id,
});
},
onProjectRemoved(project: ProjectSharingData) {
this.trackTelemetry('User selected sharee to remove', {
project_id_sharer: this.workflow.homeProject?.id,
project_id_sharee: project.id,
});
},
async onSave() {
if (this.loading) {
return;
}
this.loading = true;
const saveWorkflowPromise = async () => {
return await new Promise<string>((resolve) => {
if (this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
nodeViewEventBus.emit('saveWorkflow', () => {
resolve(this.workflow.id);
});
} else {
resolve(this.workflow.id);
}
});
};
try {
const workflowId = await saveWorkflowPromise();
await this.workflowsEEStore.saveWorkflowSharedWith({
workflowId,
sharedWithProjects: this.sharedWithProjects,
});
this.showMessage({
title: this.$locale.baseText('workflows.shareModal.onSave.success.title'),
type: 'success',
});
this.isDirty = false;
} catch (error) {
this.showError(error, this.$locale.baseText('workflows.shareModal.onSave.error.title'));
} finally {
this.modalBus.emit('close');
this.loading = false;
}
},
async onCloseModal() {
if (this.isDirty) {
const shouldSave = await this.confirm(
this.$locale.baseText('workflows.shareModal.saveBeforeClose.message'),
this.$locale.baseText('workflows.shareModal.saveBeforeClose.title'),
{
type: 'warning',
confirmButtonText: this.$locale.baseText(
'workflows.shareModal.saveBeforeClose.confirmButtonText',
),
cancelButtonText: this.$locale.baseText(
'workflows.shareModal.saveBeforeClose.cancelButtonText',
),
},
);
if (shouldSave === MODAL_CONFIRM) {
return await this.onSave();
}
}
return true;
},
goToUsersSettings() {
this.$router.push({ name: VIEWS.USERS_SETTINGS }).catch((failure) => {
if (!isNavigationFailure(failure)) {
console.error(failure);
}
});
this.modalBus.emit('close');
},
trackTelemetry(eventName: string, data: ITelemetryTrackProperties) {
this.$telemetry.track(eventName, {
workflow_id: this.workflow.id,
...data,
});
},
goToUpgrade() {
void this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
},
async initialize() {
if (this.isSharingEnabled) {
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
if (this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
await this.workflowsStore.fetchWorkflow(this.workflow.id);
}
if (this.isHomeTeamProject && this.workflow.homeProject) {
this.teamProject = await this.projectsStore.fetchProject(this.workflow.homeProject.id);
}
}
this.loading = false;
},
},
});
</script>
<style module lang="scss">
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.container > * {
overflow-wrap: break-word;
}
.content {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
.usersList {
height: 100%;
overflow-y: auto;
}
.actionButtons {
display: flex;
justify-content: flex-end;
align-items: center;
}
.roleSelect {
max-width: 100px;
}
.roleSelectRemoveOption {
border-top: 1px solid var(--color-foreground-base);
}
</style>