mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
fix(editor): Fix sorting problem in older browsers that don't support toSorted
(#11204)
This commit is contained in:
parent
83ca7f8e90
commit
c728a2ffe0
|
@ -12,6 +12,7 @@ import { ProjectTypes } from '@/types/projects.types';
|
||||||
import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue';
|
import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modalName: string;
|
modalName: string;
|
||||||
|
@ -34,14 +35,15 @@ const processedName = computed(
|
||||||
() => processProjectName(props.data.resource.homeProject?.name ?? '') ?? '',
|
() => processProjectName(props.data.resource.homeProject?.name ?? '') ?? '',
|
||||||
);
|
);
|
||||||
const availableProjects = computed(() =>
|
const availableProjects = computed(() =>
|
||||||
projectsStore.availableProjects
|
sortByProperty(
|
||||||
.filter(
|
'name',
|
||||||
|
projectsStore.availableProjects.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.name?.toLowerCase().includes(filter.value.toLowerCase()) &&
|
p.name?.toLowerCase().includes(filter.value.toLowerCase()) &&
|
||||||
p.id !== props.data.resource.homeProject?.id &&
|
p.id !== props.data.resource.homeProject?.id &&
|
||||||
(!p.scopes || getResourcePermissions(p.scopes)[props.data.resourceType].create),
|
(!p.scopes || getResourcePermissions(p.scopes)[props.data.resourceType].create),
|
||||||
)
|
),
|
||||||
.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? 0),
|
),
|
||||||
);
|
);
|
||||||
const selectedProject = computed(() =>
|
const selectedProject = computed(() =>
|
||||||
availableProjects.value.find((p) => p.id === projectId.value),
|
availableProjects.value.find((p) => p.id === projectId.value),
|
||||||
|
|
|
@ -172,4 +172,16 @@ describe('ProjectsNavigation', () => {
|
||||||
|
|
||||||
expect(queryByRole('heading', { level: 3, name: 'Projects' })).not.toBeInTheDocument();
|
expect(queryByRole('heading', { level: 3, name: 'Projects' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not show "Projects" title when there are no available projects', async () => {
|
||||||
|
projectsStore.myProjects = [];
|
||||||
|
|
||||||
|
const { queryByRole } = renderComponent({
|
||||||
|
props: {
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByRole('heading', { level: 3, name: 'Projects' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { ProjectListItem } from '@/types/projects.types';
|
import type { ProjectListItem } from '@/types/projects.types';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
@ -86,21 +87,16 @@ const addProjectClicked = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayProjects = computed(() => {
|
const displayProjects = computed(() =>
|
||||||
return projectsStore.myProjects
|
sortByProperty(
|
||||||
.filter((p) => p.type === 'team')
|
'name',
|
||||||
.toSorted((a, b) => {
|
projectsStore.myProjects.filter((p) => p.type === 'team'),
|
||||||
if (!a.name || !b.name) {
|
),
|
||||||
return 0;
|
);
|
||||||
}
|
|
||||||
if (a.name > b.name) {
|
const canCreateProjects = computed(
|
||||||
return 1;
|
() => projectsStore.hasPermissionToCreateProjects && projectsStore.isTeamProjectFeatureEnabled,
|
||||||
} else if (a.name < b.name) {
|
);
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const goToUpgrade = async () => {
|
const goToUpgrade = async () => {
|
||||||
await uiStore.goToUpgrade('rbac', 'upgrade-rbac');
|
await uiStore.goToUpgrade('rbac', 'upgrade-rbac');
|
||||||
|
@ -123,14 +119,13 @@ onMounted(async () => {
|
||||||
data-test-id="project-home-menu-item"
|
data-test-id="project-home-menu-item"
|
||||||
/>
|
/>
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
<hr
|
<hr v-if="displayProjects.length || canCreateProjects" class="mt-m mb-m" />
|
||||||
v-if="
|
<N8nText
|
||||||
displayProjects.length ||
|
v-if="!props.collapsed && displayProjects.length"
|
||||||
(projectsStore.hasPermissionToCreateProjects && projectsStore.isTeamProjectFeatureEnabled)
|
:class="$style.projectsLabel"
|
||||||
"
|
tag="h3"
|
||||||
class="mt-m mb-m"
|
bold
|
||||||
/>
|
>
|
||||||
<N8nText v-if="!props.collapsed" :class="$style.projectsLabel" tag="h3" bold>
|
|
||||||
<span>{{ locale.baseText('projects.menu.title') }}</span>
|
<span>{{ locale.baseText('projects.menu.title') }}</span>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<ElMenu v-if="displayProjects.length" :collapse="props.collapsed" :class="$style.projectItems">
|
<ElMenu v-if="displayProjects.length" :collapse="props.collapsed" :class="$style.projectItems">
|
||||||
|
@ -155,9 +150,7 @@ onMounted(async () => {
|
||||||
/>
|
/>
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
<N8nTooltip
|
<N8nTooltip
|
||||||
v-if="
|
v-if="canCreateProjects"
|
||||||
projectsStore.hasPermissionToCreateProjects && projectsStore.isTeamProjectFeatureEnabled
|
|
||||||
"
|
|
||||||
placement="right"
|
placement="right"
|
||||||
:disabled="projectsStore.canCreateProjects"
|
:disabled="projectsStore.canCreateProjects"
|
||||||
>
|
>
|
||||||
|
@ -189,13 +182,7 @@ onMounted(async () => {
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</template>
|
</template>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
<hr
|
<hr v-if="displayProjects.length || canCreateProjects" class="mt-m mb-m" />
|
||||||
v-if="
|
|
||||||
displayProjects.length ||
|
|
||||||
(projectsStore.hasPermissionToCreateProjects && projectsStore.isTeamProjectFeatureEnabled)
|
|
||||||
"
|
|
||||||
class="mt-m mb-m"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||||
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
||||||
import ProjectSharingInfo from '@/components/Projects/ProjectSharingInfo.vue';
|
import ProjectSharingInfo from '@/components/Projects/ProjectSharingInfo.vue';
|
||||||
import type { RoleMap } from '@/types/roles.types';
|
import type { RoleMap } from '@/types/roles.types';
|
||||||
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
|
@ -35,13 +36,14 @@ const noDataText = computed(
|
||||||
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
|
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
|
||||||
);
|
);
|
||||||
const filteredProjects = computed(() =>
|
const filteredProjects = computed(() =>
|
||||||
props.projects
|
sortByProperty(
|
||||||
.filter(
|
'name',
|
||||||
|
props.projects.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name?.toLowerCase().includes(filter.value.toLowerCase()) &&
|
project.name?.toLowerCase().includes(filter.value.toLowerCase()) &&
|
||||||
(Array.isArray(model.value) ? !model.value?.find((p) => p.id === project.id) : true),
|
(Array.isArray(model.value) ? !model.value?.find((p) => p.id === project.id) : true),
|
||||||
)
|
),
|
||||||
.sort((a, b) => (a.name && b.name ? a.name.localeCompare(b.name) : 0)),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const setFilter = (query: string) => {
|
const setFilter = (query: string) => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { averageWorkerLoadFromLoadsAsString, memAsGb } from '../../utils/workerU
|
||||||
import WorkerJobAccordion from './WorkerJobAccordion.ee.vue';
|
import WorkerJobAccordion from './WorkerJobAccordion.ee.vue';
|
||||||
import WorkerNetAccordion from './WorkerNetAccordion.ee.vue';
|
import WorkerNetAccordion from './WorkerNetAccordion.ee.vue';
|
||||||
import WorkerChartsAccordion from './WorkerChartsAccordion.ee.vue';
|
import WorkerChartsAccordion from './WorkerChartsAccordion.ee.vue';
|
||||||
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
|
||||||
let interval: NodeJS.Timer;
|
let interval: NodeJS.Timer;
|
||||||
|
|
||||||
|
@ -23,8 +24,8 @@ const worker = computed((): WorkerStatus | undefined => {
|
||||||
return orchestrationStore.getWorkerStatus(props.workerId);
|
return orchestrationStore.getWorkerStatus(props.workerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedWorkerInterfaces = computed(
|
const sortedWorkerInterfaces = computed(() =>
|
||||||
() => worker.value?.interfaces.toSorted((a, b) => a.family.localeCompare(b.family)) ?? [],
|
sortByProperty('family', worker.value?.interfaces.slice() ?? []),
|
||||||
);
|
);
|
||||||
|
|
||||||
function upTime(seconds: number): string {
|
function upTime(seconds: number): string {
|
||||||
|
|
50
packages/editor-ui/src/utils/sortUtils.test.ts
Normal file
50
packages/editor-ui/src/utils/sortUtils.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { sortByProperty } from '@/utils/sortUtils';
|
||||||
|
|
||||||
|
const arrayOfObjects = [
|
||||||
|
{ name: 'Álvaro', age: 30 },
|
||||||
|
{ name: 'Élodie', age: 28 },
|
||||||
|
{ name: 'Željko', age: 25 },
|
||||||
|
{ name: 'Bob', age: 35 },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('sortUtils', () => {
|
||||||
|
it('"sortByProperty" should sort an array of objects by a property', () => {
|
||||||
|
const sortedArray = sortByProperty('name', arrayOfObjects);
|
||||||
|
expect(sortedArray).toEqual([
|
||||||
|
{ name: 'Álvaro', age: 30 },
|
||||||
|
{ name: 'Bob', age: 35 },
|
||||||
|
{ name: 'Élodie', age: 28 },
|
||||||
|
{ name: 'Željko', age: 25 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"sortByProperty" should sort an array of objects by a property in descending order', () => {
|
||||||
|
const sortedArray = sortByProperty('name', arrayOfObjects, 'desc');
|
||||||
|
expect(sortedArray).toEqual([
|
||||||
|
{ name: 'Željko', age: 25 },
|
||||||
|
{ name: 'Élodie', age: 28 },
|
||||||
|
{ name: 'Bob', age: 35 },
|
||||||
|
{ name: 'Álvaro', age: 30 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"sortByProperty" should sort an array of objects by a property if its number', () => {
|
||||||
|
const sortedArray = sortByProperty('age', arrayOfObjects);
|
||||||
|
expect(sortedArray).toEqual([
|
||||||
|
{ name: 'Željko', age: 25 },
|
||||||
|
{ name: 'Élodie', age: 28 },
|
||||||
|
{ name: 'Álvaro', age: 30 },
|
||||||
|
{ name: 'Bob', age: 35 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"sortByProperty" should sort an array of objects by a property in descending order if its number', () => {
|
||||||
|
const sortedArray = sortByProperty('age', arrayOfObjects, 'desc');
|
||||||
|
expect(sortedArray).toEqual([
|
||||||
|
{ name: 'Bob', age: 35 },
|
||||||
|
{ name: 'Álvaro', age: 30 },
|
||||||
|
{ name: 'Élodie', age: 28 },
|
||||||
|
{ name: 'Željko', age: 25 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -275,3 +275,16 @@ export function sublimeSearch<T extends object>(
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sortByProperty = <T>(
|
||||||
|
property: keyof T,
|
||||||
|
arr: T[],
|
||||||
|
order: 'asc' | 'desc' = 'asc',
|
||||||
|
): T[] =>
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const result = String(a[property]).localeCompare(String(b[property]), undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: 'base',
|
||||||
|
});
|
||||||
|
return order === 'asc' ? result : -result;
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue