Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions core/app/c/[communitySlug]/members/MemberTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@

import * as React from "react";

import type { CommunitiesId, FormsId } from "db/public";

import type { TableMember } from "./getMemberTableColumns";
import { DataTable } from "~/app/components/DataTable/DataTable";
import { getMemberTableColumns } from "./getMemberTableColumns";

export const MemberTable = ({ members }: { members: TableMember[] }) => {
const memberTableColumns = getMemberTableColumns();
export const MemberTable = ({
members,
availableForms,
communityId,
}: {
members: TableMember[];
availableForms: { id: FormsId; name: string; isDefault: boolean }[];
communityId: CommunitiesId;
}) => {
const memberTableColumns = getMemberTableColumns({ availableForms, communityId });
return <DataTable columns={memberTableColumns} data={members} searchBy="email" />;
};
2 changes: 1 addition & 1 deletion core/app/c/[communitySlug]/members/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export const removeMember = defineServerAction(async function removeMember({
member: TableMember;
}) {
try {
const { user, error: adminError, community } = await isCommunityAdmin();
const { error: adminError, community } = await isCommunityAdmin();

if (adminError !== null) {
return {
Expand Down
27 changes: 22 additions & 5 deletions core/app/c/[communitySlug]/members/getMemberTableColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { ColumnDef } from "@tanstack/react-table";

import type { FormsId, UsersId } from "db/public";
import type { CommunitiesId, FormsId, UsersId } from "db/public";
import { MemberRole, MembershipType } from "db/public";
import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar";
import { Badge } from "ui/badge";
Expand All @@ -16,9 +16,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "ui/dropdown-menu";
import { Info, MoreVertical } from "ui/icon";
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
import { MoreVertical } from "ui/icon";

import { EditMemberDialog } from "~/app/components/Memberships/EditMemberDialog";
import { descriptions } from "~/app/components/Memberships/MemberInviteForm";
import { RemoveMemberButton } from "./RemoveMemberButton";

Expand All @@ -37,7 +37,12 @@ export type TableMember = {
joined: string;
};

export const getMemberTableColumns = () =>
type TableColumnsProps = {
availableForms: { id: FormsId; name: string; isDefault: boolean }[];
communityId: CommunitiesId;
};

export const getMemberTableColumns = (props: TableColumnsProps) =>
[
{
id: "select",
Expand Down Expand Up @@ -151,7 +156,7 @@ export const getMemberTableColumns = () =>
{
id: "actions",
enableHiding: false,
cell: ({ row, table }) => {
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -166,6 +171,18 @@ export const getMemberTableColumns = () =>
<div className="w-full">
<RemoveMemberButton member={row.original} />
</div>
<div className="w-full">
<EditMemberDialog
availableForms={props.availableForms}
member={{
userId: row.original.id,
role: row.original.role,
forms: row.original.forms?.map((form) => form.id) ?? [],
}}
membershipType={MembershipType.community}
membershipTargetId={props.communityId}
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
6 changes: 5 additions & 1 deletion core/app/c/[communitySlug]/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ export default async function Page(props: {
}
>
<div className="m-4">
<MemberTable members={dedupedMembers} />
<MemberTable
members={dedupedMembers}
availableForms={availableForms}
communityId={community.id}
/>
</div>
</ContentLayout>
);
Expand Down
4 changes: 2 additions & 2 deletions core/app/c/[communitySlug]/pubs/[pubId]/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use server";

import type { FormsId, MemberRole, PubsId, UsersId } from "db/public";
import { Capabilities, MembershipType } from "db/public";
import type { FormsId, PubsId, UsersId } from "db/public";
import { Capabilities, MemberRole, MembershipType } from "db/public";

import { db } from "~/kysely/database";
import { isUniqueConstraintError } from "~/kysely/errors";
Expand Down
4 changes: 3 additions & 1 deletion core/app/c/[communitySlug]/pubs/[pubId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";

import { cache } from "react";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { notFound } from "next/navigation";
import { BookOpen, Eye } from "lucide-react";

import type { CommunitiesId, PubsId } from "db/public";
Expand Down Expand Up @@ -332,10 +332,12 @@ export default async function Page(props: {
</div>
<MembersList
members={pub.members}
membershipType={MembershipType.pub}
setRole={setPubMemberRole}
removeMember={removePubMember}
targetId={pubId}
readOnly={!canRemoveMember}
availableForms={availableViewForms}
/>
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions core/app/c/[communitySlug]/stages/manage/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import type {
ActionInstancesId,
CommunitiesId,
FormsId,
MemberRole,
RulesId,
StagesId,
UsersId,
} from "db/public";
import { Capabilities, Event, MembershipType, stagesIdSchema } from "db/public";
import { Capabilities, Event, MemberRole, MembershipType, stagesIdSchema } from "db/public";
import { logger } from "logger";

import type { CreateRuleSchema } from "./components/panel/actionsTab/StagePanelRuleCreator";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ const StagePanelMembersInner = async ({ stageId, user }: PropsInner) => {
</div>
<MembersList
members={members}
membershipType={MembershipType.stage}
setRole={setStageMemberRole}
removeMember={removeStageMember}
targetId={stageId}
readOnly={!canManage}
availableForms={availableForms}
/>
</CardContent>
</Card>
Expand Down
39 changes: 39 additions & 0 deletions core/app/components/Memberships/EditMemberDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { useState } from "react";
import { UserCog } from "lucide-react";

import { Button } from "ui/button";
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "ui/dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
import { cn } from "utils";

import type { MemberEditDialogProps } from "./types";
import { MemberEditForm } from "./MemberEditForm";

export const EditMemberDialog = (props: MemberEditDialogProps & { className?: string }) => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipContent>Edit Member</TooltipContent>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
variant="ghost"
className={cn("inline-flex items-center gap-x-2", props.className)}
>
{!props.minimal && <>Edit member </>}
<UserCog size="16" />
</Button>
</DialogTrigger>
</TooltipTrigger>
</Tooltip>

<DialogContent>
<DialogTitle>Edit Member</DialogTitle>
<MemberEditForm closeForm={() => setOpen(false)} {...props} />
</DialogContent>
</Dialog>
);
};
168 changes: 168 additions & 0 deletions core/app/components/Memberships/MemberEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"use client";

import type { z } from "zod";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import { MemberRole, MembershipType } from "db/public";
import { Button } from "ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "ui/form";
import { Loader2, UserPlus } from "ui/icon";
import { MultiSelect } from "ui/multi-select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";
import { toast } from "ui/use-toast";

import type { MemberEditDialogProps } from "./types";
import { didSucceed, useServerAction } from "~/lib/serverActions";
import { updateMember } from "./actions";
import { memberEditFormSchema } from "./memberInviteFormSchema";

export const descriptions: Record<MembershipType, string> = {
[MembershipType.pub]:
"Select the forms via which this member can edit and view this Pub. If no form is selected, they will only be able to view the Pub, and will only see fields added to the default Pub form for this type.",
[MembershipType.stage]:
"Select the forms via which this member can edit and view Pubs in this stage. If no form is selected, they will only be able to view Pubs in this stage, and will only see fields added to the default Pub form for a each Pub type.",
[MembershipType.community]:
"Selecting forms will give the member the ability to create Pubs in the community using the selected forms. If no forms are added, the contributor will not be able to create any Pubs, and will only be able to see Pubs they have access to either directly or at the stage level.",
};

export const MemberEditForm = ({
member,
closeForm,
membershipTargetId,
membershipType,
availableForms,
}: MemberEditDialogProps & {
closeForm: () => void;
}) => {
const runUpdateMember = useServerAction(updateMember);

const form = useForm<z.infer<typeof memberEditFormSchema>>({
resolver: zodResolver(memberEditFormSchema),
defaultValues: {
role: member.role,
forms: member.forms,
},
mode: "onChange",
});

async function onSubmit(data: z.infer<typeof memberEditFormSchema>) {
const result = await runUpdateMember({
userId: member.userId,
role: data.role,
forms: data.forms,
targetId: membershipTargetId,
targetType: membershipType,
});

if (didSucceed(result)) {
toast({
title: "Success",
description: "Member updated successfully",
});

closeForm();
}
}

const isContributor = form.watch("role") === MemberRole.contributor;

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
{
<>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={MemberRole.admin}>Admin</SelectItem>
<SelectItem value={MemberRole.editor}>
Editor
</SelectItem>
<SelectItem value={MemberRole.contributor}>
Contributor
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the role for this user.
<ul className="list-inside list-disc">
<li>Admins can do anything.</li>
<li>Editors are able to edit most things</li>
<li>
Contributors are only able to see forms and other
public facing content that are linked to them
</li>
</ul>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{isContributor && !!availableForms.length && (
<FormField
control={form.control}
name="forms"
render={({ field }) => {
const description = descriptions[membershipType];
return (
<FormItem>
<FormLabel>Edit/View Access</FormLabel>
<FormControl>
<MultiSelect
{...field}
defaultValue={field.value ?? []}
onValueChange={(newValues) => {
field.onChange(newValues);
}}
options={availableForms.map((f) => ({
label: f.name,
value: f.id,
}))}
placeholder="Select forms"
/>
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
)}
</>
}
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<span className="flex items-center gap-x-2">
<UserPlus size="16" /> Update Member
</span>
)}
</Button>
</form>
</Form>
);
};
Loading
Loading