Skip to content
Merged
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
20 changes: 4 additions & 16 deletions src/entities/team/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ export class TeamHttp {
}

static getInvitations(slug: string, signal?: AbortSignal) {
return api<TTeam.TeamInvitationResponse[]>({
return api<TTeam.TeamInvitationListResponse>({
url: `/teams/${slug}/invitations`,
method: 'GET',
contracts: {
response: STeam.TeamInvitationResponse.array(),
response: STeam.TeamInvitationListResponse,
},
signal,
});
Expand Down Expand Up @@ -126,11 +126,11 @@ export class TeamHttp {
}

static getMembers(slug: string, signal?: AbortSignal) {
return api<TTeam.TeamMemberResponse[]>({
return api<TTeam.TeamMemberListResponse>({
url: `/teams/${slug}/members`,
method: 'GET',
contracts: {
response: STeam.TeamMemberResponse.array(),
response: STeam.TeamMemberListResponse,
},
signal,
});
Expand All @@ -157,16 +157,4 @@ export class TeamHttp {
},
});
}

static syncTags(slug: string, data: TTeam.SyncTagsBody) {
return api<TTeam.ActionResponse>({
url: `/teams/${slug}/tags`,
method: 'PUT',
data,
contracts: {
body: STeam.SyncTagsBody,
response: STeam.ActionResponse,
},
});
}
}
6 changes: 3 additions & 3 deletions src/entities/team/config/statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { MemberStatus } from '../model/types';

export const STATUS_LABELS: Record<MemberStatus, string> = {
active: 'Активен',
banned: 'Заблокирован',
inactive: 'Неактивен',
blocked: 'Заблокирован',
pending: 'Неактивен',
} as const;

export const MEMBER_STATUSES = [
...new Set<keyof typeof STATUS_LABELS>(['active', 'inactive', 'banned']),
...new Set<keyof typeof STATUS_LABELS>(['active', 'pending', 'blocked']),
] as const;
55 changes: 14 additions & 41 deletions src/entities/team/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -22,8 +22,8 @@ export const TeamRole = z.enum([

export const MemberStatus = z.enum([
'active', // Полноценный участник
'banned', // Заблокирован не может вернуться по инвайту
'inactive', // Доступ закрыт, но запись сохранена
'blocked', // Заблокирован не может вернуться по инвайту
'pending',
]);

export const CreateTeamBody = z.object({
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -131,6 +118,8 @@ export const TeamMemberResponse = z.object({
joinedAt: DateTimeString,
});

export const TeamMemberListResponse = PaginatedResponseSchema(TeamMemberResponse);

export const UpdateMemberBody = z
.object({
role: TeamRole.optional(),
Expand All @@ -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;
4 changes: 3 additions & 1 deletion src/entities/team/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export type CheckSlugResponse = z.infer<typeof STeam.CheckSlugResponse>;
export type TeamDetailsResponse = z.infer<typeof STeam.TeamDetailsResponse>;

export type TeamInvitationResponse = z.infer<typeof STeam.TeamInvitationResponse>;
export type TeamInvitationListResponse = z.infer<typeof STeam.TeamInvitationListResponse>;

export type TeamMemberResponse = z.infer<typeof STeam.TeamMemberResponse>;
export type TeamMemberListResponse = z.infer<typeof STeam.TeamMemberListResponse>;

export type InviteMemberBody = z.infer<typeof STeam.InviteMemberBody>;
export type UpdateInvitationBody = z.infer<typeof STeam.UpdateInvitationBody>;
export type UpdateMemberBody = z.infer<typeof STeam.UpdateMemberBody>;
export type SyncTagsBody = z.infer<typeof STeam.SyncTagsBody>;
export type ActionResponse = z.infer<typeof STeam.ActionResponse>;
4 changes: 2 additions & 2 deletions src/entities/user/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ export class UserHttp {
}

static getMyInvitations(signal?: AbortSignal) {
return api<TUser.UserInvitationResponse[]>({
return api<TUser.UserInvitationListResponse>({
url: '/users/me/invites',
method: 'GET',
contracts: {
response: SUser.UserInvitationResponse.array(),
response: SUser.UserInvitationListResponse,
},
signal,
});
Expand Down
17 changes: 4 additions & 13 deletions src/entities/user/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DateTimeString, GlobalSuccess } from 'shared/api';
import { PaginatedResponseSchema } from 'shared/api/';
import { z } from 'zod/v4';

export const UserAvatarSchema = z
Expand Down Expand Up @@ -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(),
Expand All @@ -111,3 +100,5 @@ export const UserInvitationResponse = z.object({
inviterName: z.string(),
expiresAt: DateTimeString,
});

export const UserInvitationListResponse = PaginatedResponseSchema(UserInvitationResponse);
3 changes: 2 additions & 1 deletion src/entities/user/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type NotificationsUpdateResponse = z.infer<typeof SUser.NotificationsUpda
export type ProfileUpdateBody = z.infer<typeof SUser.ProfileUpdateBody>;
export type ProfileUpdateResponse = z.infer<typeof SUser.ProfileUpdateResponse>;
export type UserTeamResponse = z.infer<typeof SUser.UserTeamResponse>;
export type UserTeamsListMeta = z.infer<typeof SUser.UserTeamsListMeta>;
export type UserTeamsListResponse = z.infer<typeof SUser.UserTeamsListResponse>;
export type UserInvitationListResponse = z.infer<typeof SUser.UserInvitationListResponse>;

export type UserInvitationResponse = z.infer<typeof SUser.UserInvitationResponse>;
1 change: 0 additions & 1 deletion src/features/teams/create/model/useCreateTeamForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/pages/profile/ui/teams-page/Invitations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export function Invitations() {
return (
<div className="space-y-6">
<ul className="flex flex-col gap-3">
{invitations.length === 0 ? (
{invitations.items.length === 0 ? (
<p className="text-muted-foreground text-sm">Входящих приглашений нет.</p>
) : (
invitations.map((inv) => {
invitations.items.map((inv) => {
return (
<li key={inv.code}>
<InvitationItem {...inv} />
Expand Down
12 changes: 6 additions & 6 deletions src/pages/team/config/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' };
Expand Down
6 changes: 3 additions & 3 deletions src/pages/team/model/useMembersPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function useMembersPage() {

useEffect(() => {
if (data) {
setMembers(searchValue.current, data);
setMembers(searchValue.current, data.items);
}
}, [data]);

Expand All @@ -37,15 +37,15 @@ export function useMembersPage() {
setSearch(value);

if (data) {
onFilter.debouncedCallback(value, data);
onFilter.debouncedCallback(value, data.items);
}
};

return {
search,
onChange,
filtered,
total: data?.length ?? 0,
total: data?.items?.length ?? 0,
isPending,
};
}
4 changes: 2 additions & 2 deletions src/pages/team/ui/invitations/InvitationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { InvitationsEmpty } from './InvitationsEmpty';
export function InvitationsPage() {
const { data, isPending } = useQueryInvitations();

if (!isPending && !data?.length) {
if (!isPending && !data?.items?.length) {
return <InvitationsEmpty />;
}

return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{isPending
? Array.from({ length: 6 }).map((_, i) => <InvitationCardSkeleton key={i} />)
: data?.map((inv) => <InvitationCard key={inv.code} inv={inv} />)}
: data?.items.map((inv) => <InvitationCard key={inv.code} inv={inv} />)}
</div>
);
}
3 changes: 1 addition & 2 deletions src/pages/team/ui/members/MemberCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)}
Expand All @@ -56,7 +56,6 @@ export function MemberCard({ className, member, ...props }: MemberCardProps) {
</Avatar>
<div className="flex-1">
<p className="text-sm font-semibold">{member.fullName}</p>
<p className="text-muted-foreground text-xs">{member.email}</p>
</div>
{member.role !== 'owner' && (
<ItemActions>
Expand Down
2 changes: 0 additions & 2 deletions src/pages/team/ui/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export function Settings() {
defaultValues: {
name: '',
slug: '',
//todo tags
description: '',
},
});
Expand All @@ -41,7 +40,6 @@ export function Settings() {
reset({
name: team.name,
slug: team.slug,
//todo tags
description: team.description || '',
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/team/ui/settings/TeamIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function TeamIdentity({ team, ...props }: TeamIdentityProps) {
avatar={
<TeamAvatar
wrap={{ className: 'ring-background size-28 shadow-md ring-4' }}
src={team.avatar?.medium ?? undefined}
src={team.avatarUrl ?? undefined}
alt={team.name}
/>
}
Expand Down
Loading
Loading