diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts index de5669ae11..6fa2c6f502 100644 --- a/cypress/composables/projects.ts +++ b/cypress/composables/projects.ts @@ -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'); diff --git a/packages/design-system/src/components/N8nAvatar/Avatar.vue b/packages/design-system/src/components/N8nAvatar/Avatar.vue index 933b55343e..f7993a84a0 100644 --- a/packages/design-system/src/components/N8nAvatar/Avatar.vue +++ b/packages/design-system/src/components/N8nAvatar/Avatar.vue @@ -15,6 +15,7 @@ - - {{ badgeText }} - - + + + {{ badgeText }} + + + + {{ badgeTooltip }} + + diff --git a/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts b/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts index a81e412685..b413550a39 100644 --- a/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts +++ b/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts @@ -49,6 +49,7 @@ describe('ProjectMoveResourceConfirmModal', () => { id: '1', }, projectId: '1', + projectName: 'My Project', }, }; const { getByRole, getAllByRole } = renderComponent({ props }); diff --git a/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue b/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue index 454fe163cb..dcccb49a34 100644 --- a/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue +++ b/packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue @@ -1,5 +1,5 @@ + + + {{ props.resourceTypeLabel }} + {{ props.resource.name }} + {{ props.projectName }} + + + + + {{ props.projectName }} + + + + + + diff --git a/packages/editor-ui/src/components/Projects/ProjectNavigation.vue b/packages/editor-ui/src/components/Projects/ProjectNavigation.vue index 03b6331be4..d1f5bac9ce 100644 --- a/packages/editor-ui/src/components/Projects/ProjectNavigation.vue +++ b/packages/editor-ui/src/components/Projects/ProjectNavigation.vue @@ -124,6 +124,9 @@ onMounted(async () => { { color: var(--color-primary); cursor: pointer; } + +.collapsed { + text-transform: uppercase; +} + + diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index c552869039..56af9fcade 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2473,15 +2473,22 @@ "projects.create.limit": "{num} project | {num} projects", "projects.create.limitReached": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}", "projects.create.limitReached.link": "View plans", - "projects.move.resource.modal.title": "Choose a project to move this {resourceType} to", - "projects.move.resource.modal.message": "\"{resourceName}\" is currently in the \"{resourceHomeProjectName}\" project. Which project would you like to move this {resourceType} to?", + "projects.move.resource.modal.title": "Choose a project to move this {resourceTypeLabel} to", + "projects.move.resource.modal.message": "\"{resourceName}\" is currently in the \"{resourceHomeProjectName}\" project. Which project would you like to move this {resourceTypeLabel} to?", "projects.move.resource.confirm.modal.title": "Please confirm the following", "projects.move.resource.confirm.modal.button.confirm": "Confirm move to new project", "projects.move.workflow.confirm.modal.label": "This workflow may stop working if the credentials used with it do not exist in the project its being moved to", "projects.move.credential.confirm.modal.label": "Any workflows currently using this credential will stop working once this credential has been moved", - "projects.move.resource.confirm.modal.label": "Any individual sharing currently associated with this {resourceType} will be removed. (currently shared with {numberOfUsers})", + "projects.move.resource.confirm.modal.label": "Any individual sharing currently associated with this {resourceTypeLabel} will be removed. (currently shared with {numberOfUsers})", "projects.move.resource.confirm.modal.numberOfUsers": "{numberOfUsers} user | {numberOfUsers} users", - "projects.move.resource.error.title": "Error moving {resourceName} {resourceType}", + "projects.move.resource.error.title": "Error moving {resourceName} {resourceTypeLabel}", + "projects.move.resource.success.title": "Successfully moved {resourceTypeLabel}", + "projects.move.resource.success.message": "{resourceName} {resourceTypeLabel} was moved to {targetProjectName} {link}", + "projects.move.resource.success.link": "View {targetProjectName}", + "projects.badge.tooltip.sharedOwned": "This {resourceTypeLabel} is owned by you and shared with one or more other users", + "projects.badge.tooltip.sharedPersonal": "This {resourceTypeLabel} is owned by {name} and shared with one or more other users", + "projects.badge.tooltip.personal": "This {resourceTypeLabel} is owned by {name}", + "projects.badge.tooltip.team": "This {resourceTypeLabel} is owned by the {name} project. All users in this project have access to this.", "mfa.setup.invalidAuthenticatorCode": "{code} is not a valid number", "mfa.setup.invalidCode": "Two-factor code failed. Please try again.", "mfa.code.modal.title": "Two-factor authentication", diff --git a/packages/editor-ui/src/routes/projects.routes.ts b/packages/editor-ui/src/routes/projects.routes.ts index 0bb9300fec..09c13311cc 100644 --- a/packages/editor-ui/src/routes/projects.routes.ts +++ b/packages/editor-ui/src/routes/projects.routes.ts @@ -4,7 +4,7 @@ import { VIEWS } from '@/constants'; const MainSidebar = async () => await import('@/components/MainSidebar.vue'); const WorkflowsView = async () => await import('@/views/WorkflowsView.vue'); const CredentialsView = async () => await import('@/views/CredentialsView.vue'); -const ProjectSettings = async () => await import('@/components/Projects/ProjectSettings.vue'); +const ProjectSettings = async () => await import('@/views/ProjectSettings.vue'); const commonChildRoutes: RouteRecordRaw[] = [ { diff --git a/packages/editor-ui/src/utils/projects.utils.ts b/packages/editor-ui/src/utils/projects.utils.ts index 5e9537ddd9..99aa8baf05 100644 --- a/packages/editor-ui/src/utils/projects.utils.ts +++ b/packages/editor-ui/src/utils/projects.utils.ts @@ -26,3 +26,8 @@ export const splitName = ( } } }; + +export const enum ResourceType { + Credential = 'credential', + Workflow = 'workflow', +} diff --git a/packages/editor-ui/src/views/CredentialsView.vue b/packages/editor-ui/src/views/CredentialsView.vue index b876858d7c..56bb977dfc 100644 --- a/packages/editor-ui/src/views/CredentialsView.vue +++ b/packages/editor-ui/src/views/CredentialsView.vue @@ -7,6 +7,7 @@ :filters="filters" :additional-filters-handler="onFilter" :type-props="{ itemSize: 77 }" + :loading="loading" @click:add="addCredential" @update:filters="filters = $event" > @@ -79,8 +80,6 @@ import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import useEnvironmentsStore from '@/stores/environments.ee.store'; import { useSettingsStore } from '@/stores/settings.store'; -type IResourcesListLayoutInstance = InstanceType; - export default defineComponent({ name: 'CredentialsView', components: { @@ -96,6 +95,7 @@ export default defineComponent({ type: '', }, sourceControlStoreUnsubscribe: () => {}, + loading: false, }; }, computed: { @@ -133,9 +133,6 @@ export default defineComponent({ }, }, watch: { - 'filters.type'() { - this.sendFiltersTelemetry('type'); - }, '$route.params.projectId'() { void this.initialize(); }, @@ -161,6 +158,7 @@ export default defineComponent({ }); }, async initialize() { + this.loading = true; const isVarsEnabled = useSettingsStore().isEnterpriseFeatureEnabled( EnterpriseEditionFeature.Variables, ); @@ -176,6 +174,7 @@ export default defineComponent({ ]; await Promise.all(loadPromises); + this.loading = false; }, onFilter( resource: ICredentialsResponse, @@ -199,9 +198,6 @@ export default defineComponent({ return matches; }, - sendFiltersTelemetry(source: string) { - (this.$refs.layout as IResourcesListLayoutInstance).sendFiltersTelemetry(source); - }, }, }); diff --git a/packages/editor-ui/src/components/Projects/ProjectSettings.test.ts b/packages/editor-ui/src/views/ProjectSettings.test.ts similarity index 98% rename from packages/editor-ui/src/components/Projects/ProjectSettings.test.ts rename to packages/editor-ui/src/views/ProjectSettings.test.ts index 3a55ca24e5..594db154d5 100644 --- a/packages/editor-ui/src/components/Projects/ProjectSettings.test.ts +++ b/packages/editor-ui/src/views/ProjectSettings.test.ts @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { createComponentRenderer } from '@/__tests__/render'; import { getDropdownItems } from '@/__tests__/utils'; import { useRouter } from 'vue-router'; -import ProjectSettings from '@/components/Projects/ProjectSettings.vue'; +import ProjectSettings from '@/views/ProjectSettings.vue'; import { useProjectsStore } from '@/stores/projects.store'; import { VIEWS } from '@/constants'; import { useUsersStore } from '@/stores/users.store'; diff --git a/packages/editor-ui/src/components/Projects/ProjectSettings.vue b/packages/editor-ui/src/views/ProjectSettings.vue similarity index 95% rename from packages/editor-ui/src/components/Projects/ProjectSettings.vue rename to packages/editor-ui/src/views/ProjectSettings.vue index 6002335fe8..31db1d6689 100644 --- a/packages/editor-ui/src/components/Projects/ProjectSettings.vue +++ b/packages/editor-ui/src/views/ProjectSettings.vue @@ -1,7 +1,8 @@ @@ -256,14 +259,17 @@ onBeforeMount(async () => { {{ locale.baseText('projects.settings.name') }} - @@ -343,7 +349,7 @@ onBeforeMount(async () => { > {{ locale.baseText('projects.settings.button.save') }} { server.shutdown(); }); - it('should render loading state', () => { - const wrapper = renderComponent({ pinia }); - - expect(wrapper.container.querySelectorAll('.n8n-loading')).toHaveLength(3); - }); - describe('should render empty state', () => { it('when feature is disabled and logged in user is not owner', async () => { settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = false; diff --git a/packages/editor-ui/src/views/VariablesView.vue b/packages/editor-ui/src/views/VariablesView.vue index 250734fce8..d1e30c606d 100644 --- a/packages/editor-ui/src/views/VariablesView.vue +++ b/packages/editor-ui/src/views/VariablesView.vue @@ -38,7 +38,7 @@ const TEMPORARY_VARIABLE_UID_BASE = '@tmpvar'; const allVariables = ref([]); const editMode = ref>({}); - +const loading = ref(false); const permissions = getVariablesPermissions(usersStore.currentUser); const isFeatureEnabled = computed(() => @@ -132,9 +132,11 @@ const environmentVariableToResource = (data: EnvironmentVariable): IResource => async function initialize() { if (!isFeatureEnabled.value) return; + loading.value = true; await environmentsStore.fetchAllVariables(); allVariables.value = [...environmentsStore.variables]; + loading.value = false; } function addTemporaryVariable() { @@ -260,6 +262,7 @@ onBeforeUnmount(() => { :show-filters-dropdown="false" type="datatable" :type-props="{ columns: datatableColumns }" + :loading="loading" @sort="resetNewVariablesList" @click:add="addTemporaryVariable" > diff --git a/packages/editor-ui/src/views/WorkflowsView.test.ts b/packages/editor-ui/src/views/WorkflowsView.test.ts index 4481fa302d..226f621ee3 100644 --- a/packages/editor-ui/src/views/WorkflowsView.test.ts +++ b/packages/editor-ui/src/views/WorkflowsView.test.ts @@ -73,15 +73,13 @@ describe('WorkflowsView', () => { }); it('should filter workflows by tags', async () => { - const { container, getByTestId, getAllByTestId, queryByTestId } = renderComponent({ + const { getByTestId, getAllByTestId, queryByTestId } = renderComponent({ pinia, }); - expect(container.querySelectorAll('.n8n-loading')).toHaveLength(3); expect(queryByTestId('resources-list')).not.toBeInTheDocument(); await waitFor(() => { - expect(container.querySelectorAll('.n8n-loading')).toHaveLength(0); // There are 5 workflows defined in server fixtures expect(getAllByTestId('resources-list-item')).toHaveLength(5); }); diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue index 66f9d08e42..fd0da50265 100644 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ b/packages/editor-ui/src/views/WorkflowsView.vue @@ -9,11 +9,12 @@ :shareable="isShareable" :initialize="initialize" :disabled="readOnlyEnv" + :loading="loading" @click:add="addWorkflow" @update:filters="onFiltersUpdated" > - + @@ -162,8 +163,6 @@ import { useProjectsStore } from '@/stores/projects.store'; import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import { useTemplatesStore } from '@/stores/templates.store'; -type IResourcesListLayoutInstance = InstanceType; - interface Filters { search: string; homeProject: string; @@ -194,6 +193,7 @@ const WorkflowsView = defineComponent({ tags: [], } as Filters, sourceControlStoreUnsubscribe: () => {}, + loading: false, }; }, computed: { @@ -255,13 +255,6 @@ const WorkflowsView = defineComponent({ } return ['Sales', 'sales-and-marketing'].includes(this.userRole); }, - showProjectTabs() { - return ( - !!this.$route.params.projectId || - !!this.allWorkflows.length || - this.projectsStore.myProjects.length > 1 - ); - }, addWorkflowButtonText() { return this.projectsStore.currentProject ? this.$locale.baseText('workflows.project.add') @@ -275,9 +268,6 @@ const WorkflowsView = defineComponent({ this.saveFiltersOnQueryString(); }, }, - 'filters.tags'() { - this.sendFiltersTelemetry('tags'); - }, '$route.params.projectId'() { void this.initialize(); }, @@ -323,11 +313,13 @@ const WorkflowsView = defineComponent({ }); }, async initialize() { + this.loading = true; await Promise.all([ this.usersStore.fetchUsers(), this.workflowsStore.fetchAllWorkflows(this.$route?.params?.projectId as string | undefined), this.workflowsStore.fetchActiveWorkflows(), ]); + this.loading = false; }, onClickTag(tagId: string) { if (!this.filters.tags.includes(tagId)) { @@ -357,9 +349,6 @@ const WorkflowsView = defineComponent({ return matches; }, - sendFiltersTelemetry(source: string) { - (this.$refs.layout as IResourcesListLayoutInstance).sendFiltersTelemetry(source); - }, saveFiltersOnQueryString() { const query: { [key: string]: string } = {};
+ + {{ props.projectName }} + +