refactor(editor): Migrate WorkflowShareModal.ee.vue to composition API (#11605)

This commit is contained in:
Ricardo Espinoza 2024-11-07 07:28:27 -05:00 committed by GitHub
parent d25ae8e0d9
commit 81921387ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { computed, watch, onMounted, ref } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
@ -8,10 +7,8 @@ import {
EnterpriseEditionFeature, EnterpriseEditionFeature,
MODAL_CONFIRM, MODAL_CONFIRM,
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
VIEWS,
WORKFLOW_SHARE_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import type { IUser, IWorkflowDb } from '@/Interface';
import { getResourcePermissions } from '@/permissions'; import { getResourcePermissions } from '@/permissions';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
@ -23,239 +20,208 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import type { ITelemetryTrackProperties } from 'n8n-workflow'; import type { ITelemetryTrackProperties } from 'n8n-workflow';
import type { BaseTextKey } from '@/plugins/i18n'; import type { BaseTextKey } from '@/plugins/i18n';
import { isNavigationFailure } from 'vue-router';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue'; import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectListItem, ProjectSharingData, Project } from '@/types/projects.types'; import type { ProjectSharingData, Project } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import { useRolesStore } from '@/stores/roles.store'; import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useI18n } from '@/composables/useI18n';
import { telemetry } from '@/plugins/telemetry';
export default defineComponent({ const props = defineProps<{
name: 'WorkflowShareModal', data: {
components: { id: string;
Modal, };
ProjectSharing, }>();
},
props: {
data: {
type: Object,
default: () => ({}),
},
},
setup() {
return {
...useToast(),
...useMessage(),
...usePageRedirectionHelper(),
};
},
data() {
const workflowsStore = useWorkflowsStore();
const workflow =
this.data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID
? workflowsStore.workflow
: workflowsStore.workflowsById[this.data.id];
return { const { data } = props;
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( const workflowsStore = useWorkflowsStore();
this.isSharingEnabled const settingsStore = useSettingsStore();
? (this.uiStore.contextBasedTranslationKeys.workflows.sharing.title as BaseTextKey) const uiStore = useUIStore();
: (this.uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable const usersStore = useUsersStore();
.title as BaseTextKey), const workflowsEEStore = useWorkflowsEEStore();
{ const projectsStore = useProjectsStore();
interpolate: { name: this.workflow.name }, const rolesStore = useRolesStore();
},
);
},
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() {
return getResourcePermissions(this.workflow?.scopes).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 toast = useToast();
const message = useMessage();
const pageRedirectionHelper = usePageRedirectionHelper();
const i18n = useI18n();
const saveWorkflowPromise = async () => { const workflow = ref(
return await new Promise<string>((resolve) => { data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID
if (this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) { ? workflowsStore.workflow
nodeViewEventBus.emit('saveWorkflow', () => { : workflowsStore.workflowsById[data.id],
resolve(this.workflow.id); );
}); const loading = ref(true);
} else { const isDirty = ref(false);
resolve(this.workflow.id); const modalBus = createEventBus();
} const sharedWithProjects = ref([
}); ...(workflow.value.sharedWithProjects ?? []),
}; ] as ProjectSharingData[]);
const teamProject = ref(null as Project | null);
try { const isSharingEnabled = computed(
const workflowId = await saveWorkflowPromise(); () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
await this.workflowsEEStore.saveWorkflowSharedWith({ );
workflowId,
sharedWithProjects: this.sharedWithProjects,
});
this.showMessage({ const isHomeTeamProject = computed(() => workflow.value.homeProject?.type === ProjectTypes.Team);
title: this.$locale.baseText('workflows.shareModal.onSave.success.title'),
type: 'success', const modalTitle = computed(() => {
}); if (isHomeTeamProject.value) {
this.isDirty = false; return i18n.baseText('workflows.shareModal.title.static', {
} catch (error) { interpolate: { projectName: workflow.value.homeProject?.name ?? '' },
this.showError(error, this.$locale.baseText('workflows.shareModal.onSave.error.title')); });
} finally { }
this.modalBus.emit('close');
this.loading = false; return i18n.baseText(
} isSharingEnabled.value
? (uiStore.contextBasedTranslationKeys.workflows.sharing.title as BaseTextKey)
: (uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.title as BaseTextKey),
{
interpolate: { name: workflow.value.name },
}, },
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.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;
},
},
}); });
const workflowPermissions = computed(() => getResourcePermissions(workflow.value?.scopes).workflow);
const workflowOwnerName = computed(() =>
workflowsEEStore.getWorkflowOwnerName(`${workflow.value.id}`),
);
const projects = computed(() =>
projectsStore.personalProjects.filter((project) => project.id !== workflow.value.homeProject?.id),
);
const numberOfMembersInHomeTeamProject = computed(() => teamProject.value?.relations.length ?? 0);
const workflowRoleTranslations = computed(() => ({
'workflow:editor': i18n.baseText('workflows.shareModal.role.editor'),
'workflow:owner': '',
}));
const workflowRoles = computed(() =>
rolesStore.processedWorkflowRoles.map(({ role, scopes, licensed }) => ({
role,
name: workflowRoleTranslations.value[role],
scopes,
licensed,
})),
);
const trackTelemetry = (eventName: string, data: ITelemetryTrackProperties) => {
telemetry.track(eventName, {
workflow_id: workflow.value.id,
...data,
});
};
const onProjectAdded = (project: ProjectSharingData) => {
trackTelemetry('User selected sharee to add', {
project_id_sharer: workflow.value.homeProject?.id,
project_id_sharee: project.id,
});
};
const onProjectRemoved = (project: ProjectSharingData) => {
trackTelemetry('User selected sharee to remove', {
project_id_sharer: workflow.value.homeProject?.id,
project_id_sharee: project.id,
});
};
const onSave = async () => {
if (loading.value) {
return;
}
loading.value = true;
const saveWorkflowPromise = async () => {
return await new Promise<string>((resolve) => {
if (workflow.value.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
nodeViewEventBus.emit('saveWorkflow', () => {
resolve(workflow.value.id);
});
} else {
resolve(workflow.value.id);
}
});
};
try {
const workflowId = await saveWorkflowPromise();
await workflowsEEStore.saveWorkflowSharedWith({
workflowId,
sharedWithProjects: sharedWithProjects.value,
});
toast.showMessage({
title: i18n.baseText('workflows.shareModal.onSave.success.title'),
});
isDirty.value = false;
} catch (error) {
toast.showError(error, i18n.baseText('workflows.shareModal.onSave.error.title'));
} finally {
modalBus.emit('close');
loading.value = false;
}
};
const onCloseModal = async () => {
if (isDirty.value) {
const shouldSave = await message.confirm(
i18n.baseText('workflows.shareModal.saveBeforeClose.message'),
i18n.baseText('workflows.shareModal.saveBeforeClose.title'),
{
type: 'warning',
confirmButtonText: i18n.baseText('workflows.shareModal.saveBeforeClose.confirmButtonText'),
cancelButtonText: i18n.baseText('workflows.shareModal.saveBeforeClose.cancelButtonText'),
},
);
if (shouldSave === MODAL_CONFIRM) {
return await onSave();
}
}
return true;
};
const goToUpgrade = () => {
void pageRedirectionHelper.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
};
const initialize = async () => {
if (isSharingEnabled.value) {
await Promise.all([usersStore.fetchUsers(), projectsStore.getAllProjects()]);
if (workflow.value.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
await workflowsStore.fetchWorkflow(workflow.value.id);
}
if (isHomeTeamProject.value && workflow.value.homeProject) {
teamProject.value = await projectsStore.fetchProject(workflow.value.homeProject.id);
}
}
loading.value = false;
};
onMounted(async () => {
await initialize();
});
watch(
sharedWithProjects,
() => {
isDirty.value = true;
},
{ deep: true },
);
</script> </script>
<template> <template>
@ -272,7 +238,7 @@ export default defineComponent({
<div v-if="!isSharingEnabled" :class="$style.container"> <div v-if="!isSharingEnabled" :class="$style.container">
<n8n-text> <n8n-text>
{{ {{
$locale.baseText( i18n.baseText(
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.description.modal, uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.description.modal,
) )
}} }}
@ -285,7 +251,7 @@ export default defineComponent({
class="mb-s" class="mb-s"
> >
{{ {{
$locale.baseText('workflows.shareModal.info.sharee', { i18n.baseText('workflows.shareModal.info.sharee', {
interpolate: { workflowOwnerName }, interpolate: { workflowOwnerName },
}) })
}} }}
@ -299,7 +265,7 @@ export default defineComponent({
:roles="workflowRoles" :roles="workflowRoles"
:readonly="!workflowPermissions.share" :readonly="!workflowPermissions.share"
:static="isHomeTeamProject || !workflowPermissions.share" :static="isHomeTeamProject || !workflowPermissions.share"
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')" :placeholder="i18n.baseText('workflows.shareModal.select.placeholder')"
@project-added="onProjectAdded" @project-added="onProjectAdded"
@project-removed="onProjectRemoved" @project-removed="onProjectRemoved"
/> />
@ -311,7 +277,7 @@ export default defineComponent({
<template #members> <template #members>
<strong> <strong>
{{ {{
$locale.baseText('workflows.shareModal.info.members.number', { i18n.baseText('workflows.shareModal.info.members.number', {
interpolate: { interpolate: {
number: String(numberOfMembersInHomeTeamProject), number: String(numberOfMembersInHomeTeamProject),
}, },
@ -344,9 +310,7 @@ export default defineComponent({
<div v-if="!isSharingEnabled" :class="$style.actionButtons"> <div v-if="!isSharingEnabled" :class="$style.actionButtons">
<n8n-button @click="goToUpgrade"> <n8n-button @click="goToUpgrade">
{{ {{
$locale.baseText( i18n.baseText(uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.button)
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.button,
)
}} }}
</n8n-button> </n8n-button>
</div> </div>
@ -356,10 +320,10 @@ export default defineComponent({
:class="$style.actionButtons" :class="$style.actionButtons"
> >
<n8n-text v-show="isDirty" color="text-light" size="small" class="mr-xs"> <n8n-text v-show="isDirty" color="text-light" size="small" class="mr-xs">
{{ $locale.baseText('workflows.shareModal.changesHint') }} {{ i18n.baseText('workflows.shareModal.changesHint') }}
</n8n-text> </n8n-text>
<n8n-button v-if="isHomeTeamProject" type="secondary" @click="modalBus.emit('close')"> <n8n-button v-if="isHomeTeamProject" type="secondary" @click="modalBus.emit('close')">
{{ $locale.baseText('generic.close') }} {{ i18n.baseText('generic.close') }}
</n8n-button> </n8n-button>
<n8n-button <n8n-button
v-else v-else
@ -369,7 +333,7 @@ export default defineComponent({
data-test-id="workflow-sharing-modal-save-button" data-test-id="workflow-sharing-modal-save-button"
@click="onSave" @click="onSave"
> >
{{ $locale.baseText('workflows.shareModal.save') }} {{ i18n.baseText('workflows.shareModal.save') }}
</n8n-button> </n8n-button>
</enterprise-edition> </enterprise-edition>
</template> </template>