diff --git a/drizzle/0005_heavy_night_nurse.sql b/drizzle/0005_heavy_night_nurse.sql new file mode 100644 index 0000000..f9ee0d7 --- /dev/null +++ b/drizzle/0005_heavy_night_nurse.sql @@ -0,0 +1 @@ +ALTER TABLE `team` DROP COLUMN `display_name`; \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..43744c9 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,932 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a5890c90-4c0c-4857-97c0-2ab6d7279226", + "prevId": "067296a2-71a3-4095-a021-ca14088381ff", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "database": { + "name": "database", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'healthy'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'S3'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bucket_name": { + "name": "bucket_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ak_ciphertext": { + "name": "ak_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ak_iv": { + "name": "ak_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ak_tag": { + "name": "ak_tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sk_ciphertext": { + "name": "sk_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sk_iv": { + "name": "sk_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sk_tag": { + "name": "sk_tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "database_team_id_team_id_fk": { + "name": "database_team_id_team_id_fk", + "tableFrom": "database", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "database_created_by_user_id_fk": { + "name": "database_created_by_user_id_fk", + "tableFrom": "database", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team": { + "name": "team", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "team_slug_unique": { + "name": "team_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "team_owner_id_user_id_fk": { + "name": "team_owner_id_user_id_fk", + "tableFrom": "team", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_member": { + "name": "team_member", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_member_user_id_team_id_pk": { + "columns": [ + "user_id", + "team_id" + ], + "name": "team_member_user_id_team_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roblox_credentials": { + "name": "roblox_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'healthy'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_ciphertext": { + "name": "key_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_iv": { + "name": "key_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_tag": { + "name": "key_tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_owner_roblox_id": { + "name": "key_owner_roblox_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "roblox_credentials_team_id_team_id_fk": { + "name": "roblox_credentials_team_id_team_id_fk", + "tableFrom": "roblox_credentials", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "roblox_credentials_created_by_user_id_fk": { + "name": "roblox_credentials_created_by_user_id_fk", + "tableFrom": "roblox_credentials", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project": { + "name": "project", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "project_team_id_slug_unique": { + "name": "project_team_id_slug_unique", + "columns": [ + "team_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_team_id_team_id_fk": { + "name": "project_team_id_team_id_fk", + "tableFrom": "project", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d63b919..afb887f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1775383387755, "tag": "0004_kind_vision", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1775474479580, + "tag": "0005_heavy_night_nurse", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts index d13f35e..7bccfb6 100644 --- a/src/app/api/teams/[teamId]/route.ts +++ b/src/app/api/teams/[teamId]/route.ts @@ -1,4 +1,5 @@ import { auth } from "@/src/lib/auth"; +import { RenameTeamSchema } from "@/src/lib/types/team-types"; import { ErrorToNextResponse } from "@/src/lib/utils/api-utils"; import { TeamService } from "@/src/services/TeamService"; import { headers } from "next/headers"; @@ -35,10 +36,6 @@ export async function DELETE(_: Request, context: Context) { } } -const PatchSchema = z.object({ - name: z.string().min(3).max(32).optional(), - displayName: z.string().min(3).max(32).optional(), -}); export async function PATCH(req: Request, context: Context) { const params = await context.params; const teamId = params.teamId; @@ -66,12 +63,12 @@ export async function PATCH(req: Request, context: Context) { } try { - const validatedData = PatchSchema.parse(body); + const { name } = RenameTeamSchema.parse(body); const result = await TeamService.ChangeTeamName( session.user.id, teamId, - validatedData, + name, ); return NextResponse.json(result); } catch (error) { diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 38f84a5..ca3b356 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -1,4 +1,5 @@ import { auth } from "@/src/lib/auth"; +import { CreateTeamSchema } from "@/src/lib/types/team-types"; import { ErrorToNextResponse } from "@/src/lib/utils/api-utils"; import { TeamService } from "@/src/services/TeamService"; import { headers } from "next/headers"; @@ -22,9 +23,6 @@ export async function GET(req: Request) { } } -const PostSchema = z.object({ - name: z.string().min(3).max(32), -}); export async function POST(req: Request) { const session = await auth.api.getSession({ headers: await headers(), @@ -45,7 +43,7 @@ export async function POST(req: Request) { } try { - const validatedData = PostSchema.parse(body); + const validatedData = CreateTeamSchema.parse(body); const newTeam = await TeamService.CreateTeam( session.user.id, validatedData.name, diff --git a/src/app/dashboard/[teamSlug]/settings/TeamDisplayName.tsx b/src/app/dashboard/[teamSlug]/settings/TeamDisplayName.tsx deleted file mode 100644 index 10de5ff..0000000 --- a/src/app/dashboard/[teamSlug]/settings/TeamDisplayName.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { Button } from "@/src/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/src/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/src/components/ui/form"; -import { Input } from "@/src/components/ui/input"; -import { Separator } from "@/src/components/ui/separator"; -import { Skeleton } from "@/src/components/ui/skeleton"; -import { useTeam, useTeamMutations } from "@/src/hooks/useTeam"; -import { hasPermission } from "@/src/lib/utils/team-utils"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import z from "zod"; - -const CardComponent = ({ children }: React.PropsWithChildren) => ( - - - Team Display Name - - This is the name used for your team across the dashboard and is - independent from your team's URL - - - - {children} - -); - -export default function TeamDisplayName() { - const { data: team, isLoading, refetch } = useTeam(); - const { renameTeam } = useTeamMutations(); - - const formSchema = z - .object({ - displayName: z - .string() - .min(3, { error: "Display name must be at least 3 characters" }) - .max(32, { error: "Display name must be 32 characters at maximum" }), - }) - .refine((values) => values.displayName !== team?.displayName, { - error: - "Given display name must be different than your current display name", - }); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - displayName: "", - }, - }); - - const handleChangeDisplayName = (displayName: string) => { - const id = toast.loading("Updating team display name..."); - renameTeam - .mutateAsync({ teamId: team!.id, payload: { displayName } }) - .then(async () => { - toast.success("Successfully updated team display name!", { - id, - }); - await refetch(); - form.reset(); - }) - .catch((error) => { - if (error instanceof Error) { - toast.error(error.message, { id }); - } else { - toast.error( - "An unexpected error happened while updating team display name", - { - id, - }, - ); - } - }); - }; - - if (isLoading) { - return ( - -
- - -
-
- ); - } - - const disabled = !hasPermission(team?.role, "ChangeTeamName"); - - return ( - -
- - handleChangeDisplayName(displayName), - )} - > - ( - - - - - - - )} - /> - - - -
- ); -} diff --git a/src/app/dashboard/[teamSlug]/settings/TeamName.tsx b/src/app/dashboard/[teamSlug]/settings/TeamName.tsx index d7c659d..3e46367 100644 --- a/src/app/dashboard/[teamSlug]/settings/TeamName.tsx +++ b/src/app/dashboard/[teamSlug]/settings/TeamName.tsx @@ -7,19 +7,14 @@ import { CardHeader, CardTitle, } from "@/src/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/src/components/ui/form"; +import { Form, FormControl, FormField, FormItem, FormMessage } from "@/src/components/ui/form"; import { Input } from "@/src/components/ui/input"; import { Separator } from "@/src/components/ui/separator"; import { Skeleton } from "@/src/components/ui/skeleton"; import { useTeam, useTeamMutations } from "@/src/hooks/useTeam"; import { hasPermission } from "@/src/lib/utils/team-utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -29,9 +24,7 @@ const CardComponent = ({ children }: React.PropsWithChildren) => ( Team Name - - This is the name used to generate the URL of your team. - + This is the name used to generate the URL of your team. {children} @@ -39,7 +32,8 @@ const CardComponent = ({ children }: React.PropsWithChildren) => ( ); export default function TeamName() { - const { data: team, isLoading, refetch } = useTeam(); + const router = useRouter(); + const { data: team, isLoading } = useTeam(); const [open, setIsOpen] = useState(false); const { renameTeam } = useTeamMutations(); @@ -51,8 +45,7 @@ export default function TeamName() { .max(32, { error: "Name must be at most 32 characters" }), }) .refine((values) => values.name !== team?.name, { - error: - "Given display name must be different than your current display name", + error: "Given display name must be different than your current display name", }); const form = useForm({ @@ -65,11 +58,12 @@ export default function TeamName() { const handleChangeName = (name: string) => { const id = toast.loading("Updating team name..."); renameTeam - .mutateAsync({ teamId: team!.id, payload: { name } }) - .then(async () => { + .mutateAsync({ teamId: team!.id, newName: name }) + .then(async (newTeam) => { toast.success("Successfully updated team name!", { id, }); + router.replace(`/dashboard/${newTeam.slug}/settings`); }) .catch((error) => { if (error instanceof Error) { @@ -108,11 +102,7 @@ export default function TeamName() { render={({ field }) => ( - + diff --git a/src/app/dashboard/[teamSlug]/settings/page.tsx b/src/app/dashboard/[teamSlug]/settings/page.tsx index 5213d41..329fa64 100644 --- a/src/app/dashboard/[teamSlug]/settings/page.tsx +++ b/src/app/dashboard/[teamSlug]/settings/page.tsx @@ -1,15 +1,11 @@ "use client"; import TeamDangerZone from "./TeamDangerZone"; -import TeamDisplayName from "./TeamDisplayName"; import TeamName from "./TeamName"; export default function Page() { return ( <> - - Team Settings - - + Team Settings diff --git a/src/app/dashboard/components/CreateTeamDialog.tsx b/src/app/dashboard/components/CreateTeamDialog.tsx index 3cd0a8a..5ce1f4e 100644 --- a/src/app/dashboard/components/CreateTeamDialog.tsx +++ b/src/app/dashboard/components/CreateTeamDialog.tsx @@ -1,15 +1,10 @@ import FormDialog from "@/src/components/FormDialog"; -import { - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/src/components/ui/form"; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/src/components/ui/form"; import { Input } from "@/src/components/ui/input"; import { useTeamMutations } from "@/src/hooks/useTeam"; +import { CreateTeamSchema } from "@/src/lib/types/team-types"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -21,10 +16,7 @@ interface CreateTeamDialogProps { setIsOpen: (open: boolean) => void; } -export default function CreateTeamDialog({ - open, - setIsOpen, -}: CreateTeamDialogProps) { +export default function CreateTeamDialog({ open, setIsOpen }: CreateTeamDialogProps) { const { createTeam } = useTeamMutations(); const handleTeamCreation = async (name: string) => { @@ -47,12 +39,6 @@ export default function CreateTeamDialog({ }); }; - const CreateTeamSchema = z.object({ - name: z - .string() - .min(3, { error: "Team name must be at least 3 characters" }) - .max(32, { error: "Team name must be at most 32 characters" }), - }); const form = useForm({ resolver: zodResolver(CreateTeamSchema), defaultValues: { diff --git a/src/app/dashboard/components/DashboardNavbar.tsx b/src/app/dashboard/components/DashboardNavbar.tsx index da0f365..0bfce87 100644 --- a/src/app/dashboard/components/DashboardNavbar.tsx +++ b/src/app/dashboard/components/DashboardNavbar.tsx @@ -105,7 +105,7 @@ export default function DashboardNavbar() { className="flex gap-1 items-center" > - {team.displayName} + {team.name} @@ -113,7 +113,7 @@ export default function DashboardNavbar() { label="Switch Team" items={teams} activeItem={team} - getLabel={(t) => t.displayName} + getLabel={(t) => t.name} getHref={(t) => project ? `/dashboard/${t.slug}` : `/dashboard/${t.slug}/${afterTeamPath}` } diff --git a/src/app/dashboard/components/TeamColumn.tsx b/src/app/dashboard/components/TeamColumn.tsx index 097aabf..eb1b91f 100644 --- a/src/app/dashboard/components/TeamColumn.tsx +++ b/src/app/dashboard/components/TeamColumn.tsx @@ -21,19 +21,8 @@ import { toast } from "sonner"; export const teamColumns: ColumnDef[] = [ { - id: "team_details", - header: "Team", - accessorFn: (row) => `${row.displayName} ${row.name}`, - cell: (info) => ( -
- - {info.row.original.displayName} - - - @{info.row.original.name} - -
- ), + accessorKey: "name", + header: "Name", }, { accessorKey: "role", @@ -55,13 +44,8 @@ export const teamColumns: ColumnDef[] = [ cell: ({ row }) => { const { data, isPending } = authClient.useSession(); const { mutateAsync: leaveTeam } = useMutation({ - mutationFn: ({ - teamId, - memberId, - }: { - teamId: string; - memberId: string; - }) => TeamController.removeMember(teamId, memberId), + mutationFn: ({ teamId, memberId }: { teamId: string; memberId: string }) => + TeamController.removeMember(teamId, memberId), onSuccess: (_, { teamId }) => { queryClient.setQueryData(["teams"], (prevData) => prevData ? prevData.filter((team) => team.id !== teamId) : [], @@ -85,16 +69,14 @@ export const teamColumns: ColumnDef[] = [ - navigator.clipboard.writeText(team.id)} - > + navigator.clipboard.writeText(team.id)}> Copy Team ID { const toastId = toast.loading("Leaving team..."); try { @@ -121,10 +103,7 @@ export const teamColumns: ColumnDef[] = [ submitButtonVariant={"destructive"} cancelButtonVariant={"outline"} trigger={ - e.preventDefault()} - > + e.preventDefault()}> Leave Team diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 654299c..4777c6e 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -34,7 +34,7 @@ export default function Page() { columns={teamColumns} emptyString="No Teams" searchBoxPlaceholder="Search teams" - searchBoxTarget="team_details" + searchBoxTarget="name" loading={isLoading} loadingString="Loading..." actionComponent={} diff --git a/src/controllers/TeamController.ts b/src/controllers/TeamController.ts index 68a2c09..a34b0ee 100644 --- a/src/controllers/TeamController.ts +++ b/src/controllers/TeamController.ts @@ -16,13 +16,10 @@ export const TeamController = { resolve: (slug: string) => fetcher(`/api/teams/resolve-slug/${slug}`), - changeName: ( - teamId: string, - newName: { name?: string; displayName?: string }, - ) => - fetcher(`/api/teams/${teamId}`, { + changeName: (teamId: string, newName: string) => + fetcher(`/api/teams/${teamId}`, { method: "PATCH", - body: JSON.stringify(newName), + body: JSON.stringify({ name: newName }), }), removeMember: (teamId: string, memberId: string) => diff --git a/src/db/schema/team.ts b/src/db/schema/team.ts index 2fe1fbc..1fc03e7 100644 --- a/src/db/schema/team.ts +++ b/src/db/schema/team.ts @@ -4,10 +4,9 @@ import { user } from "./user"; export const team = sqliteTable("team", { id: text("id").primaryKey(), - slug: text("slug").notNull().unique(), - displayName: text("display_name").notNull(), name: text("name").notNull(), + slug: text("slug").notNull().unique(), ownerId: text("owner_id") .notNull() diff --git a/src/hooks/useTeam.ts b/src/hooks/useTeam.ts index c6796e7..8ce1885 100644 --- a/src/hooks/useTeam.ts +++ b/src/hooks/useTeam.ts @@ -36,7 +36,6 @@ export function useTeams() { export function useTeamMutations() { const queryClient = useQueryClient(); - const router = useRouter(); const createTeam = useMutation({ mutationFn: (name: string) => TeamController.create(name), @@ -64,46 +63,15 @@ export function useTeamMutations() { }); const renameTeam = useMutation({ - mutationFn: ({ - teamId, - payload, - }: { - teamId: string; - payload: { name?: string; displayName?: string }; - }) => TeamController.changeName(teamId, payload), + mutationFn: ({ teamId, newName }: { teamId: string; newName: string }) => + TeamController.changeName(teamId, newName), onSuccess: (newTeam, variables) => { - const cachedTeam = queryClient.getQueryData(["teams"]); - const oldTeam = cachedTeam?.find((team) => team.id === variables.teamId); - const oldSlug = oldTeam?.slug; - - // Update the teams list queryClient.setQueryData(["teams"], (prevData) => { - if (!prevData) return prevData; - return prevData.map((team) => - team.id === newTeam.id ? { ...team, ...newTeam } : team, - ); + if (!prevData) return [newTeam]; + return prevData.map((t) => (t.id == newTeam.id ? newTeam : t)); }); - const pathname = window.location.pathname; - const relativePath = pathname - .split("/dashboard/")[1] - .split("/") - .slice(1) - .join("/"); - router.replace(`/dashboard/${newTeam.slug}/${relativePath}`); - - if (oldSlug && oldTeam && oldSlug !== newTeam.slug) { - queryClient.removeQueries({ queryKey: ["team", oldSlug] }); - queryClient.setQueryData(["team", newTeam.slug], { - ...oldTeam, - ...newTeam, - }); - } else if (oldSlug) { - queryClient.setQueryData(["team", oldSlug], (oldTeam) => { - if (!oldTeam) return oldTeam; - return { ...oldTeam, ...newTeam }; - }); - } + queryClient.setQueryData(["team", newTeam.slug], () => newTeam); }, }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8444221..589fcf9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -103,13 +103,7 @@ export const auth = betterAuth({ user: { create: { after: async (user) => { - const team = await TeamService.CreateTeam( - user.id, - `user-${user.id.slice(-6)}'s team`, - ); - await TeamService.ChangeTeamName(user.id, team.id, { - displayName: "Personal Team", - }); + await TeamService.CreateTeam(user.id, `${user.email}'s Teams`); }, }, }, diff --git a/src/lib/types/team-types.ts b/src/lib/types/team-types.ts index 3332365..a02c65e 100644 --- a/src/lib/types/team-types.ts +++ b/src/lib/types/team-types.ts @@ -1,5 +1,6 @@ import { user, team_member, team } from "@/src/db/schema"; import { InferDrizzleSelect } from "../utils"; +import z from "zod"; export type TeamRole = typeof team_member.$inferSelect.role; @@ -15,7 +16,6 @@ export type TeamMember = InferDrizzleSelect; export const TeamSelect = { id: team.id, name: team.name, - displayName: team.displayName, slug: team.slug, ownerId: team.ownerId, createdAt: team.createdAt, @@ -28,3 +28,18 @@ export const UserTeamSelect = { role: team_member.role, }; export type UserTeam = InferDrizzleSelect; + + +export const CreateTeamSchema = z.object({ + name: z + .string() + .min(3, { error: "Team name must be at least 3 characters" }) + .max(32, { error: "Team name must be at most 32 characters" }), +}); + +export const RenameTeamSchema = z.object({ + name: z + .string() + .min(3, { error: "Name must be at least 3 characters" }) + .max(32, { error: "Name must be at most 32 characters" }), +}); diff --git a/src/services/TeamService.ts b/src/services/TeamService.ts index 23b4830..fa4a944 100644 --- a/src/services/TeamService.ts +++ b/src/services/TeamService.ts @@ -119,7 +119,6 @@ export const TeamService = { .values({ id: teamId, name: teamName, - displayName: teamName, slug, ownerId: actorId, }) @@ -225,40 +224,37 @@ export const TeamService = { async ChangeTeamName( actorId: string, teamId: string, - newName: { displayName?: string; name?: string }, - ): Promise { + newName: string, + ): Promise { const role = await this.GetTeamUserRole(actorId, teamId); if (!hasPermission(role, "ChangeTeamName")) { throw AccessDenied; } - const updatePayload: Partial = {}; - - if (newName.displayName) { - updatePayload.displayName = newName.displayName; - } - - if (newName.name) { - updatePayload.name = newName.name; - } - - if (Object.keys(updatePayload).length < 1) { - return; - } - for (let i = 0; i < 3; i++) { - if (updatePayload.name) { - updatePayload.slug = this.CreateSlugFromName(updatePayload.name); + let slug; + try { + slug = this.CreateSlugFromName(newName); + } catch (error) { + throw error; } + try { - const [result] = await db + await db .update(team) - .set(updatePayload) - .where(eq(team.id, teamId)) - .returning(TeamSelect); + .set({ slug, name: newName }) + .where(eq(team.id, teamId)); + + const [result] = await db + .select(UserTeamSelect) + .from(team) + .innerJoin(team_member, eq(team.id, team_member.teamId)) + .where(and(eq(team.id, teamId), eq(team_member.userId, actorId))); + return result; } catch (error) { + console.log(error); if (error instanceof Error) { if (error.message.includes("SQLITE_CONSTRAINT_UNIQUE")) { continue; @@ -267,6 +263,7 @@ export const TeamService = { throw DatabaseError; } } + throw InvalidSlug; }, async GetTeamBySlug(