fix(editor): Fix sorting problem in older browsers that don't support toSorted (#11204)

This commit is contained in:
Csaba Tuncsik 2024-10-17 14:11:34 +02:00 committed by GitHub
parent 83ca7f8e90
commit c728a2ffe0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 43 deletions

View file

@ -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),

View file

@ -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();
});
}); });

View file

@ -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>

View file

@ -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) => {

View file

@ -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 {

View 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 },
]);
});
});

View file

@ -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;
});