diff --git a/src/entities/team/api/http.ts b/src/entities/team/api/http.ts index 3bbac48..af1fdf5 100644 --- a/src/entities/team/api/http.ts +++ b/src/entities/team/api/http.ts @@ -60,11 +60,11 @@ export class TeamHttp { } static getInvitations(slug: string, signal?: AbortSignal) { - return api({ + return api({ url: `/teams/${slug}/invitations`, method: 'GET', contracts: { - response: STeam.TeamInvitationResponse.array(), + response: STeam.TeamInvitationListResponse, }, signal, }); @@ -126,11 +126,11 @@ export class TeamHttp { } static getMembers(slug: string, signal?: AbortSignal) { - return api({ + return api({ url: `/teams/${slug}/members`, method: 'GET', contracts: { - response: STeam.TeamMemberResponse.array(), + response: STeam.TeamMemberListResponse, }, signal, }); @@ -157,16 +157,4 @@ export class TeamHttp { }, }); } - - static syncTags(slug: string, data: TTeam.SyncTagsBody) { - return api({ - url: `/teams/${slug}/tags`, - method: 'PUT', - data, - contracts: { - body: STeam.SyncTagsBody, - response: STeam.ActionResponse, - }, - }); - } } diff --git a/src/entities/team/config/statuses.ts b/src/entities/team/config/statuses.ts index 710c4fb..3c15654 100644 --- a/src/entities/team/config/statuses.ts +++ b/src/entities/team/config/statuses.ts @@ -2,10 +2,10 @@ import type { MemberStatus } from '../model/types'; export const STATUS_LABELS: Record = { active: 'Активен', - banned: 'Заблокирован', - inactive: 'Неактивен', + blocked: 'Заблокирован', + pending: 'Неактивен', } as const; export const MEMBER_STATUSES = [ - ...new Set(['active', 'inactive', 'banned']), + ...new Set(['active', 'pending', 'blocked']), ] as const; diff --git a/src/entities/team/model/schemas.ts b/src/entities/team/model/schemas.ts index 8105ba0..7946093 100644 --- a/src/entities/team/model/schemas.ts +++ b/src/entities/team/model/schemas.ts @@ -1,13 +1,13 @@ -import { DateTimeString, GlobalSuccess } from 'shared/api'; +import { DateTimeString, GlobalSuccess, PaginatedResponseSchema } from 'shared/api'; import { z } from 'zod/v4'; import { MAX_SLUG_LENGTH, MIN_SLUG_LENGTH } from './const'; export const TeamAvatarSchema = z .object({ - small: z.string().url(), - medium: z.string().url(), - large: z.string().url(), - original: z.string().url(), + small: z.url(), + medium: z.url(), + large: z.url(), + original: z.url(), }) .nullish(); @@ -22,8 +22,8 @@ export const TeamRole = z.enum([ export const MemberStatus = z.enum([ 'active', // Полноценный участник - 'banned', // Заблокирован не может вернуться по инвайту - 'inactive', // Доступ закрыт, но запись сохранена + 'blocked', // Заблокирован не может вернуться по инвайту + 'pending', ]); export const CreateTeamBody = z.object({ @@ -36,7 +36,7 @@ export const CreateTeamBody = z.object({ .string() .min(1, 'Добавьте описание команды') .min(10, 'Описание должно содержать не менее 10 символов') - .max(256, 'Описание не может быть длиннее 256 символов'), + .max(500, 'Описание не может быть длиннее 500 символов'), slug: z .string() .optional() @@ -54,19 +54,6 @@ export const CreateTeamBody = z.object({ ) .optional() ), - tags: z - .array(z.string()) - .optional() - .superRefine((items, ctx) => { - if (!items) return; - const hasDuplicates = new Set(items.map((item) => item.toLowerCase())).size !== items.length; - if (hasDuplicates) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Теги в списке не должны повторяться', - }); - } - }), }); export const UpdateTeamBody = CreateTeamBody.partial().refine( @@ -87,7 +74,7 @@ export const TeamDetailsResponse = z.object({ name: z.string(), slug: z.string(), description: z.string().nullable(), - avatar: TeamAvatarSchema, + avatarUrl: z.string().nullable(), coverUrl: z.string().nullable(), ownerId: z.string().nullable(), createdAt: DateTimeString, @@ -99,7 +86,7 @@ export const TeamInvitationResponse = z.object({ code: z.string(), teamId: z.string(), teamName: z.string(), - avatar: TeamAvatarSchema, + teamAvatar: z.string().nullable(), email: z.email(), role: TeamRole, inviterId: z.string(), @@ -108,6 +95,8 @@ export const TeamInvitationResponse = z.object({ expiresAt: DateTimeString, }); +export const TeamInvitationListResponse = PaginatedResponseSchema(TeamInvitationResponse); + export const InviteMemberBody = z.object({ email: z.email(), role: TeamRole, @@ -121,8 +110,6 @@ export const TeamMemberResponse = z.object({ id: z.string(), role: TeamRole, status: MemberStatus, - email: z.email(), - middleName: z.string().nullable(), fullName: z.string(), firstName: z.string(), lastName: z.string(), @@ -131,6 +118,8 @@ export const TeamMemberResponse = z.object({ joinedAt: DateTimeString, }); +export const TeamMemberListResponse = PaginatedResponseSchema(TeamMemberResponse); + export const UpdateMemberBody = z .object({ role: TeamRole.optional(), @@ -141,20 +130,4 @@ export const UpdateMemberBody = z abort: true, }); -export const SyncTagsBody = z.object({ - tags: z - .array(z.string()) - .min(1, 'Список тегов не может быть пустым') - .max(15, 'Нельзя добавить более 15 тегов за раз') - .superRefine((items, ctx) => { - const hasDuplicates = new Set(items.map((item) => item.toLowerCase())).size !== items.length; - if (hasDuplicates) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Теги в списке не должны повторяться (регистр не важен)', - }); - } - }), -}); - export const ActionResponse = GlobalSuccess; diff --git a/src/entities/team/model/types.ts b/src/entities/team/model/types.ts index 826761f..0fca72e 100644 --- a/src/entities/team/model/types.ts +++ b/src/entities/team/model/types.ts @@ -12,10 +12,12 @@ export type CheckSlugResponse = z.infer; export type TeamDetailsResponse = z.infer; export type TeamInvitationResponse = z.infer; +export type TeamInvitationListResponse = z.infer; + export type TeamMemberResponse = z.infer; +export type TeamMemberListResponse = z.infer; export type InviteMemberBody = z.infer; export type UpdateInvitationBody = z.infer; export type UpdateMemberBody = z.infer; -export type SyncTagsBody = z.infer; export type ActionResponse = z.infer; diff --git a/src/entities/user/api/http.ts b/src/entities/user/api/http.ts index cf9e8ce..13ead9f 100644 --- a/src/entities/user/api/http.ts +++ b/src/entities/user/api/http.ts @@ -60,11 +60,11 @@ export class UserHttp { } static getMyInvitations(signal?: AbortSignal) { - return api({ + return api({ url: '/users/me/invites', method: 'GET', contracts: { - response: SUser.UserInvitationResponse.array(), + response: SUser.UserInvitationListResponse, }, signal, }); diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts index 274e978..f2851f3 100644 --- a/src/entities/user/model/schemas.ts +++ b/src/entities/user/model/schemas.ts @@ -1,4 +1,5 @@ import { DateTimeString, GlobalSuccess } from 'shared/api'; +import { PaginatedResponseSchema } from 'shared/api/'; import { z } from 'zod/v4'; export const UserAvatarSchema = z @@ -89,19 +90,7 @@ export const UserTeamResponse = z.object({ permissions: TeamPermissions, }); -export const UserTeamsListMeta = z.object({ - hasNextPage: z.boolean(), - hasPrevPage: z.boolean(), - total: z.number(), - totalPages: z.number(), - page: z.number(), - limit: z.number(), -}); - -export const UserTeamsListResponse = z.object({ - items: UserTeamResponse.array(), - meta: UserTeamsListMeta, -}); +export const UserTeamsListResponse = PaginatedResponseSchema(UserTeamResponse); export const UserInvitationResponse = z.object({ code: z.string(), @@ -111,3 +100,5 @@ export const UserInvitationResponse = z.object({ inviterName: z.string(), expiresAt: DateTimeString, }); + +export const UserInvitationListResponse = PaginatedResponseSchema(UserInvitationResponse); diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts index d56957e..32d48bd 100644 --- a/src/entities/user/model/types.ts +++ b/src/entities/user/model/types.ts @@ -7,6 +7,7 @@ export type NotificationsUpdateResponse = z.infer; export type ProfileUpdateResponse = z.infer; export type UserTeamResponse = z.infer; -export type UserTeamsListMeta = z.infer; export type UserTeamsListResponse = z.infer; +export type UserInvitationListResponse = z.infer; + export type UserInvitationResponse = z.infer; diff --git a/src/features/teams/create/model/useCreateTeamForm.ts b/src/features/teams/create/model/useCreateTeamForm.ts index a8f8f13..89bfb2a 100644 --- a/src/features/teams/create/model/useCreateTeamForm.ts +++ b/src/features/teams/create/model/useCreateTeamForm.ts @@ -37,7 +37,6 @@ export function useCreateTeamForm(mutateOptions: UseCreateTeamOptions = {}) { name: data.name.trim(), description: data.description.trim(), ...(data.slug?.trim() ? { slug: data.slug.trim() } : {}), - tags: [''], //todo }; createTeam.mutate(body); diff --git a/src/pages/profile/ui/teams-page/Invitations.tsx b/src/pages/profile/ui/teams-page/Invitations.tsx index aa31eb0..53a1d26 100644 --- a/src/pages/profile/ui/teams-page/Invitations.tsx +++ b/src/pages/profile/ui/teams-page/Invitations.tsx @@ -33,10 +33,10 @@ export function Invitations() { return (
    - {invitations.length === 0 ? ( + {invitations.items.length === 0 ? (

    Входящих приглашений нет.

    ) : ( - invitations.map((inv) => { + invitations.items.map((inv) => { return (
  • diff --git a/src/pages/team/config/member.ts b/src/pages/team/config/member.ts index 55f2989..5b5db8d 100644 --- a/src/pages/team/config/member.ts +++ b/src/pages/team/config/member.ts @@ -12,14 +12,14 @@ interface IMemberCardConfig { export const memberCardConfig: IMemberCardConfig = { ringColor: { - banned: 'ring-destructive', + blocked: 'ring-destructive', active: 'ring-primary', - inactive: 'ring-muted', + pending: 'ring-muted', }, bgColor: { - banned: 'bg-destructive/10', + blocked: 'bg-destructive/10', active: 'bg-card', - inactive: 'bg-muted/90', + pending: 'bg-muted/90', }, workloadColor: (w) => { if (w === 0) return 'bg-muted/90'; @@ -28,9 +28,9 @@ export const memberCardConfig: IMemberCardConfig = { return 'bg-orange-500'; }, statusBadgeVariant: (s) => { - if (s === 'banned') return 'destructive'; + if (s === 'blocked') return 'destructive'; if (s === 'active') return 'default'; - if (s === 'inactive') return 'outline'; + if (s === 'pending') return 'outline'; }, workloadLabel: (w) => { if (w === 0) return { text: 'Не загружен', color: 'text-muted-foreground' }; diff --git a/src/pages/team/model/useMembersPage.ts b/src/pages/team/model/useMembersPage.ts index baadb09..9281695 100644 --- a/src/pages/team/model/useMembersPage.ts +++ b/src/pages/team/model/useMembersPage.ts @@ -27,7 +27,7 @@ export function useMembersPage() { useEffect(() => { if (data) { - setMembers(searchValue.current, data); + setMembers(searchValue.current, data.items); } }, [data]); @@ -37,7 +37,7 @@ export function useMembersPage() { setSearch(value); if (data) { - onFilter.debouncedCallback(value, data); + onFilter.debouncedCallback(value, data.items); } }; @@ -45,7 +45,7 @@ export function useMembersPage() { search, onChange, filtered, - total: data?.length ?? 0, + total: data?.items?.length ?? 0, isPending, }; } diff --git a/src/pages/team/ui/invitations/InvitationsPage.tsx b/src/pages/team/ui/invitations/InvitationsPage.tsx index 77016d6..b5c0c44 100644 --- a/src/pages/team/ui/invitations/InvitationsPage.tsx +++ b/src/pages/team/ui/invitations/InvitationsPage.tsx @@ -8,7 +8,7 @@ import { InvitationsEmpty } from './InvitationsEmpty'; export function InvitationsPage() { const { data, isPending } = useQueryInvitations(); - if (!isPending && !data?.length) { + if (!isPending && !data?.items?.length) { return ; } @@ -16,7 +16,7 @@ export function InvitationsPage() {
    {isPending ? Array.from({ length: 6 }).map((_, i) => ) - : data?.map((inv) => )} + : data?.items.map((inv) => )}
    ); } diff --git a/src/pages/team/ui/members/MemberCard.tsx b/src/pages/team/ui/members/MemberCard.tsx index 9f815a9..87bdf4b 100644 --- a/src/pages/team/ui/members/MemberCard.tsx +++ b/src/pages/team/ui/members/MemberCard.tsx @@ -37,7 +37,7 @@ export function MemberCard({ className, member, ...props }: MemberCardProps) { className={classNames( 'border-border bg-card rounded-xl border transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-14px_rgba(15,23,42,0.18)]', { - 'opacity-50 grayscale': member.status === 'inactive', + 'opacity-50 grayscale': member.status === 'pending', }, [cfg.bgColor[member.status], className] )} @@ -56,7 +56,6 @@ export function MemberCard({ className, member, ...props }: MemberCardProps) {

    {member.fullName}

    -

    {member.email}

    {member.role !== 'owner' && ( diff --git a/src/pages/team/ui/settings/SettingsPage.tsx b/src/pages/team/ui/settings/SettingsPage.tsx index 97de2e1..724e874 100644 --- a/src/pages/team/ui/settings/SettingsPage.tsx +++ b/src/pages/team/ui/settings/SettingsPage.tsx @@ -29,7 +29,6 @@ export function Settings() { defaultValues: { name: '', slug: '', - //todo tags description: '', }, }); @@ -41,7 +40,6 @@ export function Settings() { reset({ name: team.name, slug: team.slug, - //todo tags description: team.description || '', }); } diff --git a/src/pages/team/ui/settings/TeamIdentity.tsx b/src/pages/team/ui/settings/TeamIdentity.tsx index 506cbf0..d6bcbb7 100644 --- a/src/pages/team/ui/settings/TeamIdentity.tsx +++ b/src/pages/team/ui/settings/TeamIdentity.tsx @@ -31,7 +31,7 @@ export function TeamIdentity({ team, ...props }: TeamIdentityProps) { avatar={ } diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts index acd41c5..716a251 100644 --- a/src/shared/api/index.ts +++ b/src/shared/api/index.ts @@ -5,6 +5,12 @@ export { extractValidationIssues, type ValidationIssue, } from './validation'; -export { GlobalSuccess, GlobalError, DateTimeString } from './schemas'; +export { + GlobalSuccess, + GlobalError, + DateTimeString, + PaginatedResponseSchema, + MetaSchema, +} from './schemas'; export { AccessToken } from './token'; export { queryClient } from './query-client'; diff --git a/src/shared/api/schemas/index.ts b/src/shared/api/schemas/index.ts index ace9de0..e4df2be 100644 --- a/src/shared/api/schemas/index.ts +++ b/src/shared/api/schemas/index.ts @@ -1,3 +1,4 @@ export { GlobalSuccess } from './global-success'; export { GlobalError } from './global-error'; export { DateTimeString } from './date-time-string'; +export { PaginatedResponseSchema, MetaSchema } from './pagination'; diff --git a/src/shared/api/schemas/pagination.ts b/src/shared/api/schemas/pagination.ts new file mode 100644 index 0000000..290dc16 --- /dev/null +++ b/src/shared/api/schemas/pagination.ts @@ -0,0 +1,16 @@ +import { z } from 'zod/v4'; + +export const MetaSchema = z.object({ + hasNextPage: z.boolean(), + hasPrevPage: z.boolean(), + total: z.number(), + totalPages: z.number(), + page: z.number(), + limit: z.number(), +}); + +export const PaginatedResponseSchema = (schema: T) => + z.object({ + items: z.array(schema), + meta: MetaSchema, + }); diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 5cd10cc..5b579bb 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -3,3 +3,9 @@ import * as SApi from './schemas'; export type GlobalSuccess = z.infer; export type GlobalError = z.infer; +export type PaginationMeta = z.infer; + +export type PaginatedResponse = { + items: T[]; + meta: PaginationMeta; +};