8000 Add role autocomplete based on existing active workers by devin-ai-integration[bot] · Pull Request #288 · antiwork/flexile · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add role autocomplete based on existing active workers #288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 11, 2025
Merged
57 changes: 46 additions & 11 deletions apps/next/app/people/FormFields.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,64 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { PayRateType } from "@/trpc/client";
import { PayRateType, trpc } from "@/trpc/client";
import { useFormContext } from "react-hook-form";
import { Input } from "@/components/ui/input";
import RadioButtons from "@/components/RadioButtons";
import NumberInput from "@/components/NumberInput";
import { useUserStore } from "@/global";
import { Popover, PopoverContent } from "@/components/ui/popover";
import { PopoverTrigger } from "@radix-ui/react-popover";
import { Command, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import { skipToken } from "@tanstack/react-query";

export default function FormFields() {
const form = useFormContext();
const payRateType: unknown = form.watch("payRateType");
const companyId = useUserStore((state) => state.user?.currentCompanyId);
const { data: workers } = trpc.contractors.list.useQuery(companyId ? { companyId, excludeAlumni: true } : skipToken);

const uniqueRoles = workers ? [...new Set(workers.map((worker) => worker.role))].sort() : [];

return (
<>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
render={({ field }) => {
const filter = new RegExp(`${field.value}`, "iu");
return (
<FormItem>
<FormLabel>Role</FormLabel>
<Command shouldFilter={false}>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Input {...field} type="text" />
</FormControl>
</PopoverTrigger>
<PopoverContent
=> e.preventDefault()}
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<CommandList>
<CommandGroup>
{uniqueRoles
.filter((role) => filter.test(role))
.map((option) => (
<CommandItem key={option} value={option} => field.onChange(e)}>
{option}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</PopoverContent>
</Popover>
</Command>
<FormMessage />
</FormItem>
);
}}
/>

<FormField
Expand Down
2 changes: 1 addition & 1 deletion apps/next/app/people/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const schema = z.object({
function Create() {
const company = useCurrentCompany();
const router = useRouter();
const [{ workers }] = trpc.contractors.list.useSuspenseQuery({ companyId: company.id, limit: 1 });
const [workers] = trpc.contractors.list.useSuspenseQuery({ companyId: company.id, limit: 1 });
const lastContractor = workers[0];
const [templateId, setTemplateId] = useState<string | null>(null);

Expand Down
7 changes: 2 additions & 5 deletions apps/next/app/people/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ import Status from "@/components/Status";
import { Button } from "@/components/ui/button";
import { useCurrentCompany } from "@/global";
import { countries } from "@/models/constants";
import type { RouterOutput } from "@/trpc";
import { trpc } from "@/trpc/client";
import { formatDate } from "@/utils/time";

type Contractor = RouterOutput["contractors"]["list"]["workers"][number];

export default function PeoplePage() {
const company = useCurrentCompany();
const [{ workers }] = trpc.contractors.list.useSuspenseQuery({ companyId: company.id });
const [workers] = trpc.contractors.list.useSuspenseQuery({ companyId: company.id });

const columnHelper = createColumnHelper<Contractor>();
const columnHelper = createColumnHelper<(typeof workers)[number]>();
const columns = useMemo(
() => [
columnHelper.accessor("user.name", {
Expand Down
2 changes: 1 addition & 1 deletion apps/next/trpc/routes/contractors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const contractorsRouter = createRouter({
onboardingCompleted: isOnboardingCompleted(worker.user),
} as const,
}));
return { workers };
return workers;
}),
listForTeamUpdates: companyProcedure.query(async ({ ctx }) => {
if (!ctx.companyAdministrator && !isActive(ctx.companyContractor)) throw new TRPCError({ code: "FORBIDDEN" });
Expand Down
92 changes: 92 additions & 0 deletions e2e/tests/company/administrator/role-autocomplete.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { companiesFactory } from "@test/factories/companies";
import { companyAdministratorsFactory } from "@test/factories/companyAdministrators";
import { companyContractorsFactory } from "@test/factories/companyContractors";
import { usersFactory } from "@test/factories/users";
import { login } from "@test/helpers/auth";
import { expect, test, type Page } from "@test/index";
import { PayRateType } from "@/db/enums";

test.describe("Role autocomplete", () => {
const role1 = "Developer";
const role2 = "Designer";
const role3 = "Project Manager";

const setup = async () => {
const { company } = await companiesFactory.create();
const { user: admin } = await usersFactory.create();
await companyAdministratorsFactory.create({
companyId: company.id,
userId: admin.id,
});

await companyContractorsFactory.create({
companyId: company.id,
role: role1,
payRateType: PayRateType.Hourly,
});

await companyContractorsFactory.create({
companyId: company.id,
role: role2,
payRateType: PayRateType.Hourly,
});

await companyContractorsFactory.create({
companyId: company.id,
role: role3,
payRateType: PayRateType.Hourly,
});

await companyContractorsFactory.create({
companyId: company.id,
role: "Alumni Role",
endedAt: new Date(),
payRateType: PayRateType.Hourly,
});
return { company, admin };
};

const testAutofill = async (page: Page) => {
const roleField = page.getByLabel("Role");
await roleField.fill("");
await roleField.click();
await expect(page.getByRole("option", { name: role1 })).toBeVisible();
await expect(page.getByRole("option", { name: role2 })).toBeVisible();
await expect(page.getByRole("option", { name: role3 })).toBeVisible();
await expect(page.getByRole("option", { name: "Alumni Role" })).not.toBeVisible();

await roleField.fill("dev");
await expect(page.getByRole("option", { name: role1 })).toBeVisible();
await expect(page.getByRole("option", { name: role2 })).not.toBeVisible();

await page.getByRole("option", { name: role1 }).click();
await expect(roleField).toHaveValue(role1);

await roleField.fill("dev");
await roleField.press("Enter");
await expect(roleField).toHaveValue(role1);
};

test("suggests existing roles when inviting a new contractor", async ({ page }) => {
const { admin } = await setup();
await login(page, admin);
await page.getByRole("link", { name: "People" }).click();
await page.getByRole("link", { name: "Invite contractor" }).click();
await testAutofill(page);
});

test("suggests existing roles when editing a contractor", async ({ page }) => {
const { company, admin } = await setup();
const { user } = await usersFactory.create();
const { companyContractor: contractor } = await companyContractorsFactory.create({
companyId: company.id,
userId: user.id,
});

await login(page, admin);
await page.getByRole("link", { name: "People" }).click();
await page.getByRole("link", { name: user.preferredName ?? "" }).click();
await expect(page.getByLabel("Role")).toHaveValue(contractor.role);
await testAutofill(page);
});
});
0