Skip to content

Commit

Permalink
fix(editor): UX Improvements to RBAC feature set (#9683)
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik authored Jul 18, 2024
1 parent 5b440a7 commit 028a8a2
Show file tree
Hide file tree
Showing 32 changed files with 335 additions and 110 deletions.
2 changes: 1 addition & 1 deletion cypress/composables/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const addProjectMember = (email: string) => {
getProjectMembersSelect().click();
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
};
export const getProjectNameInput = () => cy.get('#projectName');
export const getProjectNameInput = () => cy.get('#projectName').find('input');
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
export const getResourceMoveConfirmModal = () =>
cy.getByTestId('project-move-resource-confirm-modal');
Expand Down
7 changes: 2 additions & 5 deletions packages/design-system/src/components/N8nAvatar/Avatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import Avatar from 'vue-boring-avatars';
import { getInitials } from '../../utils/labelUtil';
interface AvatarProps {
firstName: string;
Expand All @@ -37,11 +38,7 @@ const props = withDefaults(defineProps<AvatarProps>(), {
],
});
const initials = computed(
() =>
(props.firstName ? props.firstName.charAt(0) : '') +
(props.lastName ? props.lastName.charAt(0) : ''),
);
const initials = computed(() => getInitials(`${props.firstName} ${props.lastName}`));
const getColors = (colors: string[]): string[] => {
const style = getComputedStyle(document.body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ import N8nIcon from '../N8nIcon';
import ConditionalRouterLink from '../ConditionalRouterLink';
import type { IMenuItem } from '../../types';
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
import { getInitials } from './labelUtil';
import { getInitials } from '../../utils/labelUtil';
interface MenuItemProps {
item: IMenuItem;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getInitials } from '../labelUtil';
import { getInitials } from './labelUtil';

describe('labelUtil.getInitials', () => {
it.each([
Expand Down
14 changes: 11 additions & 3 deletions packages/editor-ui/src/components/CredentialCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ProjectSharingData } from '@/types/projects.types';
import { useProjectsStore } from '@/stores/projects.store';
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
import { useI18n } from '@/composables/useI18n';
import { ResourceType } from '@/utils/projects.utils';
const CREDENTIAL_LIST_ITEM_ACTIONS = {
OPEN: 'open',
Expand Down Expand Up @@ -45,6 +46,7 @@ const uiStore = useUIStore();
const credentialsStore = useCredentialsStore();
const projectsStore = useProjectsStore();
const resourceTypeLabel = computed(() => locale.baseText('generic.credential').toLowerCase());
const credentialType = computed(() => credentialsStore.getCredentialTypeByName(props.data.type));
const credentialPermissions = computed(() => getCredentialPermissions(props.data));
const actions = computed(() => {
Expand Down Expand Up @@ -76,7 +78,7 @@ const formattedCreatedAtDate = computed(() => {
return dateformat(
props.data.createdAt,
`d mmmm${props.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`,
`d mmmm${String(props.data.createdAt).startsWith(currentYear) ? '' : ', yyyy'}`,
);
});
Expand Down Expand Up @@ -121,7 +123,8 @@ function moveResource() {
name: PROJECT_MOVE_RESOURCE_MODAL,
data: {
resource: props.data,
resourceType: locale.baseText('generic.credential').toLocaleLowerCase(),
resourceType: ResourceType.Credential,
resourceTypeLabel: resourceTypeLabel.value,
},
});
}
Expand Down Expand Up @@ -150,7 +153,12 @@ function moveResource() {
</div>
<template #append>
<div :class="$style.cardActions" @click.stop>
<ProjectCardBadge :resource="data" :personal-project="projectsStore.personalProject" />
<ProjectCardBadge
:resource="data"
:resource-type="ResourceType.Credential"
:resource-type-label="resourceTypeLabel"
:personal-project="projectsStore.personalProject"
/>
<n8n-action-toggle
data-test-id="credential-card-actions"
:actions="actions"
Expand Down
6 changes: 3 additions & 3 deletions packages/editor-ui/src/components/PersonalizationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -674,10 +674,10 @@ export default defineComponent({
methods: {
closeDialog() {
this.modalBus.emit('close');
// In case the redirect to canvas for new users didn't happen
// In case the redirect to homepage for new users didn't happen
// we try again after closing the modal
if (this.$route.name !== VIEWS.NEW_WORKFLOW) {
void this.$router.replace({ name: VIEWS.NEW_WORKFLOW });
if (this.$route.name !== VIEWS.HOMEPAGE) {
void this.$router.replace({ name: VIEWS.HOMEPAGE });
}
},
async loadDomainBlocklist() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('ProjectCardBadge', () => {
id: '1',
},
},
resourceType: 'workflow',
personalProject: {
id: '1',
},
Expand All @@ -49,6 +50,7 @@ describe('ProjectCardBadge', () => {
name,
},
},
resourceType: 'workflow',
personalProject: {
id: '2',
},
Expand Down
103 changes: 85 additions & 18 deletions packages/editor-ui/src/components/Projects/ProjectCardBadge.vue
Original file line number Diff line number Diff line change
@@ -1,52 +1,119 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from '@/composables/useI18n';
import type { ResourceType } from '@/utils/projects.utils';
import { splitName } from '@/utils/projects.utils';
import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
import type { Project } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
type Props = {
resource: IWorkflowDb | ICredentialsResponse;
resourceType: ResourceType;
resourceTypeLabel: string;
personalProject: Project | null;
};
const enum ProjectState {
SharedPersonal = 'shared-personal',
SharedOwned = 'shared-owned',
Owned = 'owned',
Personal = 'personal',
Team = 'team',
Unknown = 'unknown',
}
const props = defineProps<Props>();
const locale = useI18n();
const i18n = useI18n();
const badgeText = computed(() => {
const projectState = computed(() => {
if (
(props.resource.homeProject &&
props.personalProject &&
props.resource.homeProject.id === props.personalProject.id) ||
!props.resource.homeProject
) {
return locale.baseText('generic.ownedByMe');
} else {
const { firstName, lastName, email } = splitName(props.resource.homeProject?.name ?? '');
return !firstName ? email : `${firstName}${lastName ? ' ' + lastName : ''}`;
if (props.resource.sharedWithProjects?.length) {
return ProjectState.SharedOwned;
}
return ProjectState.Owned;
} else if (props.resource.homeProject?.type !== ProjectTypes.Team) {
if (props.resource.sharedWithProjects?.length) {
return ProjectState.SharedPersonal;
}
return ProjectState.Personal;
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
return ProjectState.Team;
}
return ProjectState.Unknown;
});
const badgeIcon = computed(() => {
const badgeText = computed(() => {
if (
props.resource.sharedWithProjects?.length &&
props.resource.homeProject?.type !== ProjectTypes.Team
projectState.value === ProjectState.Owned ||
projectState.value === ProjectState.SharedOwned
) {
return 'user-friends';
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
return 'archive';
return i18n.baseText('generic.ownedByMe');
} else {
return '';
const { firstName, lastName, email } = splitName(props.resource.homeProject?.name ?? '');
return (!firstName ? email : `${firstName}${lastName ? ' ' + lastName : ''}`) ?? '';
}
});
const badgeIcon = computed(() => {
switch (projectState.value) {
case ProjectState.SharedPersonal:
case ProjectState.SharedOwned:
return 'user-friends';
case ProjectState.Team:
return 'archive';
default:
return '';
}
});
const badgeTooltip = computed(() => {
switch (projectState.value) {
case ProjectState.SharedOwned:
return i18n.baseText('projects.badge.tooltip.sharedOwned', {
interpolate: {
resourceTypeLabel: props.resourceTypeLabel,
},
});
case ProjectState.SharedPersonal:
return i18n.baseText('projects.badge.tooltip.sharedPersonal', {
interpolate: {
resourceTypeLabel: props.resourceTypeLabel,
name: badgeText.value,
},
});
case ProjectState.Personal:
return i18n.baseText('projects.badge.tooltip.personal', {
interpolate: {
resourceTypeLabel: props.resourceTypeLabel,
name: badgeText.value,
},
});
case ProjectState.Team:
return i18n.baseText('projects.badge.tooltip.team', {
interpolate: {
resourceTypeLabel: props.resourceTypeLabel,
name: badgeText.value,
},
});
default:
return '';
}
});
</script>
<template>
<n8n-badge v-if="badgeText" class="mr-xs" theme="tertiary" bold data-test-id="card-badge">
{{ badgeText }}
<n8n-icon v-if="badgeIcon" :icon="badgeIcon" size="small" class="ml-5xs" />
</n8n-badge>
<N8nTooltip :disabled="!badgeTooltip" placement="top">
<N8nBadge v-if="badgeText" class="mr-xs" theme="tertiary" bold data-test-id="card-badge">
{{ badgeText }}
<N8nIcon v-if="badgeIcon" :icon="badgeIcon" size="small" class="ml-5xs" />
</N8nBadge>
<template #content>
{{ badgeTooltip }}
</template>
</N8nTooltip>
</template>

<style lang="scss" module></style>
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('ProjectMoveResourceConfirmModal', () => {
id: '1',
},
projectId: '1',
projectName: 'My Project',
},
};
const { getByRole, getAllByRole } = renderComponent({ props });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, h } from 'vue';
import type { ICredentialsResponse, IWorkflowDb } from '@/Interface';
import { useI18n } from '@/composables/useI18n';
import { useUIStore } from '@/stores/ui.store';
Expand All @@ -8,13 +8,18 @@ import Modal from '@/components/Modal.vue';
import { N8nCheckbox, N8nText } from 'n8n-design-system';
import { useToast } from '@/composables/useToast';
import { useTelemetry } from '@/composables/useTelemetry';
import { VIEWS } from '@/constants';
import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue';
import { ResourceType } from '@/utils/projects.utils';
const props = defineProps<{
modalName: string;
data: {
resource: IWorkflowDb | ICredentialsResponse;
resourceType: 'workflow' | 'credential';
resourceType: ResourceType;
resourceTypeLabel: string;
projectId: string;
projectName: string;
};
}>();
Expand All @@ -28,7 +33,7 @@ const checks = ref([false, false]);
const allChecked = computed(() => checks.value.every(Boolean));
const moveResourceLabel = computed(() =>
props.data.resourceType === 'workflow'
props.data.resourceType === ResourceType.Workflow
? i18n.baseText('projects.move.workflow.confirm.modal.label')
: i18n.baseText('projects.move.credential.confirm.modal.label'),
);
Expand All @@ -49,12 +54,30 @@ const confirm = async () => {
[`${props.data.resourceType}_id`]: props.data.resource.id,
project_from_type: projectsStore.currentProject?.type ?? projectsStore.personalProject?.type,
});
toast.showToast({
title: i18n.baseText('projects.move.resource.success.title', {
interpolate: {
resourceTypeLabel: props.data.resourceTypeLabel,
},
}),
message: h(ProjectMoveSuccessToastMessage, {
routeName:
props.data.resourceType === ResourceType.Workflow
? VIEWS.PROJECTS_WORKFLOWS
: VIEWS.PROJECTS_CREDENTIALS,
resource: props.data.resource,
resourceTypeLabel: props.data.resourceTypeLabel,
projectId: props.data.projectId,
projectName: props.data.projectName,
}),
type: 'success',
});
} catch (error) {
toast.showError(
error.message,
i18n.baseText('projects.move.resource.error.title', {
interpolate: {
resourceType: props.data.resourceType,
resourceTypeLabel: props.data.resourceTypeLabel,
resourceName: props.data.resource.name,
},
}),
Expand All @@ -74,7 +97,7 @@ const confirm = async () => {
<N8nCheckbox v-model="checks[1]">
<N8nText>
<i18n-t keypath="projects.move.resource.confirm.modal.label">
<template #resourceType>{{ props.data.resourceType }}</template>
<template #resourceTypeLabel>{{ props.data.resourceTypeLabel }}</template>
<template #numberOfUsers>{{
i18n.baseText('projects.move.resource.confirm.modal.numberOfUsers', {
interpolate: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('ProjectMoveResourceModal', () => {
id: '1',
},
projectId: '1',
projectName: 'My Project',
},
};
renderComponent({ props });
Expand Down
Loading

0 comments on commit 028a8a2

Please sign in to comment.