From 39e35d546bba8689309170b14c0f0129141c3fa6 Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Sun, 7 Jun 2026 21:51:54 +0300 Subject: [PATCH 1/3] refactor(user): update user schemas and add UserActivityResponse schema --- src/entities/user/api/http.ts | 4 +- src/entities/user/model/schemas.ts | 77 +++++++++++++++++++++++------- src/entities/user/model/types.ts | 2 + 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/entities/user/api/http.ts b/src/entities/user/api/http.ts index 13ead9f..b2dc8ab 100644 --- a/src/entities/user/api/http.ts +++ b/src/entities/user/api/http.ts @@ -19,7 +19,9 @@ export class UserHttp { return api({ url: '/users/me/activity', method: 'GET', - contracts: {}, + contracts: { + response: SUser.UserActivityResponse, + }, signal, }); } diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts index f2851f3..f00c8a1 100644 --- a/src/entities/user/model/schemas.ts +++ b/src/entities/user/model/schemas.ts @@ -11,20 +11,31 @@ export const UserAvatarSchema = z }) .nullish(); +export const ProfileResponse = z.object({ + firstName: z.string(), + lastName: z.string(), + middleName: z.string().nullable(), + bio: z.string().nullable(), + avatar: UserAvatarSchema, + headline: z.string().nullable(), + location: z.string().nullable(), + phone: z.string().nullable(), + gender: z + .enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']) + .default('none'), + vacationStart: z.string().nullable(), + vacationEnd: z.string().nullable(), + vacationMessage: z.string().nullable(), + pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).default('none'), + pronounsCustom: z.string().max(50, 'Максимальная длина 50 символов').nullable().optional(), + createdAt: DateTimeString, + updatedAt: DateTimeString, +}); + export const UserResponse = z.object({ id: z.string(), email: z.email(), - profile: z.object({ - firstName: z.string(), - lastName: z.string(), - middleName: z.string().nullable(), - bio: z.string().nullable(), - avatar: UserAvatarSchema, - timezone: z.string(), - language: z.string(), - createdAt: DateTimeString, - updatedAt: DateTimeString, - }), + profile: ProfileResponse, security: z.object({ is2faEnabled: z.boolean(), lastPasswordChange: DateTimeString, @@ -40,6 +51,11 @@ export const UserResponse = z.object({ reminders: z.boolean(), }), }), + preferences: z.object({ + timezone: z.string(), + language: z.string(), + theme: z.enum(['light', 'dark', 'system']).optional(), + }), }); export const NotificationsUpdateBody = z.object({ @@ -61,12 +77,33 @@ export const NotificationsUpdateBody = z.object({ export const NotificationsUpdateResponse = GlobalSuccess; export const ProfileUpdateBody = z.object({ - firstName: z.string().min(1).max(50).optional(), - lastName: z.string().min(1).max(50).optional(), - middleName: z.string().max(50).nullish(), - bio: z.string().max(512).nullish(), + firstName: z + .string() + .min(1, 'Имя не может быть пустым') + .max(50, 'Имя слишком длинное') + .optional(), + lastName: z + .string() + .min(1, 'Фамилия не может быть пустой') + .max(50, 'Фамилия слишком длинная') + .optional(), + middleName: z.string().max(50, 'Отчество слишком длинное').nullable().optional(), + headline: z.string().max(100, 'Должность слишком длинная').nullable().optional(), + location: z.string().max(100, 'Локация слишком длинная').nullable().optional(), + phone: z.string().max(20, 'Номер телефона слишком длинный').nullable().optional(), + gender: z + .enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']) + .default('none') + .optional(), + vacationStart: z.string().nullable().optional(), + vacationEnd: z.string().nullable().optional(), + vacationMessage: z.string().max(500, 'Сообщение слишком длинное').nullable().optional(), + pronouns: z.enum(['he_him', 'she_her', 'they_them', 'other', 'none']).default('none').optional(), + pronounsCustom: z.string().max(50, 'Максимальная длина 50 символов').nullable().optional(), + bio: z.string().max(1000, 'О себе не более 1000 символов').nullable().optional(), timezone: z.string().max(50).optional(), - language: z.string().min(2).max(2).optional(), + language: z.string().length(2).optional(), + theme: z.enum(['light', 'dark', 'system']).optional(), }); export const ProfileUpdateResponse = GlobalSuccess; @@ -102,3 +139,11 @@ export const UserInvitationResponse = z.object({ }); export const UserInvitationListResponse = PaginatedResponseSchema(UserInvitationResponse); + +export const UserActivityResponse = z.object({ + id: z.string(), + eventType: z.string(), + entityId: z.string().nullable().optional(), + metadata: z.record(z.string(), z.unknown()).nullable().optional(), + createdAt: DateTimeString, +}); diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts index 32d48bd..948490f 100644 --- a/src/entities/user/model/types.ts +++ b/src/entities/user/model/types.ts @@ -11,3 +11,5 @@ export type UserTeamsListResponse = z.infer; export type UserInvitationListResponse = z.infer; export type UserInvitationResponse = z.infer; + +export type UserActivityResponse = z.infer; From 1ae09edd1a6323472b13ec7249f89c6b41f19547 Mon Sep 17 00:00:00 2001 From: Alexandr Nelyubov Date: Mon, 8 Jun 2026 00:17:09 +0300 Subject: [PATCH 2/3] refactor: remove slug --- src/app/layouts/SidebarLayout.tsx | 4 +- src/entities/project/api/http.ts | 28 +++--- src/entities/project/api/queries.ts | 12 +-- src/entities/project/model/const.ts | 4 +- src/entities/project/model/schemas.ts | 7 +- src/entities/team/api/http.ts | 55 +++++------- src/entities/team/api/queries.ts | 33 +++---- src/entities/team/index.ts | 5 +- src/entities/team/lib/useCheckSlug.ts | 90 ------------------- src/entities/team/lib/useSlugFieldStatus.ts | 41 --------- src/entities/team/lib/validate-team-slug.ts | 21 ----- src/entities/team/model/const.ts | 12 +-- src/entities/team/model/schemas.ts | 24 ----- src/entities/team/model/store.ts | 16 ++-- src/entities/team/model/types.ts | 1 - src/entities/team/ui/SlugField.tsx | 68 -------------- src/entities/team/ui/SlugFieldStatus.tsx | 30 ------- src/entities/user/model/schemas.ts | 1 - .../archive/model/useArchiveProject.ts | 8 +- .../archive/model/useRestoreProject.ts | 8 +- .../archive/ui/ArchiveProjectDialog.tsx | 6 +- .../archive/ui/RestoreProjectDialog.tsx | 6 +- .../projects/create/model/useCreateProject.ts | 6 +- .../create/model/useCreateProjectForm.ts | 8 +- .../projects/remove/model/useRemoveProject.ts | 6 +- .../remove/ui/RemoveProjectDialog.tsx | 6 +- .../projects/share/model/useShareProject.ts | 6 +- .../projects/share/ui/ShareProjectDialog.tsx | 6 +- src/features/teams/active-team/index.ts | 2 +- .../teams/active-team/model/useSwitchTeam.ts | 10 +-- .../model/useTeamsQueryWithSlugSync.ts | 23 ----- .../model/useTeamsQueryWithTeamIdSync.ts | 23 +++++ .../teams/active-team/ui/TeamSlugSync.tsx | 8 -- .../teams/active-team/ui/TeamTeamIdSync.tsx | 8 ++ .../teams/create/model/useCreateTeamForm.ts | 12 +-- .../teams/create/ui/CreateTeamForm.tsx | 2 - .../teams/invite/model/useInviteTeamMember.ts | 10 +-- .../invite/model/useInviteTeamMemberForm.ts | 6 +- .../teams/remove/ui/RemoveTeamDialog.tsx | 6 +- src/pages/profile/ui/teams-page/TeamList.tsx | 10 +-- src/pages/project/api/useQueryProject.ts | 6 +- src/pages/project/api/useUpdateProject.ts | 12 +-- .../project/ui/settings/ProjectDangerZone.tsx | 6 +- .../ui/settings/ProjectSettingsPage.tsx | 10 +-- src/pages/team/api/useQueryInvitations.ts | 6 +- src/pages/team/api/useQueryTeam.ts | 6 +- src/pages/team/api/useRemoveMember.ts | 10 +-- .../team/api/useRemoveMemberInvitation.ts | 10 +-- src/pages/team/api/useUpdateInvitation.ts | 10 +-- src/pages/team/api/useUpdateMember.ts | 10 +-- src/pages/team/api/useUpdateTeam.ts | 14 ++- src/pages/team/model/useMembersPage.ts | 4 +- src/pages/team/ui/projects/ProjectCard.tsx | 20 ++--- src/pages/team/ui/projects/ProjectsPage.tsx | 8 +- src/pages/team/ui/settings/DangerZone.tsx | 6 +- src/pages/team/ui/settings/SaveBar.tsx | 1 - src/pages/team/ui/settings/SettingsPage.tsx | 12 +-- .../team/ui/settings/TeamIdentityForm.tsx | 46 ++++------ .../app-sidebar/model/useTeamHotkeys.ts | 7 +- src/widgets/app-sidebar/ui/Projects.tsx | 14 +-- .../app-sidebar/ui/teams/TeamTrigger.tsx | 6 +- .../app-sidebar/ui/teams/TeamsDropdown.tsx | 2 +- src/widgets/quick-create/ui/QuickCreate.tsx | 4 +- 63 files changed, 266 insertions(+), 602 deletions(-) delete mode 100644 src/entities/team/lib/useCheckSlug.ts delete mode 100644 src/entities/team/lib/useSlugFieldStatus.ts delete mode 100644 src/entities/team/lib/validate-team-slug.ts delete mode 100644 src/entities/team/ui/SlugField.tsx delete mode 100644 src/entities/team/ui/SlugFieldStatus.tsx delete mode 100644 src/features/teams/active-team/model/useTeamsQueryWithSlugSync.ts create mode 100644 src/features/teams/active-team/model/useTeamsQueryWithTeamIdSync.ts delete mode 100644 src/features/teams/active-team/ui/TeamSlugSync.tsx create mode 100644 src/features/teams/active-team/ui/TeamTeamIdSync.tsx diff --git a/src/app/layouts/SidebarLayout.tsx b/src/app/layouts/SidebarLayout.tsx index 6291ce5..72857fd 100644 --- a/src/app/layouts/SidebarLayout.tsx +++ b/src/app/layouts/SidebarLayout.tsx @@ -1,4 +1,4 @@ -import { TeamSlugSync } from 'features/teams/active-team'; +import { TeamIdSync } from 'features/teams/active-team'; import { ComponentProps } from 'react'; import { Separator, SidebarInset, SidebarProvider, SidebarTrigger } from 'shared/ui'; import { AppSidebar } from 'widgets/app-sidebar'; @@ -9,7 +9,7 @@ import { QuickCreate } from 'widgets/quick-create'; export function SidebarLayout({ children, ...props }: ComponentProps) { return ( - +
diff --git a/src/entities/project/api/http.ts b/src/entities/project/api/http.ts index bb4c028..81ba007 100644 --- a/src/entities/project/api/http.ts +++ b/src/entities/project/api/http.ts @@ -3,9 +3,9 @@ import * as SProject from '../model/schemas'; import * as TProject from '../model/types'; export class ProjectHttp { - static getProjects(teamSlug: string, signal?: AbortSignal) { + static getProjects(teamId: string, signal?: AbortSignal) { return api({ - url: `/teams/${teamSlug}/projects`, + url: `/teams/${teamId}/projects`, method: 'GET', contracts: { response: SProject.ProjectListResponse, @@ -14,9 +14,9 @@ export class ProjectHttp { }); } - static getProject(teamSlug: string, id: string, token?: string, signal?: AbortSignal) { + static getProject(teamId: string, id: string, token?: string, signal?: AbortSignal) { return api({ - url: `/teams/${teamSlug}/projects/${id}`, + url: `/teams/${teamId}/projects/${id}`, method: 'GET', params: token ? { token } : undefined, contracts: { @@ -26,9 +26,9 @@ export class ProjectHttp { }); } - static createProject(teamSlug: string, data: TProject.CreateProjectBody) { + static createProject(teamId: string, data: TProject.CreateProjectBody) { return api({ - url: `/teams/${teamSlug}/projects`, + url: `/teams/${teamId}/projects`, method: 'POST', data, contracts: { @@ -38,9 +38,9 @@ export class ProjectHttp { }); } - static updateProject(teamSlug: string, id: string, data: TProject.UpdateProjectBody) { + static updateProject(teamId: string, id: string, data: TProject.UpdateProjectBody) { return api({ - url: `/teams/${teamSlug}/projects/${id}`, + url: `/teams/${teamId}/projects/${id}`, method: 'PATCH', data, contracts: { @@ -50,9 +50,9 @@ export class ProjectHttp { }); } - static removeProject(teamSlug: string, id: string) { + static removeProject(teamId: string, id: string) { return api({ - url: `/teams/${teamSlug}/projects/${id}`, + url: `/teams/${teamId}/projects/${id}`, method: 'DELETE', contracts: { response: SProject.ActionResponse, @@ -60,9 +60,9 @@ export class ProjectHttp { }); } - static archiveProject(teamSlug: string, id: string) { + static archiveProject(teamId: string, id: string) { return api({ - url: `/teams/${teamSlug}/projects/${id}/archive`, + url: `/teams/${teamId}/projects/${id}/archive`, method: 'POST', contracts: { response: SProject.ActionResponse, @@ -70,9 +70,9 @@ export class ProjectHttp { }); } - static createShareToken(teamSlug: string, id: string, data: TProject.CreateShareTokenBody = {}) { + static createShareToken(teamId: string, id: string, data: TProject.CreateShareTokenBody = {}) { return api({ - url: `/teams/${teamSlug}/projects/${id}/share`, + url: `/teams/${teamId}/projects/${id}/share`, method: 'POST', data, contracts: { diff --git a/src/entities/project/api/queries.ts b/src/entities/project/api/queries.ts index 6bfb089..6b42d9c 100644 --- a/src/entities/project/api/queries.ts +++ b/src/entities/project/api/queries.ts @@ -3,18 +3,18 @@ import { projectFabricKeys } from '../model/const'; import { ProjectHttp } from './http'; export class ProjectQueries { - static getProjects(teamSlug: string) { + static getProjects(teamId: string) { return queryOptions({ - queryKey: projectFabricKeys.list(teamSlug), - queryFn: async ({ signal }) => ProjectHttp.getProjects(teamSlug, signal), + queryKey: projectFabricKeys.list(teamId), + queryFn: async ({ signal }) => ProjectHttp.getProjects(teamId, signal), staleTime: 60_000, }); } - static getProject(teamSlug: string, id: string, token?: string) { + static getProject(teamId: string, id: string, token?: string) { return queryOptions({ - queryKey: [...projectFabricKeys.detail(teamSlug, id), token ?? null], - queryFn: async ({ signal }) => ProjectHttp.getProject(teamSlug, id, token, signal), + queryKey: [...projectFabricKeys.detail(teamId, id), token ?? null], + queryFn: async ({ signal }) => ProjectHttp.getProject(teamId, id, token, signal), staleTime: 60_000, }); } diff --git a/src/entities/project/model/const.ts b/src/entities/project/model/const.ts index 0276da1..53574a5 100644 --- a/src/entities/project/model/const.ts +++ b/src/entities/project/model/const.ts @@ -1,6 +1,6 @@ import { createEntityKeys } from 'shared/lib/utils'; export const projectFabricKeys = createEntityKeys('project', { - list: (teamSlug: string) => ['teams', teamSlug, 'projects'], - detail: (teamSlug: string, id: string) => ['teams', teamSlug, 'projects', id], + list: (teamId: string) => ['teams', teamId, 'projects'], + detail: (teamId: string, id: string) => ['teams', teamId, 'projects', id], }); diff --git a/src/entities/project/model/schemas.ts b/src/entities/project/model/schemas.ts index a226915..67d1cba 100644 --- a/src/entities/project/model/schemas.ts +++ b/src/entities/project/model/schemas.ts @@ -1,4 +1,4 @@ -import { DateTimeString, GlobalSuccess } from 'shared/api'; +import { DateTimeString, GlobalSuccess, PaginatedResponseSchema } from 'shared/api'; import { z } from 'zod/v4'; import { PROJECT_ICONS } from '../config/icons'; @@ -57,15 +57,12 @@ export const ProjectListItemResponse = z.object({ canEdit: z.boolean(), }); -export const ProjectListResponse = z.object({ +export const ProjectListResponse = PaginatedResponseSchema(ProjectListItemResponse).extend({ team: z.object({ id: z.string(), name: z.string(), - slug: z.string(), role: z.string(), }), - items: ProjectListItemResponse.array(), - meta: z.object({ total: z.number() }), }); export const ProjectDetailResponse = z.object({ diff --git a/src/entities/team/api/http.ts b/src/entities/team/api/http.ts index af1fdf5..5333f19 100644 --- a/src/entities/team/api/http.ts +++ b/src/entities/team/api/http.ts @@ -15,20 +15,9 @@ export class TeamHttp { }); } - static checkSlug(slug: string, signal?: AbortSignal) { - return api({ - url: `/teams/check-slug/${slug}`, - method: 'GET', - contracts: { - response: STeam.CheckSlugResponse, - }, - signal, - }); - } - - static getTeam(slug: string, signal?: AbortSignal) { + static getTeam(teamId: string, signal?: AbortSignal) { return api({ - url: `/teams/${slug}`, + url: `/teams/${teamId}`, method: 'GET', contracts: { response: STeam.TeamDetailsResponse, @@ -37,9 +26,9 @@ export class TeamHttp { }); } - static updateTeam(slug: string, data: TTeam.UpdateTeamBody) { + static updateTeam(teamId: string, data: TTeam.UpdateTeamBody) { return api({ - url: `/teams/${slug}`, + url: `/teams/${teamId}`, method: 'PATCH', data, contracts: { @@ -49,9 +38,9 @@ export class TeamHttp { }); } - static removeTeam(slug: string) { + static removeTeam(teamId: string) { return api({ - url: `/teams/${slug}`, + url: `/teams/${teamId}`, method: 'DELETE', contracts: { response: STeam.ActionResponse, @@ -59,9 +48,9 @@ export class TeamHttp { }); } - static getInvitations(slug: string, signal?: AbortSignal) { + static getInvitations(teamId: string, signal?: AbortSignal) { return api({ - url: `/teams/${slug}/invitations`, + url: `/teams/${teamId}/invitations`, method: 'GET', contracts: { response: STeam.TeamInvitationListResponse, @@ -70,9 +59,9 @@ export class TeamHttp { }); } - static getInvitation(slug: string, code: string, signal?: AbortSignal) { + static getInvitation(teamId: string, code: string, signal?: AbortSignal) { return api({ - url: `/teams/${slug}/invitations/${code}`, + url: `/teams/${teamId}/invitations/${code}`, method: 'GET', contracts: { response: STeam.TeamInvitationResponse, @@ -81,9 +70,9 @@ export class TeamHttp { }); } - static inviteMember(slug: string, data: TTeam.InviteMemberBody) { + static inviteMember(teamId: string, data: TTeam.InviteMemberBody) { return api({ - url: `/teams/${slug}/invitations`, + url: `/teams/${teamId}/invitations`, method: 'POST', data, contracts: { @@ -103,9 +92,9 @@ export class TeamHttp { }); } - static updateInvitation(slug: string, code: string, data: TTeam.UpdateInvitationBody) { + static updateInvitation(teamId: string, code: string, data: TTeam.UpdateInvitationBody) { return api({ - url: `/teams/${slug}/invitations/${code}`, + url: `/teams/${teamId}/invitations/${code}`, method: 'PATCH', data, contracts: { @@ -115,9 +104,9 @@ export class TeamHttp { }); } - static removeInvitation(slug: string, code: string) { + static removeInvitation(teamId: string, code: string) { return api({ - url: `/teams/${slug}/invitations/${code}`, + url: `/teams/${teamId}/invitations/${code}`, method: 'DELETE', contracts: { response: STeam.ActionResponse, @@ -125,9 +114,9 @@ export class TeamHttp { }); } - static getMembers(slug: string, signal?: AbortSignal) { + static getMembers(teamId: string, signal?: AbortSignal) { return api({ - url: `/teams/${slug}/members`, + url: `/teams/${teamId}/members`, method: 'GET', contracts: { response: STeam.TeamMemberListResponse, @@ -136,9 +125,9 @@ export class TeamHttp { }); } - static updateMember(slug: string, userId: string, data: TTeam.UpdateMemberBody) { + static updateMember(teamId: string, userId: string, data: TTeam.UpdateMemberBody) { return api({ - url: `/teams/${slug}/members/${userId}`, + url: `/teams/${teamId}/members/${userId}`, method: 'PATCH', data, contracts: { @@ -148,9 +137,9 @@ export class TeamHttp { }); } - static removeMember(slug: string, userId: string) { + static removeMember(teamId: string, userId: string) { return api({ - url: `/teams/${slug}/members/${userId}`, + url: `/teams/${teamId}/members/${userId}`, method: 'DELETE', contracts: { response: STeam.ActionResponse, diff --git a/src/entities/team/api/queries.ts b/src/entities/team/api/queries.ts index 566233f..eb8320a 100644 --- a/src/entities/team/api/queries.ts +++ b/src/entities/team/api/queries.ts @@ -3,43 +3,34 @@ import { teamFabricKeys } from '../model/const'; import { TeamHttp } from './http'; export class TeamQueries { - static getTeam(slug: string) { + static getTeam(teamId: string) { return queryOptions({ - queryKey: teamFabricKeys.bySlug(slug), - queryFn: async ({ signal }) => TeamHttp.getTeam(slug, signal), + queryKey: teamFabricKeys.byId(teamId), + queryFn: async ({ signal }) => TeamHttp.getTeam(teamId, signal), staleTime: 60_000, }); } - static checkSlug(slug: string) { + static getInvitation(teamId: string, code: string) { return queryOptions({ - queryKey: teamFabricKeys.checkSlug(slug), - queryFn: async ({ signal }) => TeamHttp.checkSlug(slug, signal), - gcTime: 5000, - staleTime: 5000, - }); - } - - static getInvitation(slug: string, code: string) { - return queryOptions({ - queryKey: teamFabricKeys.invitation(slug, code), - queryFn: async ({ signal }) => TeamHttp.getInvitation(slug, code, signal), + queryKey: teamFabricKeys.invitation(teamId, code), + queryFn: async ({ signal }) => TeamHttp.getInvitation(teamId, code, signal), staleTime: 60_000, }); } - static getInvitations(slug: string) { + static getInvitations(teamId: string) { return queryOptions({ - queryKey: teamFabricKeys.invitations(slug), - queryFn: async ({ signal }) => TeamHttp.getInvitations(slug, signal), + queryKey: teamFabricKeys.invitations(teamId), + queryFn: async ({ signal }) => TeamHttp.getInvitations(teamId, signal), staleTime: 60_000, }); } - static getMembers(slug: string) { + static getMembers(teamId: string) { return queryOptions({ - queryKey: teamFabricKeys.members(slug), - queryFn: async ({ signal }) => TeamHttp.getMembers(slug, signal), + queryKey: teamFabricKeys.members(teamId), + queryFn: async ({ signal }) => TeamHttp.getMembers(teamId, signal), staleTime: 60_000, }); } diff --git a/src/entities/team/index.ts b/src/entities/team/index.ts index 326e935..2777269 100644 --- a/src/entities/team/index.ts +++ b/src/entities/team/index.ts @@ -2,11 +2,8 @@ export * as STeam from './model/schemas'; export type * as TTeam from './model/types'; export { TeamHttp } from './api/http'; export { TeamQueries } from './api/queries'; -export { MAX_SLUG_LENGTH, MIN_SLUG_LENGTH, teamFabricKeys } from './model/const'; +export { teamFabricKeys } from './model/const'; export { ROLE_LABELS, INVITATION_ROLES } from './config/roles'; export { STATUS_LABELS, MEMBER_STATUSES } from './config/statuses'; -export { useCheckSlug } from './lib/useCheckSlug'; -export { validateTeamSlugAsync } from './lib/validate-team-slug'; export { useTeamStore } from './model/store'; export { TeamAvatar } from './ui/TeamAvatar'; -export { SlugField } from './ui/SlugField'; diff --git a/src/entities/team/lib/useCheckSlug.ts b/src/entities/team/lib/useCheckSlug.ts deleted file mode 100644 index 1928b51..0000000 --- a/src/entities/team/lib/useCheckSlug.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import type { FieldErrors } from 'react-hook-form'; -import { debounce } from 'shared/lib/utils'; -import { TeamQueries } from '../api/queries'; -import { MAX_SLUG_LENGTH, MIN_SLUG_LENGTH, teamFabricKeys } from '../model/const'; - -const DEBOUNCE_MS = 400; -const SLUG_UNAVAILABLE_MESSAGE = 'Этот адрес уже занят'; - -export type CheckSlugErrors = FieldErrors<{ slug: string }>; - -function prepareResponse(available: boolean, message?: string): CheckSlugErrors { - if (available) { - return {}; - } - - return { - slug: { - type: 'validate', - message: message ?? SLUG_UNAVAILABLE_MESSAGE, - }, - }; -} - -export function useCheckSlug(defaultValue: string) { - const currentSlug = useRef(defaultValue); - const pendingResolve = useRef<((errors: CheckSlugErrors) => void) | null>(null); - const queryClient = useQueryClient(); - - const resolvePending = useCallback((errors: CheckSlugErrors) => { - pendingResolve.current?.(errors); - pendingResolve.current = null; - }, []); - - const debouncedCheckSlug = useMemo(() => { - // eslint-disable-next-line react-hooks/refs -- refs read only in async-callback debounce - return debounce(async (value: string) => { - try { - const data = await queryClient.fetchQuery(TeamQueries.checkSlug(value)); - - // when the server response arrives but is already outdated - if (value !== currentSlug.current) { - return; - } - - resolvePending(prepareResponse(data.available, data.message)); - } catch { - if (value === currentSlug.current) { - resolvePending({}); - } - } - }, DEBOUNCE_MS); - }, [queryClient, resolvePending]); - - const cancel = useCallback(() => { - debouncedCheckSlug.cancelDebouncedCallback(); - queryClient.cancelQueries({ queryKey: teamFabricKeys.checkSlug() }); - resolvePending({}); - }, [debouncedCheckSlug, queryClient, resolvePending]); - - useEffect(() => { - return () => { - cancel(); - queryClient.removeQueries({ queryKey: teamFabricKeys.checkSlug() }); - }; - }, [cancel, queryClient]); - - return useCallback( - (value: string): Promise => - new Promise((resolve) => { - const isValid = - defaultValue !== value && - value.length >= MIN_SLUG_LENGTH && - value.length <= MAX_SLUG_LENGTH; - - cancel(); - currentSlug.current = value; - - if (!isValid) { - resolve({}); - return; - } - - pendingResolve.current = resolve; - debouncedCheckSlug.debouncedCallback(value); - }), - [cancel, debouncedCheckSlug, defaultValue] - ); -} diff --git a/src/entities/team/lib/useSlugFieldStatus.ts b/src/entities/team/lib/useSlugFieldStatus.ts deleted file mode 100644 index 3b8c139..0000000 --- a/src/entities/team/lib/useSlugFieldStatus.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useMemo } from 'react'; -import { TeamQueries } from '../api/queries'; -import { MAX_SLUG_LENGTH, MIN_SLUG_LENGTH } from '../model/const'; - -type SlugFieldStatusValue = 'pending' | 'success' | 'error'; - -interface SlugFieldStatusState { - isDirty: boolean; - slug: string; -} - -export function useSlugFieldStatus({ - isDirty, - slug, -}: SlugFieldStatusState): SlugFieldStatusValue | undefined { - const { data, isPending } = useQuery({ - ...TeamQueries.checkSlug(slug), - enabled: false, - }); - - return useMemo(() => { - if (!isDirty || slug.length < MIN_SLUG_LENGTH || slug.length > MAX_SLUG_LENGTH) { - return undefined; - } - - if (isPending) { - return 'pending'; - } - - if (data?.available === false) { - return 'error'; - } - - if (data?.available === true) { - return 'success'; - } - - return undefined; - }, [data?.available, isDirty, isPending, slug]); -} diff --git a/src/entities/team/lib/validate-team-slug.ts b/src/entities/team/lib/validate-team-slug.ts deleted file mode 100644 index acdd285..0000000 --- a/src/entities/team/lib/validate-team-slug.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { FieldValues, ResolverOptions } from 'react-hook-form'; -import type { CheckSlugErrors } from './useCheckSlug'; - -type SlugFormValues = { slug?: string }; - -function shouldValidateSlugAsync(names: readonly string[] | undefined): boolean { - return !names?.length || names.includes('slug'); -} - -export function validateTeamSlugAsync( - checkSlug: (value: string) => Promise, - values: T, - _context: unknown, - options: ResolverOptions -): Promise { - if (!shouldValidateSlugAsync(options.names as readonly string[] | undefined)) { - return Promise.resolve({}); - } - - return checkSlug(values.slug ?? ''); -} diff --git a/src/entities/team/model/const.ts b/src/entities/team/model/const.ts index 1e50a15..3009607 100644 --- a/src/entities/team/model/const.ts +++ b/src/entities/team/model/const.ts @@ -1,12 +1,8 @@ import { createEntityKeys } from 'shared/lib/utils'; -export const MIN_SLUG_LENGTH = 2; -export const MAX_SLUG_LENGTH = 100; - export const teamFabricKeys = createEntityKeys('team', { - bySlug: (slug: string) => ['teams', slug], - checkSlug: (slug?: string) => ['teams', 'check-slug', slug].filter(Boolean), - invitations: (slug: string) => ['teams', slug, 'invitations'], - invitation: (slug: string, code: string) => ['teams', slug, 'invitations', code], - members: (slug: string) => ['teams', slug, 'members'], + byId: (teamId: string) => ['teams', teamId], + invitations: (teamId: string) => ['teams', teamId, 'invitations'], + invitation: (teamId: string, code: string) => ['teams', teamId, 'invitations', code], + members: (teamId: string) => ['teams', teamId, 'members'], }); diff --git a/src/entities/team/model/schemas.ts b/src/entities/team/model/schemas.ts index 7946093..b2cf387 100644 --- a/src/entities/team/model/schemas.ts +++ b/src/entities/team/model/schemas.ts @@ -1,6 +1,5 @@ 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({ @@ -37,23 +36,6 @@ export const CreateTeamBody = z.object({ .min(1, 'Добавьте описание команды') .min(10, 'Описание должно содержать не менее 10 символов') .max(500, 'Описание не может быть длиннее 500 символов'), - slug: z - .string() - .optional() - .transform((val) => (val === '' || val === undefined ? undefined : val)) - .pipe( - z - .string() - .min( - MIN_SLUG_LENGTH, - `Короткий адрес должен содержать не менее ${MIN_SLUG_LENGTH} символов` - ) - .max( - MAX_SLUG_LENGTH, - `Короткий адрес в ссылке не может быть длиннее ${MAX_SLUG_LENGTH} символов` - ) - .optional() - ), }); export const UpdateTeamBody = CreateTeamBody.partial().refine( @@ -64,15 +46,9 @@ export const UpdateTeamBody = CreateTeamBody.partial().refine( } ); -export const CheckSlugResponse = z.object({ - available: z.boolean(), - message: z.string().optional(), -}); - export const TeamDetailsResponse = z.object({ id: z.string(), name: z.string(), - slug: z.string(), description: z.string().nullable(), avatarUrl: z.string().nullable(), coverUrl: z.string().nullable(), diff --git a/src/entities/team/model/store.ts b/src/entities/team/model/store.ts index 05d491d..5d709e0 100644 --- a/src/entities/team/model/store.ts +++ b/src/entities/team/model/store.ts @@ -2,21 +2,21 @@ import { createStore } from 'shared/lib/store'; import { persist } from 'zustand/middleware'; type TeamState = { - slug: string | null; - setSlug: (slug: string | null) => void; - clearSlug: () => void; + teamId: string | null; + setTeamId: (teamId: string | null) => void; + clearTeamId: () => void; }; export const useTeamStore = createStore( (set) => ({ - slug: null, - setSlug: (slug) => + teamId: null, + setTeamId: (teamId) => set((state) => { - state.slug = slug; + state.teamId = teamId; }), - clearSlug: () => + clearTeamId: () => set((state) => { - state.slug = null; + state.teamId = null; }), }), [ diff --git a/src/entities/team/model/types.ts b/src/entities/team/model/types.ts index 0fca72e..1fb0892 100644 --- a/src/entities/team/model/types.ts +++ b/src/entities/team/model/types.ts @@ -8,7 +8,6 @@ export type MemberStatus = z.infer; export type CreateTeamBody = z.infer; export type UpdateTeamBody = z.infer; -export type CheckSlugResponse = z.infer; export type TeamDetailsResponse = z.infer; export type TeamInvitationResponse = z.infer; diff --git a/src/entities/team/ui/SlugField.tsx b/src/entities/team/ui/SlugField.tsx deleted file mode 100644 index f05192a..0000000 --- a/src/entities/team/ui/SlugField.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client'; - -import { useId } from 'react'; -import { Controller, FieldPath, FieldValues, useFormContext } from 'react-hook-form'; -import { cn } from 'shared/lib/utils'; -import { - Field, - FieldError, - FieldLabel, - InputGroup, - InputGroupAddon, - InputGroupInput, -} from 'shared/ui'; -import { SlugFieldStatus } from './SlugFieldStatus'; - -interface SlugFieldProps { - name: FieldPath; - disabled?: boolean; - label?: string; - prefix?: string; - placeholder?: string; - className?: string; -} - -export function SlugField({ - disabled = false, - label = 'Короткий адрес в ссылке (необязательно)', - prefix, - placeholder = 'my-team', - className, - name, -}: SlugFieldProps) { - const id = useId(); - const { trigger, control } = useFormContext(); - - return ( - ( - - {label} - - {prefix ? {prefix} : null} - { - field.onChange(e.target.value.trim().toLowerCase()); - void trigger(name); - }} - id={id} - aria-label={label} - placeholder={placeholder} - aria-invalid={fieldState.invalid} - autoComplete="off" - disabled={disabled} - /> - - - - - {fieldState.invalid && } - - )} - /> - ); -} diff --git a/src/entities/team/ui/SlugFieldStatus.tsx b/src/entities/team/ui/SlugFieldStatus.tsx deleted file mode 100644 index ada8a83..0000000 --- a/src/entities/team/ui/SlugFieldStatus.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { CheckCircle, XCircle } from 'lucide-react'; -import { Spinner } from 'shared/ui'; -import { useSlugFieldStatus } from '../lib/useSlugFieldStatus'; - -interface SlugFieldStatusProps { - slug: string; - isDirty: boolean; -} - -export function SlugFieldStatus({ slug, isDirty }: SlugFieldStatusProps) { - const status = useSlugFieldStatus({ isDirty, slug }); - - if (!status) { - return null; - } - - return ( - <> - {status === 'pending' ? ( - - ) : status === 'error' ? ( - - ) : ( - - )} - - ); -} diff --git a/src/entities/user/model/schemas.ts b/src/entities/user/model/schemas.ts index f00c8a1..100fef2 100644 --- a/src/entities/user/model/schemas.ts +++ b/src/entities/user/model/schemas.ts @@ -119,7 +119,6 @@ export const TeamPermissions = z.object({ export const UserTeamResponse = z.object({ id: z.string(), name: z.string(), - slug: z.string(), description: z.string(), avatar: UserAvatarSchema, role: z.string(), diff --git a/src/features/projects/archive/model/useArchiveProject.ts b/src/features/projects/archive/model/useArchiveProject.ts index f7d221b..d537c4e 100644 --- a/src/features/projects/archive/model/useArchiveProject.ts +++ b/src/features/projects/archive/model/useArchiveProject.ts @@ -3,7 +3,7 @@ import { projectFabricKeys, ProjectHttp, type TProject } from 'entities/project' import { toast } from 'sonner'; type ArchiveProjectVariables = { - teamSlug: string; + teamId: string; id: string; }; @@ -15,17 +15,17 @@ export type UseArchiveProjectOptions = Omit< export function useArchiveProject({ onSuccess, ...rest }: UseArchiveProjectOptions = {}) { return useMutation({ ...rest, - mutationFn: ({ teamSlug, id }) => ProjectHttp.archiveProject(teamSlug, id), + mutationFn: ({ teamId, id }) => ProjectHttp.archiveProject(teamId, id), onSuccess: async (res, variables, _r, context) => { onSuccess?.(res, variables, _r, context); toast.success(res.message ?? 'Проект архивирован'); await Promise.all([ context.client.invalidateQueries({ - queryKey: projectFabricKeys.list(variables.teamSlug), + queryKey: projectFabricKeys.list(variables.teamId), }), context.client.invalidateQueries({ - queryKey: projectFabricKeys.detail(variables.teamSlug, variables.id), + queryKey: projectFabricKeys.detail(variables.teamId, variables.id), }), ]); }, diff --git a/src/features/projects/archive/model/useRestoreProject.ts b/src/features/projects/archive/model/useRestoreProject.ts index 44847d5..1b6a558 100644 --- a/src/features/projects/archive/model/useRestoreProject.ts +++ b/src/features/projects/archive/model/useRestoreProject.ts @@ -3,7 +3,7 @@ import { projectFabricKeys, ProjectHttp, type TProject } from 'entities/project' import { toast } from 'sonner'; type RestoreProjectVariables = { - teamSlug: string; + teamId: string; id: string; }; @@ -15,17 +15,17 @@ export type UseRestoreProjectOptions = Omit< export function useRestoreProject({ onSuccess, ...rest }: UseRestoreProjectOptions = {}) { return useMutation({ ...rest, - mutationFn: ({ teamSlug, id }) => ProjectHttp.updateProject(teamSlug, id, { status: 'active' }), + mutationFn: ({ teamId, id }) => ProjectHttp.updateProject(teamId, id, { status: 'active' }), onSuccess: async (res, variables, _r, context) => { onSuccess?.(res, variables, _r, context); toast.success(res.message ?? 'Проект восстановлен'); await Promise.all([ context.client.invalidateQueries({ - queryKey: projectFabricKeys.list(variables.teamSlug), + queryKey: projectFabricKeys.list(variables.teamId), }), context.client.invalidateQueries({ - queryKey: projectFabricKeys.detail(variables.teamSlug, variables.id), + queryKey: projectFabricKeys.detail(variables.teamId, variables.id), }), ]); }, diff --git a/src/features/projects/archive/ui/ArchiveProjectDialog.tsx b/src/features/projects/archive/ui/ArchiveProjectDialog.tsx index c4384f6..0eb111d 100644 --- a/src/features/projects/archive/ui/ArchiveProjectDialog.tsx +++ b/src/features/projects/archive/ui/ArchiveProjectDialog.tsx @@ -16,14 +16,14 @@ import { useArchiveProject } from '../model/useArchiveProject'; interface ArchiveProjectDialogProps extends ComponentProps { projectName: string; - teamSlug: string; + teamId: string; projectId: string; onArchived?: () => void; } export function ArchiveProjectDialog({ projectName, - teamSlug, + teamId, projectId, onArchived, ...props @@ -33,7 +33,7 @@ export function ArchiveProjectDialog({ }); const onArchive = () => { - archiveProject.mutate({ teamSlug, id: projectId }); + archiveProject.mutate({ teamId, id: projectId }); }; return ( diff --git a/src/features/projects/archive/ui/RestoreProjectDialog.tsx b/src/features/projects/archive/ui/RestoreProjectDialog.tsx index 3c144ff..5dda208 100644 --- a/src/features/projects/archive/ui/RestoreProjectDialog.tsx +++ b/src/features/projects/archive/ui/RestoreProjectDialog.tsx @@ -16,20 +16,20 @@ import { useRestoreProject } from '../model/useRestoreProject'; interface RestoreProjectDialogProps extends ComponentProps { projectName: string; - teamSlug: string; + teamId: string; projectId: string; } export function RestoreProjectDialog({ projectName, - teamSlug, + teamId, projectId, ...props }: RestoreProjectDialogProps) { const restoreProject = useRestoreProject(); const onRestore = () => { - restoreProject.mutate({ teamSlug, id: projectId }); + restoreProject.mutate({ teamId, id: projectId }); }; return ( diff --git a/src/features/projects/create/model/useCreateProject.ts b/src/features/projects/create/model/useCreateProject.ts index c3e4ff7..94cda91 100644 --- a/src/features/projects/create/model/useCreateProject.ts +++ b/src/features/projects/create/model/useCreateProject.ts @@ -3,7 +3,7 @@ import { projectFabricKeys, ProjectHttp, type TProject } from 'entities/project' import { toast } from 'sonner'; type CreateProjectVariables = { - teamSlug: string; + teamId: string; body: TProject.CreateProjectBody; }; @@ -15,13 +15,13 @@ export type UseCreateProjectOptions = Omit< export function useCreateProject({ onSuccess, ...rest }: UseCreateProjectOptions = {}) { return useMutation({ ...rest, - mutationFn: ({ teamSlug, body }) => ProjectHttp.createProject(teamSlug, body), + mutationFn: ({ teamId, body }) => ProjectHttp.createProject(teamId, body), onSuccess: async (res, variables, _r, context) => { onSuccess?.(res, variables, _r, context); toast.success(res.message ?? 'Проект создан'); await context.client.invalidateQueries({ - queryKey: projectFabricKeys.list(variables.teamSlug), + queryKey: projectFabricKeys.list(variables.teamId), }); }, }); diff --git a/src/features/projects/create/model/useCreateProjectForm.ts b/src/features/projects/create/model/useCreateProjectForm.ts index e265595..dfb6079 100644 --- a/src/features/projects/create/model/useCreateProjectForm.ts +++ b/src/features/projects/create/model/useCreateProjectForm.ts @@ -10,7 +10,7 @@ import type { CreateProjectFormValues } from './types'; import { useCreateProject, type UseCreateProjectOptions } from './useCreateProject'; export function useCreateProjectForm(options: UseCreateProjectOptions = {}) { - const teamSlug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const form = useForm({ resolver: zodResolver(CreateProjectFormSchema), @@ -29,7 +29,7 @@ export function useCreateProjectForm(options: UseCreateProjectOptions = {}) { }); const onSubmit = (data: CreateProjectFormValues) => { - if (!teamSlug) return; + if (!teamId) return; const body: TProject.CreateProjectBody = { name: data.name.trim(), @@ -40,12 +40,12 @@ export function useCreateProjectForm(options: UseCreateProjectOptions = {}) { ...(data.color ? { color: data.color } : {}), }; - createProject.mutate({ teamSlug, body }); + createProject.mutate({ teamId, body }); }; return { form, - teamSlug, + teamId, isPending: createProject.isPending, handleSubmit: form.handleSubmit(onSubmit), }; diff --git a/src/features/projects/remove/model/useRemoveProject.ts b/src/features/projects/remove/model/useRemoveProject.ts index 43c6cb1..638add9 100644 --- a/src/features/projects/remove/model/useRemoveProject.ts +++ b/src/features/projects/remove/model/useRemoveProject.ts @@ -3,7 +3,7 @@ import { projectFabricKeys, ProjectHttp, type TProject } from 'entities/project' import { toast } from 'sonner'; type RemoveProjectVariables = { - teamSlug: string; + teamId: string; id: string; }; @@ -15,13 +15,13 @@ export type UseRemoveProjectOptions = Omit< export function useRemoveProject({ onSuccess, ...rest }: UseRemoveProjectOptions = {}) { return useMutation({ ...rest, - mutationFn: ({ teamSlug, id }) => ProjectHttp.removeProject(teamSlug, id), + mutationFn: ({ teamId, id }) => ProjectHttp.removeProject(teamId, id), onSuccess: async (res, variables, _r, context) => { onSuccess?.(res, variables, _r, context); toast.success(res.message ?? 'Проект удалён'); await context.client.invalidateQueries({ - queryKey: projectFabricKeys.list(variables.teamSlug), + queryKey: projectFabricKeys.list(variables.teamId), }); }, }); diff --git a/src/features/projects/remove/ui/RemoveProjectDialog.tsx b/src/features/projects/remove/ui/RemoveProjectDialog.tsx index 490f4a2..c892c6f 100644 --- a/src/features/projects/remove/ui/RemoveProjectDialog.tsx +++ b/src/features/projects/remove/ui/RemoveProjectDialog.tsx @@ -15,18 +15,18 @@ import { useRemoveProject } from '../model/useRemoveProject'; interface Props extends ComponentProps { projectName: string; - teamSlug: string; + teamId: string; projectId: string; } -export function RemoveProjectDialog({ projectName, teamSlug, projectId, ...props }: Props) { +export function RemoveProjectDialog({ projectName, teamId, projectId, ...props }: Props) { const [inputValue, setInputValue] = useState(''); const removeProject = useRemoveProject(); const isMatch = inputValue.trim() === projectName.trim(); const onRemove = () => { - removeProject.mutate({ teamSlug, id: projectId }); + removeProject.mutate({ teamId, id: projectId }); }; return ( diff --git a/src/features/projects/share/model/useShareProject.ts b/src/features/projects/share/model/useShareProject.ts index 7519043..9b3dfed 100644 --- a/src/features/projects/share/model/useShareProject.ts +++ b/src/features/projects/share/model/useShareProject.ts @@ -3,7 +3,7 @@ import { projectFabricKeys, ProjectHttp, type TProject } from 'entities/project' import { toast } from 'sonner'; type ShareProjectVariables = { - teamSlug: string; + teamId: string; id: string; body?: TProject.CreateShareTokenBody; }; @@ -16,13 +16,13 @@ export type UseShareProjectOptions = Omit< export function useShareProject({ onSuccess, ...rest }: UseShareProjectOptions = {}) { return useMutation({ ...rest, - mutationFn: ({ teamSlug, id, body = {} }) => ProjectHttp.createShareToken(teamSlug, id, body), + mutationFn: ({ teamId, id, body = {} }) => ProjectHttp.createShareToken(teamId, id, body), onSuccess: async (res, variables, _r, context) => { onSuccess?.(res, variables, _r, context); toast.success(res.message ?? 'Ссылка для доступа создана'); await context.client.invalidateQueries({ - queryKey: projectFabricKeys.detail(variables.teamSlug, variables.id), + queryKey: projectFabricKeys.detail(variables.teamId, variables.id), }); }, }); diff --git a/src/features/projects/share/ui/ShareProjectDialog.tsx b/src/features/projects/share/ui/ShareProjectDialog.tsx index 2adf5da..508def8 100644 --- a/src/features/projects/share/ui/ShareProjectDialog.tsx +++ b/src/features/projects/share/ui/ShareProjectDialog.tsx @@ -37,14 +37,14 @@ import { useShareProject } from '../model/useShareProject'; interface ShareProjectDialogProps extends ComponentProps { projectName: string; - teamSlug: string; + teamId: string; projectId: string; dialog?: ComponentProps; } export function ShareProjectDialog({ projectName, - teamSlug, + teamId, projectId, dialog = {}, ...props @@ -81,7 +81,7 @@ export function ShareProjectDialog({ const onCreateLink = () => { shareProject.mutate({ - teamSlug, //todo не будет + teamId, //todo не будет id: projectId, body: ttlOptionToBody(ttlOption), }); diff --git a/src/features/teams/active-team/index.ts b/src/features/teams/active-team/index.ts index bf5608f..48b9dc0 100644 --- a/src/features/teams/active-team/index.ts +++ b/src/features/teams/active-team/index.ts @@ -1,2 +1,2 @@ export { useSwitchTeam } from './model/useSwitchTeam'; -export { TeamSlugSync } from './ui/TeamSlugSync'; +export { TeamIdSync } from './ui/TeamTeamIdSync'; diff --git a/src/features/teams/active-team/model/useSwitchTeam.ts b/src/features/teams/active-team/model/useSwitchTeam.ts index f5dbe7a..050f851 100644 --- a/src/features/teams/active-team/model/useSwitchTeam.ts +++ b/src/features/teams/active-team/model/useSwitchTeam.ts @@ -19,26 +19,26 @@ interface UseSwitchTeamProps { export function useSwitchTeam({ teams = [], defaultOptions = {} }: UseSwitchTeamProps = {}) { const router = useRouter(); - const setSlug = useTeamStore.use.setSlug(); + const setTeamId = useTeamStore.use.setTeamId(); const switchTeam = useCallback( - (slug: string, options: SwitchTeamOptions = {}) => { + (teamId: string, options: SwitchTeamOptions = {}) => { const { redirect = false, showToast = true } = { ...defaultOptions, ...options }; - const team = teams.find((t) => t.slug === slug); + const team = teams.find((t) => t.id === teamId); if (!team) { if (showToast) toast.error('Команда не найдена!'); return; } - setSlug(slug); + setTeamId(teamId); if (showToast) toast.success(`Вы сменили команду на "${team.name}"`); if (redirect) { router.push(routes.team.root()); } }, - [setSlug, teams, router, defaultOptions] + [setTeamId, teams, router, defaultOptions] ); return { switchTeam }; diff --git a/src/features/teams/active-team/model/useTeamsQueryWithSlugSync.ts b/src/features/teams/active-team/model/useTeamsQueryWithSlugSync.ts deleted file mode 100644 index c4e84fa..0000000 --- a/src/features/teams/active-team/model/useTeamsQueryWithSlugSync.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { UserQueries } from 'entities/user'; -import { useEffect } from 'react'; -import { useTeamStore } from 'entities/team'; - -export function useTeamsQueryWithSlugSync() { - const query = useQuery(UserQueries.getMyTeams()); - const slug = useTeamStore.use.slug(); - const setCurrentTeamSlug = useTeamStore.use.setSlug(); - - useEffect(() => { - if (!query.data) return; - - const items = query.data.items; - const hasTeamSlug = !!slug && items.some((d) => d.slug === slug); - - if (hasTeamSlug) return; - - setCurrentTeamSlug(items[0]?.slug); - }, [slug, setCurrentTeamSlug, query.data]); - - return { query, slug }; -} diff --git a/src/features/teams/active-team/model/useTeamsQueryWithTeamIdSync.ts b/src/features/teams/active-team/model/useTeamsQueryWithTeamIdSync.ts new file mode 100644 index 0000000..4a850b1 --- /dev/null +++ b/src/features/teams/active-team/model/useTeamsQueryWithTeamIdSync.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { UserQueries } from 'entities/user'; +import { useEffect } from 'react'; +import { useTeamStore } from 'entities/team'; + +export function useTeamsQueryWithTeamIdSync() { + const query = useQuery(UserQueries.getMyTeams()); + const teamId = useTeamStore.use.teamId(); + const setCurrentTeamId = useTeamStore.use.setTeamId(); + + useEffect(() => { + if (!query.data) return; + + const items = query.data.items; + const hasTeamId = !!teamId && items.some((d) => d.id === teamId); + + if (hasTeamId) return; + + setCurrentTeamId(items[0]?.id); + }, [teamId, setCurrentTeamId, query.data]); + + return { query, teamId }; +} diff --git a/src/features/teams/active-team/ui/TeamSlugSync.tsx b/src/features/teams/active-team/ui/TeamSlugSync.tsx deleted file mode 100644 index c972a5b..0000000 --- a/src/features/teams/active-team/ui/TeamSlugSync.tsx +++ /dev/null @@ -1,8 +0,0 @@ -'use client'; - -import { useTeamsQueryWithSlugSync } from '../model/useTeamsQueryWithSlugSync'; - -export function TeamSlugSync() { - useTeamsQueryWithSlugSync(); - return null; -} diff --git a/src/features/teams/active-team/ui/TeamTeamIdSync.tsx b/src/features/teams/active-team/ui/TeamTeamIdSync.tsx new file mode 100644 index 0000000..17134df --- /dev/null +++ b/src/features/teams/active-team/ui/TeamTeamIdSync.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { useTeamsQueryWithTeamIdSync } from '../model/useTeamsQueryWithTeamIdSync'; + +export function TeamIdSync() { + useTeamsQueryWithTeamIdSync(); + return null; +} diff --git a/src/features/teams/create/model/useCreateTeamForm.ts b/src/features/teams/create/model/useCreateTeamForm.ts index 89bfb2a..e5b69e2 100644 --- a/src/features/teams/create/model/useCreateTeamForm.ts +++ b/src/features/teams/create/model/useCreateTeamForm.ts @@ -1,23 +1,18 @@ -import { type TTeam, useCheckSlug, validateTeamSlugAsync } from 'entities/team'; +import { type TTeam } from 'entities/team'; import { useForm } from 'react-hook-form'; import { extractValidationIssues } from 'shared/api'; -import { useZodValidationWithAsyncCheck } from 'shared/lib/hooks'; import { setFormErrors } from 'shared/lib/utils'; import { CreateTeamFormSchema } from './schemas'; import type { CreateTeamFormValues } from './types'; import { useCreateTeam, type UseCreateTeamOptions } from './useCreateTeam'; +import { zodResolver } from '@hookform/resolvers/zod'; export function useCreateTeamForm(mutateOptions: UseCreateTeamOptions = {}) { - const checkSlug = useCheckSlug(''); - const form = useForm({ - resolver: useZodValidationWithAsyncCheck(CreateTeamFormSchema, (...args) => - validateTeamSlugAsync(checkSlug, ...args) - ), + resolver: zodResolver(CreateTeamFormSchema), defaultValues: { name: '', description: '', - slug: '', }, }); @@ -36,7 +31,6 @@ export function useCreateTeamForm(mutateOptions: UseCreateTeamOptions = {}) { const body: TTeam.CreateTeamBody = { name: data.name.trim(), description: data.description.trim(), - ...(data.slug?.trim() ? { slug: data.slug.trim() } : {}), }; createTeam.mutate(body); diff --git a/src/features/teams/create/ui/CreateTeamForm.tsx b/src/features/teams/create/ui/CreateTeamForm.tsx index c6717e0..37a2d0c 100644 --- a/src/features/teams/create/ui/CreateTeamForm.tsx +++ b/src/features/teams/create/ui/CreateTeamForm.tsx @@ -1,6 +1,5 @@ 'use client'; -import { SlugField } from 'entities/team'; import { ComponentProps } from 'react'; import { Controller, FormProvider } from 'react-hook-form'; import { cn } from 'shared/lib/utils'; @@ -39,7 +38,6 @@ export function CreateTeamForm({ className, mutateOptions, ...props }: CreateTea )} /> - , @@ -10,17 +10,17 @@ export type UseInviteTeamMemberOptions = Omit< >; export function useInviteTeamMember({ onSuccess, ...rest }: UseInviteTeamMemberOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useMutation({ ...rest, - mutationFn: ({ slug, body }) => TeamHttp.inviteMember(slug, body), + mutationFn: ({ teamId, body }) => TeamHttp.inviteMember(teamId, body), onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success(res.message ?? 'Приглашение отправлено'); - if (slug) { - await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(slug) }); + if (teamId) { + await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(teamId) }); } }, }); diff --git a/src/features/teams/invite/model/useInviteTeamMemberForm.ts b/src/features/teams/invite/model/useInviteTeamMemberForm.ts index 9686df4..6b7c801 100644 --- a/src/features/teams/invite/model/useInviteTeamMemberForm.ts +++ b/src/features/teams/invite/model/useInviteTeamMemberForm.ts @@ -10,7 +10,7 @@ import type { InviteTeamMemberFormValues } from './types'; import { useInviteTeamMember, type UseInviteTeamMemberOptions } from './useInviteTeamMember'; export function useInviteTeamMemberForm(mutateOptions: UseInviteTeamMemberOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const form = useForm({ resolver: zodResolver(InviteTeamMemberFormSchema), @@ -32,8 +32,8 @@ export function useInviteTeamMemberForm(mutateOptions: UseInviteTeamMemberOption }); const onSubmit = (data: InviteTeamMemberFormValues) => { - if (!slug) return; - inviteTeamMember.mutate({ slug, body: data }); + if (!teamId) return; + inviteTeamMember.mutate({ teamId, body: data }); }; return { diff --git a/src/features/teams/remove/ui/RemoveTeamDialog.tsx b/src/features/teams/remove/ui/RemoveTeamDialog.tsx index 9ba881a..621f87e 100644 --- a/src/features/teams/remove/ui/RemoveTeamDialog.tsx +++ b/src/features/teams/remove/ui/RemoveTeamDialog.tsx @@ -15,17 +15,17 @@ import { useRemoveTeam } from '../model/useRemoveTeam'; interface Props extends ComponentProps { teamName: string; - slug: string; + teamId: string; } -export function RemoveTeamDialog({ teamName, slug, ...props }: Props) { +export function RemoveTeamDialog({ teamName, teamId, ...props }: Props) { const [inputValue, setInputValue] = useState(''); const removeTeam = useRemoveTeam(); const isMatch = inputValue.trim() === teamName.trim(); const onRemove = () => { - removeTeam.mutate(slug); + removeTeam.mutate(teamId); }; return ( diff --git a/src/pages/profile/ui/teams-page/TeamList.tsx b/src/pages/profile/ui/teams-page/TeamList.tsx index ea80b12..fb7af79 100644 --- a/src/pages/profile/ui/teams-page/TeamList.tsx +++ b/src/pages/profile/ui/teams-page/TeamList.tsx @@ -19,7 +19,7 @@ import { useSwitchTeam } from 'features/teams/active-team'; export function TeamsList() { const teamsQuery = useQuery(UserQueries.getMyTeams()); - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const { switchTeam } = useSwitchTeam({ teams: teamsQuery.data?.items, @@ -71,7 +71,7 @@ export function TeamsList() {
- + @@ -80,10 +80,10 @@ export function TeamsList() { type="button" size="sm" variant="link" - onClick={() => switchTeam(team.slug)} - disabled={slug === team.slug} + onClick={() => switchTeam(team.id)} + disabled={teamId === team.id} > - {slug === team.slug ? 'Текущая' : 'Перейти'} + {teamId === team.id ? 'Текущая' : 'Перейти'}
diff --git a/src/pages/project/api/useQueryProject.ts b/src/pages/project/api/useQueryProject.ts index fa1dfb1..b40624b 100644 --- a/src/pages/project/api/useQueryProject.ts +++ b/src/pages/project/api/useQueryProject.ts @@ -4,12 +4,12 @@ import { useTeamStore } from 'entities/team'; import { useParams } from 'next/navigation'; export function useQueryProject() { - const teamSlug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const params = useParams(); const projectId = typeof params?.projectId === 'string' ? params.projectId : undefined; return useQuery({ - ...ProjectQueries.getProject(teamSlug!, projectId!), - enabled: Boolean(teamSlug && projectId), + ...ProjectQueries.getProject(teamId!, projectId!), + enabled: Boolean(teamId && projectId), }); } diff --git a/src/pages/project/api/useUpdateProject.ts b/src/pages/project/api/useUpdateProject.ts index d73ee0e..6c07d27 100644 --- a/src/pages/project/api/useUpdateProject.ts +++ b/src/pages/project/api/useUpdateProject.ts @@ -10,29 +10,29 @@ type UseUpdateProjectProps = Omit< >; export function useUpdateProject({ onSuccess, ...rest }: UseUpdateProjectProps = {}) { - const teamSlug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const params = useParams(); const projectId = typeof params?.projectId === 'string' ? params.projectId : undefined; return useMutation({ ...rest, mutationFn: (data) => { - if (!teamSlug || !projectId) { + if (!teamId || !projectId) { throw new Error('Не выбран проект'); } - return ProjectHttp.updateProject(teamSlug, projectId, data); + return ProjectHttp.updateProject(teamId, projectId, data); }, onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success(res.message ?? 'Проект обновлён'); - if (teamSlug && projectId) { + if (teamId && projectId) { await Promise.all([ context.client.invalidateQueries({ - queryKey: projectFabricKeys.detail(teamSlug, projectId), + queryKey: projectFabricKeys.detail(teamId, projectId), }), context.client.invalidateQueries({ - queryKey: projectFabricKeys.list(teamSlug), + queryKey: projectFabricKeys.list(teamId), }), ]); } diff --git a/src/pages/project/ui/settings/ProjectDangerZone.tsx b/src/pages/project/ui/settings/ProjectDangerZone.tsx index f7bb975..06b5410 100644 --- a/src/pages/project/ui/settings/ProjectDangerZone.tsx +++ b/src/pages/project/ui/settings/ProjectDangerZone.tsx @@ -14,11 +14,11 @@ import { interface ProjectDangerZoneProps { projectName: string; - teamSlug: string; + teamId: string; projectId: string; } -export function ProjectDangerZone({ projectName, teamSlug, projectId }: ProjectDangerZoneProps) { +export function ProjectDangerZone({ projectName, teamId, projectId }: ProjectDangerZoneProps) { return ( @@ -33,7 +33,7 @@ export function ProjectDangerZone({ projectName, teamSlug, projectId }: ProjectD diff --git a/src/pages/project/ui/settings/ProjectSettingsPage.tsx b/src/pages/project/ui/settings/ProjectSettingsPage.tsx index 52c0781..65a50ad 100644 --- a/src/pages/project/ui/settings/ProjectSettingsPage.tsx +++ b/src/pages/project/ui/settings/ProjectSettingsPage.tsx @@ -23,7 +23,7 @@ import { ProjectDangerZone } from './ProjectDangerZone'; import { ProjectSettingsSaveBar } from './ProjectSettingsSaveBar'; export function ProjectSettingsPage() { - const teamSlug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const projectQuery = useQueryProject(); const project = projectQuery.data; @@ -117,13 +117,9 @@ export function ProjectSettingsPage() { - {project.access.canDelete && teamSlug && ( + {project.access.canDelete && teamId && ( - + )} diff --git a/src/pages/team/api/useQueryInvitations.ts b/src/pages/team/api/useQueryInvitations.ts index 0c78450..ee1d4ea 100644 --- a/src/pages/team/api/useQueryInvitations.ts +++ b/src/pages/team/api/useQueryInvitations.ts @@ -2,10 +2,10 @@ import { useQuery } from '@tanstack/react-query'; import { TeamQueries, useTeamStore } from 'entities/team'; export function useQueryInvitations() { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useQuery({ - ...TeamQueries.getInvitations(slug!), - enabled: Boolean(slug), + ...TeamQueries.getInvitations(teamId!), + enabled: Boolean(teamId), }); } diff --git a/src/pages/team/api/useQueryTeam.ts b/src/pages/team/api/useQueryTeam.ts index 43a8601..596f758 100644 --- a/src/pages/team/api/useQueryTeam.ts +++ b/src/pages/team/api/useQueryTeam.ts @@ -2,10 +2,10 @@ import { useQuery } from '@tanstack/react-query'; import { TeamQueries, useTeamStore } from 'entities/team'; export function useQueryTeam() { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useQuery({ - ...TeamQueries.getTeam(slug!), - enabled: Boolean(slug), + ...TeamQueries.getTeam(teamId!), + enabled: Boolean(teamId), }); } diff --git a/src/pages/team/api/useRemoveMember.ts b/src/pages/team/api/useRemoveMember.ts index 7495dac..de324a2 100644 --- a/src/pages/team/api/useRemoveMember.ts +++ b/src/pages/team/api/useRemoveMember.ts @@ -8,22 +8,22 @@ type UseRemoveMemberOptions = Omit< >; export function useRemoveMember({ onSuccess, ...rest }: UseRemoveMemberOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useMutation({ ...rest, mutationFn: (userId) => { - if (!slug) { + if (!teamId) { throw new Error('Не выбрана команда'); } - return TeamHttp.removeMember(slug, userId); + return TeamHttp.removeMember(teamId, userId); }, onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success(res.message ?? 'Участник удалён из команды'); - if (slug) { - await context.client.invalidateQueries({ queryKey: teamFabricKeys.members(slug) }); + if (teamId) { + await context.client.invalidateQueries({ queryKey: teamFabricKeys.members(teamId) }); } }, }); diff --git a/src/pages/team/api/useRemoveMemberInvitation.ts b/src/pages/team/api/useRemoveMemberInvitation.ts index f97bbe5..ed9ee8b 100644 --- a/src/pages/team/api/useRemoveMemberInvitation.ts +++ b/src/pages/team/api/useRemoveMemberInvitation.ts @@ -11,22 +11,22 @@ export function useRemoveMemberInvitation({ onSuccess, ...rest }: UseRemoveMemberInvitationOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useMutation({ ...rest, mutationFn: (code) => { - if (!slug) { + if (!teamId) { throw new Error('Не выбрана команда'); } - return TeamHttp.removeInvitation(slug, code); + return TeamHttp.removeInvitation(teamId, code); }, onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success(res.message ?? 'Приглашение отозвано'); - if (slug) { - await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(slug) }); + if (teamId) { + await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(teamId) }); } }, }); diff --git a/src/pages/team/api/useUpdateInvitation.ts b/src/pages/team/api/useUpdateInvitation.ts index 8384ab7..6139666 100644 --- a/src/pages/team/api/useUpdateInvitation.ts +++ b/src/pages/team/api/useUpdateInvitation.ts @@ -10,22 +10,22 @@ type UseUpdateInvitationOptions = Omit< >; export function useUpdateInvitation({ onSuccess, ...rest }: UseUpdateInvitationOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useMutation({ ...rest, mutationFn: ({ code, ...data }) => { - if (!slug) { + if (!teamId) { throw new Error('Не выбрана команда'); } - return TeamHttp.updateInvitation(slug, code, data); + return TeamHttp.updateInvitation(teamId, code, data); }, onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success('Роль в приглашении обновлена'); - if (slug) { - await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(slug) }); + if (teamId) { + await context.client.invalidateQueries({ queryKey: teamFabricKeys.invitations(teamId) }); } }, }); diff --git a/src/pages/team/api/useUpdateMember.ts b/src/pages/team/api/useUpdateMember.ts index 27ad8af..feda5bc 100644 --- a/src/pages/team/api/useUpdateMember.ts +++ b/src/pages/team/api/useUpdateMember.ts @@ -10,22 +10,22 @@ type UseUpdateMemberOptions = Omit< >; export function useUpdateMember({ onSuccess, ...rest }: UseUpdateMemberOptions = {}) { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); return useMutation({ ...rest, mutationFn: ({ userId, ...data }) => { - if (!slug) { + if (!teamId) { throw new Error('Не выбрана команда'); } - return TeamHttp.updateMember(slug, userId, data); + return TeamHttp.updateMember(teamId, userId, data); }, onSuccess: async (res, _v, _r, context) => { onSuccess?.(res, _v, _r, context); toast.success(res.message ?? 'Данные участника обновлены'); - if (slug) { - await context.client.invalidateQueries({ queryKey: teamFabricKeys.members(slug) }); + if (teamId) { + await context.client.invalidateQueries({ queryKey: teamFabricKeys.members(teamId) }); } }, }); diff --git a/src/pages/team/api/useUpdateTeam.ts b/src/pages/team/api/useUpdateTeam.ts index 18aadda..aea12f8 100644 --- a/src/pages/team/api/useUpdateTeam.ts +++ b/src/pages/team/api/useUpdateTeam.ts @@ -9,16 +9,16 @@ type UseUpdateTeamProps = Omit< >; export function useUpdateTeam({ onSuccess, ...rest }: UseUpdateTeamProps = {}) { - const slug = useTeamStore.use.slug(); - const setSlug = useTeamStore.use.setSlug(); + const teamId = useTeamStore.use.teamId(); + // TODO return useMutation({ ...rest, mutationFn: (data) => { - if (!slug) { + if (!teamId) { throw new Error('Не выбрана команда'); } - return TeamHttp.updateTeam(slug, data); + return TeamHttp.updateTeam(teamId, data); }, onSuccess: async (res, v, _r, context) => { onSuccess?.(res, v, _r, context); @@ -26,16 +26,12 @@ export function useUpdateTeam({ onSuccess, ...rest }: UseUpdateTeamProps = {}) { await Promise.all([ context.client.invalidateQueries({ - queryKey: teamFabricKeys.bySlug(v.slug ?? slug!), + queryKey: teamFabricKeys.byId(teamId!), }), context.client.invalidateQueries({ queryKey: userFabricKeys.myTeams(), }), ]); - - if (v.slug) { - setSlug(v.slug); - } }, }); } diff --git a/src/pages/team/model/useMembersPage.ts b/src/pages/team/model/useMembersPage.ts index 9281695..fab201a 100644 --- a/src/pages/team/model/useMembersPage.ts +++ b/src/pages/team/model/useMembersPage.ts @@ -6,8 +6,8 @@ import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from 'shared/lib/utils'; export function useMembersPage() { - const slug = useTeamStore.use.slug(); - const { data, isPending } = useQuery(TeamQueries.getMembers(slug!)); + const teamId = useTeamStore.use.teamId(); + const { data, isPending } = useQuery(TeamQueries.getMembers(teamId!)); const [search, setSearch] = useState(''); const [filtered, setFiltered] = useState([]); diff --git a/src/pages/team/ui/projects/ProjectCard.tsx b/src/pages/team/ui/projects/ProjectCard.tsx index 202582c..be85f17 100644 --- a/src/pages/team/ui/projects/ProjectCard.tsx +++ b/src/pages/team/ui/projects/ProjectCard.tsx @@ -50,7 +50,7 @@ export function ProjectCard({ statusLabel: statusLabelProp, ...props }: ProjectCardProps) { - const teamSlug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const name = nameProp ?? project?.name ?? 'Atlas Platform'; const description = descriptionProp ?? (project ? `Ключ проекта: ${project.key}` : 'Core team workspace.'); @@ -64,7 +64,7 @@ export function ProjectCard({ const mockMembersCount = project ? (project.id.charCodeAt(1) % 3) + 2 : 3; const mockMembers = Array.from({ length: mockMembersCount }).map((_, i) => i + 1); - const projectHref = project && teamSlug ? routes.team.project.root(project.id) : null; + const projectHref = project && teamId ? routes.team.project.root(project.id) : null; const card = ( e.preventDefault()}>Поделиться {project?.status === 'archived' ? ( e.preventDefault()}> Восстановить @@ -174,10 +174,10 @@ export function ProjectCard({ project?.status !== 'template' && ( e.preventDefault()}> Архивировать @@ -187,10 +187,10 @@ export function ProjectCard({ )} e.preventDefault()}> Удалить diff --git a/src/pages/team/ui/projects/ProjectsPage.tsx b/src/pages/team/ui/projects/ProjectsPage.tsx index 75799f9..a8b7a39 100644 --- a/src/pages/team/ui/projects/ProjectsPage.tsx +++ b/src/pages/team/ui/projects/ProjectsPage.tsx @@ -11,10 +11,10 @@ import { ProjectCardSkeleton } from './ProjectCard.skeleton'; import { ProjectsEmpty } from './ProjectsEmpty'; export function ProjectsPage() { - const slug = useTeamStore.use.slug(); + const teamId = useTeamStore.use.teamId(); const { data, isPending } = useQuery({ - ...ProjectQueries.getProjects(slug!), - enabled: !!slug, + ...ProjectQueries.getProjects(teamId!), + enabled: !!teamId, }); if (!isPending && !data?.items.length) { @@ -25,7 +25,7 @@ export function ProjectsPage() { <>
- diff --git a/src/pages/team/ui/settings/DangerZone.tsx b/src/pages/team/ui/settings/DangerZone.tsx index e33ea6e..9aef23a 100644 --- a/src/pages/team/ui/settings/DangerZone.tsx +++ b/src/pages/team/ui/settings/DangerZone.tsx @@ -12,10 +12,10 @@ import { RemoveTeamDialog } from 'features/teams/remove'; interface Props { teamName: string; - slug: string; + teamId: string; } -export function DangerZone({ teamName, slug }: Props) { +export function DangerZone({ teamName, teamId }: Props) { return ( @@ -29,7 +29,7 @@ export function DangerZone({ teamName, slug }: Props) { - + diff --git a/src/pages/team/ui/settings/SaveBar.tsx b/src/pages/team/ui/settings/SaveBar.tsx index 17b2e5b..52cb744 100644 --- a/src/pages/team/ui/settings/SaveBar.tsx +++ b/src/pages/team/ui/settings/SaveBar.tsx @@ -17,7 +17,6 @@ export function SaveBar({ team }: { team: TTeam.TeamDetailsResponse }) { const onSubmit = (data: TeamSettingsFormValues) => { const body: TTeam.UpdateTeamBody = { ...(dirtyFields.name && { name: data.name?.trim() }), - ...(dirtyFields.slug && { slug: data.slug?.trim() }), ...(dirtyFields.description && { description: data.description?.trim() }), }; updateTeam.mutateAsync(body); diff --git a/src/pages/team/ui/settings/SettingsPage.tsx b/src/pages/team/ui/settings/SettingsPage.tsx index 724e874..94c4045 100644 --- a/src/pages/team/ui/settings/SettingsPage.tsx +++ b/src/pages/team/ui/settings/SettingsPage.tsx @@ -1,9 +1,7 @@ 'use client'; -import { useCheckSlug, validateTeamSlugAsync } from 'entities/team'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { useZodValidationWithAsyncCheck } from 'shared/lib/hooks'; import { useQueryTeam } from '../../api/useQueryTeam'; import { TeamSettingsFormSchema, type TeamSettingsFormValues } from '../../model/settings'; import { DangerZone } from './DangerZone'; @@ -15,20 +13,17 @@ import { DangerZoneSkeleton } from './skeletons/DangerZone.skeleton'; import { DefaultSettingsSkeleton } from './skeletons/DefaultSettings.skeleton'; import { InvitationSecuritySkeleton } from './skeletons/InvitationSecurity.skeleton'; import { TeamIdentitySkeleton } from './skeletons/TeamIdentity.skeleton'; +import { zodResolver } from '@hookform/resolvers/zod'; export function Settings() { const teamQuery = useQueryTeam(); const team = teamQuery.data; - const checkSlug = useCheckSlug(team?.slug ?? ''); const form = useForm({ - resolver: useZodValidationWithAsyncCheck(TeamSettingsFormSchema, (...args) => - validateTeamSlugAsync(checkSlug, ...args) - ), + resolver: zodResolver(TeamSettingsFormSchema), mode: 'onChange', defaultValues: { name: '', - slug: '', description: '', }, }); @@ -39,7 +34,6 @@ export function Settings() { if (team) { reset({ name: team.name, - slug: team.slug, description: team.description || '', }); } @@ -60,7 +54,7 @@ export function Settings() { {team ? : } {team ? : } {team ? : } - {team ? : } + {team ? : } {team ? : null} diff --git a/src/pages/team/ui/settings/TeamIdentityForm.tsx b/src/pages/team/ui/settings/TeamIdentityForm.tsx index aaf6271..284c77c 100644 --- a/src/pages/team/ui/settings/TeamIdentityForm.tsx +++ b/src/pages/team/ui/settings/TeamIdentityForm.tsx @@ -1,15 +1,12 @@ 'use client'; -import { SlugField } from 'entities/team'; import { ComponentProps, useId } from 'react'; import { Controller, useFormContext, useFormState } from 'react-hook-form'; import { Field, FieldError, FieldLabel, Input, Textarea } from 'shared/ui'; -import { getTeamPathPrefix } from '../../model/team-identity'; export function TeamIdentityForm(props: Omit, 'children'>) { const idName = useId(); const idDescription = useId(); - const teamPathPrefix = getTeamPathPrefix(); const form = useFormContext(); const { isSubmitting } = useFormState({ control: form.control }); @@ -35,33 +32,26 @@ export function TeamIdentityForm(props: Omit, 'children'>) )} /> - ( + + Описание команды +