diff --git a/packages/editor-ui/src/components/Projects/ProjectMoveResourceModal.vue b/packages/editor-ui/src/components/Projects/ProjectMoveResourceModal.vue index 1f30e1d1b3..c4814550cd 100644 --- a/packages/editor-ui/src/components/Projects/ProjectMoveResourceModal.vue +++ b/packages/editor-ui/src/components/Projects/ProjectMoveResourceModal.vue @@ -12,6 +12,7 @@ import { ProjectTypes } from '@/types/projects.types'; import ProjectMoveSuccessToastMessage from '@/components/Projects/ProjectMoveSuccessToastMessage.vue'; import { useToast } from '@/composables/useToast'; import { getResourcePermissions } from '@/permissions'; +import { sortByProperty } from '@/utils/sortUtils'; const props = defineProps<{ modalName: string; @@ -34,14 +35,15 @@ const processedName = computed( () => processProjectName(props.data.resource.homeProject?.name ?? '') ?? '', ); const availableProjects = computed(() => - projectsStore.availableProjects - .filter( + sortByProperty( + 'name', + projectsStore.availableProjects.filter( (p) => p.name?.toLowerCase().includes(filter.value.toLowerCase()) && p.id !== props.data.resource.homeProject?.id && (!p.scopes || getResourcePermissions(p.scopes)[props.data.resourceType].create), - ) - .sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? 0), + ), + ), ); const selectedProject = computed(() => availableProjects.value.find((p) => p.id === projectId.value), diff --git a/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts b/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts index 5ad62ed8e9..29a30f8a0c 100644 --- a/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts +++ b/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts @@ -172,4 +172,16 @@ describe('ProjectsNavigation', () => { 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(); + }); }); diff --git a/packages/editor-ui/src/components/Projects/ProjectNavigation.vue b/packages/editor-ui/src/components/Projects/ProjectNavigation.vue index e14328b01e..a4cde7ea8c 100644 --- a/packages/editor-ui/src/components/Projects/ProjectNavigation.vue +++ b/packages/editor-ui/src/components/Projects/ProjectNavigation.vue @@ -8,6 +8,7 @@ import { useProjectsStore } from '@/stores/projects.store'; import type { ProjectListItem } from '@/types/projects.types'; import { useToast } from '@/composables/useToast'; import { useUIStore } from '@/stores/ui.store'; +import { sortByProperty } from '@/utils/sortUtils'; type Props = { collapsed: boolean; @@ -86,21 +87,16 @@ const addProjectClicked = async () => { } }; -const displayProjects = computed(() => { - return projectsStore.myProjects - .filter((p) => p.type === 'team') - .toSorted((a, b) => { - if (!a.name || !b.name) { - return 0; - } - if (a.name > b.name) { - return 1; - } else if (a.name < b.name) { - return -1; - } - return 0; - }); -}); +const displayProjects = computed(() => + sortByProperty( + 'name', + projectsStore.myProjects.filter((p) => p.type === 'team'), + ), +); + +const canCreateProjects = computed( + () => projectsStore.hasPermissionToCreateProjects && projectsStore.isTeamProjectFeatureEnabled, +); const goToUpgrade = async () => { await uiStore.goToUpgrade('rbac', 'upgrade-rbac'); @@ -123,14 +119,13 @@ onMounted(async () => { data-test-id="project-home-menu-item" /> -
- +
+ {{ locale.baseText('projects.menu.title') }} @@ -155,9 +150,7 @@ onMounted(async () => { /> @@ -189,13 +182,7 @@ onMounted(async () => { -
+
diff --git a/packages/editor-ui/src/components/Projects/ProjectSharing.vue b/packages/editor-ui/src/components/Projects/ProjectSharing.vue index 788eec20cb..600d55892d 100644 --- a/packages/editor-ui/src/components/Projects/ProjectSharing.vue +++ b/packages/editor-ui/src/components/Projects/ProjectSharing.vue @@ -4,6 +4,7 @@ import { useI18n } from '@/composables/useI18n'; import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types'; import ProjectSharingInfo from '@/components/Projects/ProjectSharingInfo.vue'; import type { RoleMap } from '@/types/roles.types'; +import { sortByProperty } from '@/utils/sortUtils'; const locale = useI18n(); @@ -35,13 +36,14 @@ const noDataText = computed( () => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'), ); const filteredProjects = computed(() => - props.projects - .filter( + sortByProperty( + 'name', + props.projects.filter( (project) => project.name?.toLowerCase().includes(filter.value.toLowerCase()) && (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) => { diff --git a/packages/editor-ui/src/components/Workers/WorkerCard.ee.vue b/packages/editor-ui/src/components/Workers/WorkerCard.ee.vue index c8c7c222c9..75096b7639 100644 --- a/packages/editor-ui/src/components/Workers/WorkerCard.ee.vue +++ b/packages/editor-ui/src/components/Workers/WorkerCard.ee.vue @@ -7,6 +7,7 @@ import { averageWorkerLoadFromLoadsAsString, memAsGb } from '../../utils/workerU import WorkerJobAccordion from './WorkerJobAccordion.ee.vue'; import WorkerNetAccordion from './WorkerNetAccordion.ee.vue'; import WorkerChartsAccordion from './WorkerChartsAccordion.ee.vue'; +import { sortByProperty } from '@/utils/sortUtils'; let interval: NodeJS.Timer; @@ -23,8 +24,8 @@ const worker = computed((): WorkerStatus | undefined => { return orchestrationStore.getWorkerStatus(props.workerId); }); -const sortedWorkerInterfaces = computed( - () => worker.value?.interfaces.toSorted((a, b) => a.family.localeCompare(b.family)) ?? [], +const sortedWorkerInterfaces = computed(() => + sortByProperty('family', worker.value?.interfaces.slice() ?? []), ); function upTime(seconds: number): string { diff --git a/packages/editor-ui/src/utils/sortUtils.test.ts b/packages/editor-ui/src/utils/sortUtils.test.ts new file mode 100644 index 0000000000..45a657df07 --- /dev/null +++ b/packages/editor-ui/src/utils/sortUtils.test.ts @@ -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 }, + ]); + }); +}); diff --git a/packages/editor-ui/src/utils/sortUtils.ts b/packages/editor-ui/src/utils/sortUtils.ts index 3d92d7a5e1..5e5534cdda 100644 --- a/packages/editor-ui/src/utils/sortUtils.ts +++ b/packages/editor-ui/src/utils/sortUtils.ts @@ -275,3 +275,16 @@ export function sublimeSearch( return results; } + +export const sortByProperty = ( + 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; + });