Skip to content

Commit f21436a

Browse files
authored
Merge pull request #2541 from trycompai/sale-34-policy-sort
SALE-34: Sort policies alphabetically by default
2 parents ff7b2ec + ced89c2 commit f21436a

File tree

6 files changed

+109
-34
lines changed

6 files changed

+109
-34
lines changed

apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyFilters.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { Search } from '@trycompai/design-system/icons';
1717
import { useMemo, useState } from 'react';
1818
import { PoliciesTableDS } from './PoliciesTableDS';
19+
import { comparePoliciesByName } from './policy-name-sort';
1920

2021
interface PolicyFiltersProps {
2122
policies: Policy[];
@@ -33,8 +34,8 @@ export function PolicyFilters({ policies }: PolicyFiltersProps) {
3334
const [searchQuery, setSearchQuery] = useState('');
3435
const [statusFilter, setStatusFilter] = useState<PolicyStatus | 'all' | 'archived'>('all');
3536
const [departmentFilter, setDepartmentFilter] = useState<string>('all');
36-
const [sortColumn, setSortColumn] = useState<'name' | 'status' | 'updatedAt'>('updatedAt');
37-
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
37+
const [sortColumn, setSortColumn] = useState<'name' | 'status' | 'updatedAt'>('name');
38+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
3839

3940
// Get unique departments from policies
4041
const departments = useMemo(() => {
@@ -74,7 +75,7 @@ export function PolicyFilters({ policies }: PolicyFiltersProps) {
7475
result.sort((a, b) => {
7576
let comparison = 0;
7677
if (sortColumn === 'name') {
77-
comparison = a.name.localeCompare(b.name);
78+
comparison = comparePoliciesByName(a, b);
7879
} else if (sortColumn === 'status') {
7980
comparison = a.status.localeCompare(b.status);
8081
} else if (sortColumn === 'updatedAt') {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { comparePoliciesByName } from './policy-name-sort';
3+
4+
describe('comparePoliciesByName', () => {
5+
it('sorts policy names alphabetically without case sensitivity', () => {
6+
const policies = [
7+
{ id: '2', name: 'zebra policy' },
8+
{ id: '3', name: 'Alpha policy' },
9+
{ id: '1', name: 'beta policy' },
10+
];
11+
12+
const sorted = [...policies].sort(comparePoliciesByName);
13+
14+
expect(sorted.map((policy) => policy.name)).toEqual([
15+
'Alpha policy',
16+
'beta policy',
17+
'zebra policy',
18+
]);
19+
});
20+
21+
it('falls back to deterministic ordering when names only differ by case', () => {
22+
const policies = [
23+
{ id: 'b', name: 'Policy' },
24+
{ id: 'a', name: 'policy' },
25+
{ id: 'c', name: 'policy' },
26+
];
27+
28+
const sorted = [...policies].sort(comparePoliciesByName);
29+
30+
expect(sorted.map((policy) => policy.id)).toEqual(['a', 'b', 'c']);
31+
});
32+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type NamedPolicy = {
2+
id: string;
3+
name: string;
4+
};
5+
6+
const POLICY_NAME_COLLATOR = new Intl.Collator(undefined, { sensitivity: 'base' });
7+
8+
export function comparePoliciesByName(
9+
a: NamedPolicy,
10+
b: NamedPolicy,
11+
): number {
12+
const byName = POLICY_NAME_COLLATOR.compare(a.name, b.name);
13+
if (byName !== 0) {
14+
return byName;
15+
}
16+
17+
return a.id.localeCompare(b.id);
18+
}

apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { evidenceFormDefinitionList } from '@trycompai/company';
44
import { NoAccessMessage } from '../../components/NoAccessMessage';
55
import type { FleetPolicy, Host } from '../types';
66
import { EmployeeTasksList } from './EmployeeTasksList';
7+
import { sortPoliciesByName } from './policy/sort-policies-by-name';
78

89
const portalForms = evidenceFormDefinitionList
910
.filter((f) => f.portalAccessible)
@@ -31,23 +32,25 @@ export async function OrganizationDashboard({
3132
agentDevices,
3233
}: OrganizationDashboardProps) {
3334
// Fetch policies specific to the selected organization
34-
const policies = await db.policy.findMany({
35-
where: {
36-
organizationId: organizationId,
37-
isRequiredToSign: true,
38-
status: 'published',
39-
},
40-
include: {
41-
currentVersion: {
42-
select: {
43-
id: true,
44-
content: true,
45-
pdfUrl: true,
46-
version: true,
35+
const policies = sortPoliciesByName(
36+
await db.policy.findMany({
37+
where: {
38+
organizationId: organizationId,
39+
isRequiredToSign: true,
40+
status: 'published',
41+
},
42+
include: {
43+
currentVersion: {
44+
select: {
45+
id: true,
46+
content: true,
47+
pdfUrl: true,
48+
version: true,
49+
},
4750
},
4851
},
49-
},
50-
});
52+
}),
53+
);
5154

5255
// Fetch training video completions specific to the member
5356
const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
type NamedPolicy = {
2+
id: string;
3+
name: string;
4+
};
5+
6+
const POLICY_NAME_COLLATOR = new Intl.Collator(undefined, { sensitivity: 'base' });
7+
8+
export function sortPoliciesByName<T extends NamedPolicy>(
9+
policies: T[],
10+
): T[] {
11+
return [...policies].sort((a, b) => {
12+
const byName = POLICY_NAME_COLLATOR.compare(a.name, b.name);
13+
if (byName !== 0) {
14+
return byName;
15+
}
16+
17+
return a.id.localeCompare(b.id);
18+
});
19+
}

apps/portal/src/app/(app)/(home)/[orgId]/policies/page.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Document } from '@trycompai/design-system/icons';
1313
import { headers } from 'next/headers';
1414
import Link from 'next/link';
1515
import { redirect } from 'next/navigation';
16+
import { sortPoliciesByName } from '../components/policy/sort-policies-by-name';
1617

1718
export default async function SignedPoliciesPage({
1819
params,
@@ -41,22 +42,23 @@ export default async function SignedPoliciesPage({
4142
redirect('/');
4243
}
4344

44-
const policies = await db.policy.findMany({
45-
where: {
46-
organizationId: orgId,
47-
status: 'published',
48-
isRequiredToSign: true,
49-
isArchived: false,
50-
signedBy: { has: member.id },
51-
},
52-
orderBy: { name: 'asc' },
53-
select: {
54-
id: true,
55-
name: true,
56-
description: true,
57-
updatedAt: true,
58-
},
59-
});
45+
const policies = sortPoliciesByName(
46+
await db.policy.findMany({
47+
where: {
48+
organizationId: orgId,
49+
status: 'published',
50+
isRequiredToSign: true,
51+
isArchived: false,
52+
signedBy: { has: member.id },
53+
},
54+
select: {
55+
id: true,
56+
name: true,
57+
description: true,
58+
updatedAt: true,
59+
},
60+
}),
61+
);
6062

6163
return (
6264
<PageLayout>

0 commit comments

Comments
 (0)