diff --git a/packages/app/src/app/teams/[team]/settings/layout.tsx b/packages/app/src/app/teams/[team]/settings/layout.tsx index 51908f27..056a8da2 100644 --- a/packages/app/src/app/teams/[team]/settings/layout.tsx +++ b/packages/app/src/app/teams/[team]/settings/layout.tsx @@ -8,8 +8,8 @@ import { PropsWithChildren } from 'react' import { SidebarNav } from '@/components/sidebar-nav' import { Main } from '@/components/main' -export interface NextPageProps { - params: { team: Team } +export interface NextPageProps { + params: { team: TeamSlug } searchParams?: { [key: string]: string | string[] | undefined } } @@ -24,7 +24,7 @@ export default function Layout({ }, { title: 'Members', - href: `/teams/${params.team}/members` + href: `/teams/${params.team}/settings/members` } ] diff --git a/packages/app/src/app/teams/[team]/settings/members/page.tsx b/packages/app/src/app/teams/[team]/settings/members/page.tsx new file mode 100644 index 00000000..428d0942 --- /dev/null +++ b/packages/app/src/app/teams/[team]/settings/members/page.tsx @@ -0,0 +1,26 @@ +import { Separator } from '@/components/ui/separator' +import { SettingsGeneralForm } from '@/components/teams/settings-general-form' +import { PropsWithChildren, Suspense } from 'react' +import { LoadingSpinner } from '@/components/loading-spinner' + +export interface NextPageProps { + params: { team: Team } + searchParams?: { [key: string]: string | string[] | undefined } +} + +export default function Page({ params }: PropsWithChildren) { + return ( + <> +
+

Members

+

+ Manage the members of your team. +

+
+ + }> + + + + ) +} diff --git a/packages/app/src/app/teams/[team]/settings/page.tsx b/packages/app/src/app/teams/[team]/settings/page.tsx index f0dd4eb8..b6884ab0 100644 --- a/packages/app/src/app/teams/[team]/settings/page.tsx +++ b/packages/app/src/app/teams/[team]/settings/page.tsx @@ -8,17 +8,12 @@ export interface NextPageProps { searchParams?: { [key: string]: string | string[] | undefined } } -export default function Page({ - children, - params -}: PropsWithChildren) { +export default function Page({ params }: PropsWithChildren) { return ( <>

General

-

- Application wide settings. -

+

Team Settings

}> diff --git a/packages/app/src/components/team-switcher.tsx b/packages/app/src/components/team-switcher.tsx index 155f6829..dc6f9a34 100644 --- a/packages/app/src/components/team-switcher.tsx +++ b/packages/app/src/components/team-switcher.tsx @@ -80,7 +80,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) { React.useEffect(() => { if (mutation.status === 'success') { - router.push(`/teams/${mutation.data.id}/settings`) + router.push(`/teams/${mutation.data.slug}/settings`) } }, [router, mutation.status, mutation.data]) @@ -210,11 +210,11 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) { name="name" render={({ field }) => ( - +

Name

- + Give it a great name. @@ -222,12 +222,30 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) { )} /> + ( + + Slug + + + + + {`This is the short name used for URLs (e.g. + 'solution-architects', 'order-service')`} + + + + )} + /> + ( - +

Contact email

@@ -247,7 +265,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) { render={({ field }) => (
- +

Description

diff --git a/packages/app/src/components/teams/new-form.schema.tsx b/packages/app/src/components/teams/new-form.schema.tsx index a32502bd..9131e5c8 100644 --- a/packages/app/src/components/teams/new-form.schema.tsx +++ b/packages/app/src/components/teams/new-form.schema.tsx @@ -3,5 +3,6 @@ import { TeamsCreateSchema } from '@/server/routers/schemas/teams' export type NewTeamFormValues = z.infer export const defaultValues: Partial = { - name: '' + name: '', + slug: '' } diff --git a/packages/app/src/components/teams/settings-general-form.schema.tsx b/packages/app/src/components/teams/settings-general-form.schema.tsx index 694b4aac..abbbb92f 100644 --- a/packages/app/src/components/teams/settings-general-form.schema.tsx +++ b/packages/app/src/components/teams/settings-general-form.schema.tsx @@ -1,7 +1,16 @@ import { z } from 'zod' +const reservedSlugs = ['app', 'admin', 'www', 'admin'] + export const settingsGeneralFormSchema = z.object({ name: z.string().min(3).max(256), + slug: z + .string() + .min(3) + .max(128) + .refine(slug => !reservedSlugs.includes(slug), { + message: "Slug can't be one of reserved slugs." + }), description: z.string().min(10).max(2048).optional() }) diff --git a/packages/app/src/components/teams/settings-general-form.tsx b/packages/app/src/components/teams/settings-general-form.tsx index d8a24e96..fa2e06d4 100644 --- a/packages/app/src/components/teams/settings-general-form.tsx +++ b/packages/app/src/components/teams/settings-general-form.tsx @@ -13,7 +13,6 @@ import { FormMessage } from '@/components/ui/form' import { Input } from '@/components/ui/input' -import { toast } from '@/components/ui/use-toast' import { use } from 'react' import { SettingGeneralFormValues, @@ -22,31 +21,21 @@ import { import { api } from '@/trpc/client' export function SettingsGeneralForm({ teamId }: { teamId: string }) { - const team = use(api.teams.get.query(teamId)) + const team = use(api.teams.getByName.query(teamId)) const form = useForm({ resolver: zodResolver(settingsGeneralFormSchema), defaultValues: { name: team?.name, + slug: team?.slug, description: team?.description }, mode: 'onChange' }) - function onSubmit(data: SettingGeneralFormValues) { - toast({ - title: 'You submitted the following values:', - description: ( -
-          {JSON.stringify(data, null, 2)}
-        
- ) - }) - } - return (
- + Name - + This is the name of the team. )} /> + ( + + Slug + + + + + {`This is the short name used for URLs (e.g. + 'solution-architects', 'order-service')`} + + + + )} + /> Description - + This a brief description of the application instance. @@ -77,7 +87,9 @@ export function SettingsGeneralForm({ teamId }: { teamId: string }) { )} /> - + ) diff --git a/packages/app/src/db/migrations/20231120145740-added_new_auth.js b/packages/app/src/db/migrations/20231120145740-added_new_auth.js index 66b89c51..735c72c5 100644 --- a/packages/app/src/db/migrations/20231120145740-added_new_auth.js +++ b/packages/app/src/db/migrations/20231120145740-added_new_auth.js @@ -123,6 +123,10 @@ module.exports = { name: { type: Sequelize.STRING }, + slug: { + type: Sequelize.STRING, + unique: true + }, description: { type: Sequelize.STRING }, diff --git a/packages/app/src/db/models/teams.ts b/packages/app/src/db/models/teams.ts index 807a490d..abf68288 100644 --- a/packages/app/src/db/models/teams.ts +++ b/packages/app/src/db/models/teams.ts @@ -11,6 +11,7 @@ import { Min, Max, AllowNull, + Unique, Default } from 'sequelize-typescript' import { Optional } from 'sequelize' @@ -18,6 +19,7 @@ import { Optional } from 'sequelize' export interface TeamAttributes { id: string name: string + slug: string description?: string createdAt?: Date updatedAt?: Date @@ -39,10 +41,17 @@ export class Team extends Model { @NotEmpty @Min(3) - @Max(256) + @Max(128) @Column declare name: string + @NotEmpty + @Unique + @Min(3) + @Max(128) + @Column + declare slug: string + @NotEmpty @Min(12) @Max(2048) diff --git a/packages/app/src/db/schemas/teams.ts b/packages/app/src/db/schemas/teams.ts index be843227..59eb0291 100644 --- a/packages/app/src/db/schemas/teams.ts +++ b/packages/app/src/db/schemas/teams.ts @@ -1,5 +1,7 @@ import { z } from 'zod' +const reservedSlugs = ['app', 'admin', 'www', 'admin'] + export const FindAndCountTeamsSchema = z.object({ limit: z.number().min(0).max(100).default(10), offset: z.number().min(0).default(0) @@ -7,9 +9,23 @@ export const FindAndCountTeamsSchema = z.object({ export const FindOneTeamSchema = z.string().uuid() export const CreateTeamSchema = z.object({ name: z.string().min(3).max(128), + slug: z + .string() + .min(3) + .max(128) + .refine(slug => !reservedSlugs.includes(slug), { + message: "Slug can't be one of reserved slugs" + }), userId: z.string().uuid(), description: z.string().min(3).max(255).optional(), contactEmail: z.string().email().optional() }) - export type CreateTeamSchema = z.infer + +export const FindOneTeamByNameSlug = z + .string() + .trim() + .toLowerCase() + .min(3) + .max(128) +export type FindOneTeamByNameSlug = z.infer diff --git a/packages/app/src/db/services/teams.ts b/packages/app/src/db/services/teams.ts index 2b89bbd9..5a265894 100644 --- a/packages/app/src/db/services/teams.ts +++ b/packages/app/src/db/services/teams.ts @@ -1,6 +1,7 @@ import { Team } from '@/db/models/teams' import { User } from '@/db/models/users' import { FindAndCountTeamsSchema, FindOneTeamSchema } from '../schemas/teams' +import type { FindOneTeamByNameSlug } from '../schemas/teams' import type { CreateTeamSchema } from '../schemas/teams' import { z } from 'zod' import sequelize from '@/db/config/config' @@ -27,6 +28,11 @@ export const findOneTeam = async (opts: z.infer) => where: { id: opts } }) +export const findOneTeamBySlug = async (opts: FindOneTeamByNameSlug) => + await Team.findOne({ + where: { slug: opts } + }) + export const findAndCountTeams = async ( opts: z.infer ) => diff --git a/packages/app/src/server/routers/actions/teams.ts b/packages/app/src/server/routers/actions/teams.ts index 82f76dea..57d5c5a7 100644 --- a/packages/app/src/server/routers/actions/teams.ts +++ b/packages/app/src/server/routers/actions/teams.ts @@ -1,10 +1,14 @@ import { protectedProcedure } from '../../trpc' import { - TeamsCreateSchema, TeamsGetSchema, - TeamsListSchema + TeamsListSchema, + TeamsGetBySlugSchema } from '../schemas/teams' -import { findAndCountTeams, findOneTeam } from '@/db/services/teams' +import { + findAndCountTeams, + findOneTeam, + findOneTeamBySlug +} from '@/db/services/teams' import { router } from '@/server/trpc' export const listTeams = protectedProcedure @@ -15,13 +19,17 @@ export const getTeam = protectedProcedure .input(TeamsGetSchema) .query(async opts => await findOneTeam(opts.input)) -export const addTeam = protectedProcedure.input(TeamsCreateSchema).mutation( - async opts => {} - // await createTeam({ ...opts.input, userId: opts.ctx.session.user.id }) -) +export const getTeamBySlug = protectedProcedure + .input(TeamsGetBySlugSchema) + .query(async opts => await findOneTeamBySlug(opts.input)) + +// export const addTeam = protectedProcedure +// .input(TeamsCreateSchema) +// .mutation(async opts => await createTeam({})) export const teamsRouter = router({ list: listTeams, - add: addTeam, - get: getTeam + // add: addTeam, + get: getTeam, + getByName: getTeamBySlug }) diff --git a/packages/app/src/server/routers/schemas/teams.ts b/packages/app/src/server/routers/schemas/teams.ts index f3304fe4..b51c00bc 100644 --- a/packages/app/src/server/routers/schemas/teams.ts +++ b/packages/app/src/server/routers/schemas/teams.ts @@ -5,6 +5,16 @@ export const TeamsListSchema = PaginationSchema export const TeamsGetSchema = z.string().uuid() export const TeamsCreateSchema = z.object({ name: z.string().min(3).max(128), + slug: z.string().trim().toLowerCase().min(3).max(128).default(''), description: z.string().min(10).max(256).optional(), contactEmail: z.string().email().optional() }) + +export const TeamsGetBySlugSchema = z + .string() + .trim() + .toLowerCase() + .min(3) + .max(128) + .default('') +export type TeamsGetBySlugSchema = z.infer