@taskless-app/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/dist/branding.d.ts +32 -0
- package/dist/branding.js +28 -0
- package/dist/branding.js.map +1 -0
- package/dist/chatMessageKinds.d.ts +2 -0
- package/dist/chatMessageKinds.js +10 -0
- package/dist/chatMessageKinds.js.map +1 -0
- package/dist/designTokens.d.ts +190 -0
- package/dist/designTokens.js +249 -0
- package/dist/designTokens.js.map +1 -0
- package/dist/domain.d.ts +2 -0
- package/dist/domain.js +8 -0
- package/dist/domain.js.map +1 -0
- package/dist/finishChip.d.ts +18 -0
- package/dist/finishChip.js +95 -0
- package/dist/finishChip.js.map +1 -0
- package/dist/gender.d.ts +6 -0
- package/dist/gender.js +11 -0
- package/dist/gender.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/labelColors.d.ts +3 -0
- package/dist/labelColors.js +33 -0
- package/dist/labelColors.js.map +1 -0
- package/dist/notifications.d.ts +36 -0
- package/dist/notifications.js +2 -0
- package/dist/notifications.js.map +1 -0
- package/dist/releaseSources.d.ts +20 -0
- package/dist/releaseSources.js +44 -0
- package/dist/releaseSources.js.map +1 -0
- package/dist/schemas.d.ts +1929 -0
- package/dist/schemas.js +655 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sortWorkflowStates.d.ts +5 -0
- package/dist/sortWorkflowStates.js +18 -0
- package/dist/sortWorkflowStates.js.map +1 -0
- package/dist/statusIcons.d.ts +2 -0
- package/dist/statusIcons.js +40 -0
- package/dist/statusIcons.js.map +1 -0
- package/dist/translit.d.ts +11 -0
- package/dist/translit.js +41 -0
- package/dist/translit.js.map +1 -0
- package/dist/uiTour.d.ts +48 -0
- package/dist/uiTour.js +175 -0
- package/dist/uiTour.js.map +1 -0
- package/dist/userHandle.d.ts +14 -0
- package/dist/userHandle.js +25 -0
- package/dist/userHandle.js.map +1 -0
- package/dist/userHue.d.ts +1 -0
- package/dist/userHue.js +24 -0
- package/dist/userHue.js.map +1 -0
- package/dist/weight.d.ts +11 -0
- package/dist/weight.js +71 -0
- package/dist/weight.js.map +1 -0
- package/dist/workspaceBrand.d.ts +5 -0
- package/dist/workspaceBrand.js +39 -0
- package/dist/workspaceBrand.js.map +1 -0
- package/package.json +32 -0
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { STATUS_ICONS } from "./statusIcons.js";
|
|
3
|
+
import { ReleaseAllowedSources } from "./releaseSources.js";
|
|
4
|
+
export const SignupInput = z.object({
|
|
5
|
+
email: z.string().email(),
|
|
6
|
+
password: z.string().min(6).max(256),
|
|
7
|
+
displayName: z.string().min(1).max(80),
|
|
8
|
+
inviteToken: z.string().optional(),
|
|
9
|
+
// TAS-177: filled on the invite/link signup form so the profile is complete
|
|
10
|
+
// immediately. position → User.position (global free text). workRole →
|
|
11
|
+
// WorkspaceMember.workRole (per-workspace enum). Both ignored on non-invite
|
|
12
|
+
// public/bootstrap signup (no workspace to attach workRole to).
|
|
13
|
+
position: z.string().trim().max(120).optional(),
|
|
14
|
+
workRole: z.enum(["lead", "product", "dev", "design", "qa", "other"]).optional(),
|
|
15
|
+
// Пол — для согласования глаголов в ленте новостей. Опционален в схеме
|
|
16
|
+
// (bootstrap/тесты не обязаны слать), но формы регистрации требуют выбор.
|
|
17
|
+
gender: z.enum(["male", "female"]).optional(),
|
|
18
|
+
});
|
|
19
|
+
export const LoginInput = z.object({
|
|
20
|
+
email: z.string().email(),
|
|
21
|
+
password: z.string().min(1).max(256),
|
|
22
|
+
});
|
|
23
|
+
export const CreateWorkspaceInput = z.object({
|
|
24
|
+
name: z.string().min(1).max(80),
|
|
25
|
+
slug: z.string().regex(/^[a-z0-9][a-z0-9-]{1,22}[a-z0-9]$/).optional(),
|
|
26
|
+
avatarUrl: z.string().max(500).optional(),
|
|
27
|
+
});
|
|
28
|
+
export const CreateInviteInput = z.object({
|
|
29
|
+
email: z.string().email(),
|
|
30
|
+
role: z.enum(["admin", "member"]),
|
|
31
|
+
workRole: z.enum(["lead", "product", "dev", "design", "qa", "other"]).nullable().optional(),
|
|
32
|
+
});
|
|
33
|
+
export const AcceptInviteInput = z.object({
|
|
34
|
+
token: z.string().min(10).max(200),
|
|
35
|
+
});
|
|
36
|
+
export const PasswordResetRequestInput = z.object({
|
|
37
|
+
email: z.string().email(),
|
|
38
|
+
});
|
|
39
|
+
export const PasswordResetConfirmInput = z.object({
|
|
40
|
+
token: z.string().min(10).max(200),
|
|
41
|
+
password: z.string().min(6).max(256),
|
|
42
|
+
});
|
|
43
|
+
// ——— Workflow states ———————————————————————————————————————————
|
|
44
|
+
export const WorkflowCategory = z.enum(["backlog", "unstarted", "started", "completed", "cancelled", "archived"]);
|
|
45
|
+
/** True для категорий означающих "задача закрыта" — completedAt
|
|
46
|
+
* проставляется автоматически при переходе в такой статус. */
|
|
47
|
+
export function isClosingCategory(c) {
|
|
48
|
+
return c === "completed" || c === "cancelled" || c === "archived";
|
|
49
|
+
}
|
|
50
|
+
export const WorkflowStateCreate = z.object({
|
|
51
|
+
name: z.string().min(1).max(40),
|
|
52
|
+
category: WorkflowCategory,
|
|
53
|
+
position: z.number().int().min(0),
|
|
54
|
+
color: z.string().regex(/^#[0-9a-f]{6}$/i),
|
|
55
|
+
isDefault: z.boolean().optional().default(false),
|
|
56
|
+
enabled: z.boolean().optional().default(true),
|
|
57
|
+
// «Ready to prod»-флаг: задачи в таких статусах в ReleaseModal по
|
|
58
|
+
// умолчанию помечены как «попадает в этот релиз». Подробнее — в
|
|
59
|
+
// schema.prisma.
|
|
60
|
+
isReadyForRelease: z.boolean().optional().default(false),
|
|
61
|
+
icon: z.enum(STATUS_ICONS).nullable().optional(),
|
|
62
|
+
ownerBoardId: z.string().min(1).nullable().optional(), // null = общий ws-статус; "" запрещена — иначе FK-violation → 500
|
|
63
|
+
});
|
|
64
|
+
export const WorkflowStateUpdate = WorkflowStateCreate.partial();
|
|
65
|
+
// ——— Labels ————————————————————————————————————————————————————
|
|
66
|
+
export const LabelCreate = z.object({
|
|
67
|
+
name: z.string().min(1).max(40),
|
|
68
|
+
color: z.string().regex(/^#[0-9a-f]{6}$/i),
|
|
69
|
+
spaceId: z.string().min(1).optional(),
|
|
70
|
+
});
|
|
71
|
+
export const LabelUpdate = z.object({
|
|
72
|
+
name: z.string().min(1).max(40).optional(),
|
|
73
|
+
color: z.string().regex(/^#[0-9a-f]{6}$/i).optional(),
|
|
74
|
+
}).refine((d) => d.name !== undefined || d.color !== undefined, "empty_update");
|
|
75
|
+
// ——— Teams ————————————————————————————————————————————————————
|
|
76
|
+
export const TeamCreate = z.object({
|
|
77
|
+
name: z.string().min(1).max(80),
|
|
78
|
+
description: z.string().max(1000).optional(),
|
|
79
|
+
});
|
|
80
|
+
export const TeamMemberAdd = z.object({
|
|
81
|
+
userId: z.string().min(1),
|
|
82
|
+
});
|
|
83
|
+
// ——— Projects ————————————————————————————————————————————————
|
|
84
|
+
export const ProjectCreate = z.object({
|
|
85
|
+
name: z.string().min(1).max(120),
|
|
86
|
+
description: z.string().max(4000).optional(),
|
|
87
|
+
teamId: z.string().optional(),
|
|
88
|
+
// PRJ-9: parentId для подпроектов (null/omitted = root project).
|
|
89
|
+
parentId: z.string().nullable().optional(),
|
|
90
|
+
// TAS-223: спейс проекта (null = workspace-level). Подпроект наследует
|
|
91
|
+
// спейс родителя автоматически (форсится в роутах), но явно задать тоже можно.
|
|
92
|
+
spaceId: z.string().nullable().optional(),
|
|
93
|
+
taskPrefix: z.string().regex(/^[A-Z][A-Z0-9]{1,4}$/, "uppercase-alnum-2-5").optional(),
|
|
94
|
+
// URL аватарки проекта (загружается через POST /uploads, потом сюда
|
|
95
|
+
// приходит уже готовый relative path).
|
|
96
|
+
avatarUrl: z.string().max(500).optional(),
|
|
97
|
+
// PRJ-55: создавать ли канал для проекта. Default false — раньше канал
|
|
98
|
+
// создавался всегда, юзеры жаловались на спам в списке каналов.
|
|
99
|
+
createChannel: z.boolean().optional(),
|
|
100
|
+
// TAS-139: visibility — public (default) или private. Private проекты
|
|
101
|
+
// видны только ProjectMember'ам + ws admin/owner.
|
|
102
|
+
visibility: z.enum(["public", "private"]).optional(),
|
|
103
|
+
// TAS-191: AI-сгенерированный brief проекта (markdown, до 20000 символов).
|
|
104
|
+
brief: z.string().max(20000).optional(),
|
|
105
|
+
});
|
|
106
|
+
export const ProjectUpdate = ProjectCreate.partial().extend({
|
|
107
|
+
status: z.enum(["active", "archived"]).optional(),
|
|
108
|
+
// При редактировании можно СНЯТЬ команду (null), не только сменить. В
|
|
109
|
+
// ProjectCreate teamId просто optional; здесь nullable для clearing.
|
|
110
|
+
teamId: z.string().nullable().optional(),
|
|
111
|
+
taskPrefix: z.string().regex(/^[A-Z][A-Z0-9]{1,4}$/, "uppercase-alnum-2-5").optional(),
|
|
112
|
+
// null = наследовать от parent'а (walk-up до первого non-null);
|
|
113
|
+
// omitted = не трогать; иначе — explicit override на этом проекте.
|
|
114
|
+
releaseFlow: z.enum(["stage_then_prod", "prod_only"]).nullable().optional(),
|
|
115
|
+
// TAS-4: manual sort order для drag-drop reorder.
|
|
116
|
+
order: z.number().nullable().optional(),
|
|
117
|
+
// PRJ-9: имя workflow-файла, который подпроект считает «своим» primary
|
|
118
|
+
// деплоем. null = снять выбор (показывать все workflow'ы как default).
|
|
119
|
+
deployWorkflow: z.string().max(120).regex(/^[a-zA-Z0-9._-]+\.ya?ml$/, "yaml-only").nullable().optional(),
|
|
120
|
+
// PRJ4-10: какие источники (ui/mcp/agent) могут запускать release/deploy-
|
|
121
|
+
// действия проекта. null = сбросить на дефолт { ui, mcp, !agent }.
|
|
122
|
+
releaseAllowedSources: ReleaseAllowedSources.nullable().optional(),
|
|
123
|
+
});
|
|
124
|
+
export const ProjectRole = z.enum(["owner", "lead", "member"]);
|
|
125
|
+
export const ProjectMemberAdd = z.object({
|
|
126
|
+
userId: z.string().min(1),
|
|
127
|
+
role: ProjectRole.optional(),
|
|
128
|
+
});
|
|
129
|
+
// ——— Tasks ————————————————————————————————————————————————————
|
|
130
|
+
export const TaskPriority = z.enum(["low", "medium", "high", "urgent"]);
|
|
131
|
+
export const TaskRelationType = z.enum(["blocks", "depends_on", "duplicates", "relates_to"]);
|
|
132
|
+
// TAS-171: тело POST /tasks/:id/relations.
|
|
133
|
+
export const TaskRelationBody = z.object({
|
|
134
|
+
targetTaskId: z.string().min(1),
|
|
135
|
+
type: TaskRelationType,
|
|
136
|
+
});
|
|
137
|
+
export const TaskCreate = z.object({
|
|
138
|
+
title: z.string().min(1).max(200),
|
|
139
|
+
description: z.unknown().optional(),
|
|
140
|
+
projectId: z.string().optional(),
|
|
141
|
+
// TAS-223: спейс задачи БЕЗ проекта (личный инбокс спейса). С проектом
|
|
142
|
+
// игнорируется — spaceId берётся из проекта. Передаётся фронтом из скоупа.
|
|
143
|
+
spaceId: z.string().optional(),
|
|
144
|
+
parentId: z.string().optional(),
|
|
145
|
+
statusId: z.string().optional(),
|
|
146
|
+
priority: TaskPriority.optional().default("medium"),
|
|
147
|
+
assigneeId: z.string().optional(),
|
|
148
|
+
assigneeTeamId: z.string().optional(),
|
|
149
|
+
deadline: z.string().datetime().optional(),
|
|
150
|
+
estimateHours: z.number().positive().max(2000).optional(),
|
|
151
|
+
startAt: z.string().datetime().optional(),
|
|
152
|
+
labelIds: z.array(z.string()).optional().default([]),
|
|
153
|
+
boardOrder: z.number().optional(),
|
|
154
|
+
// TAS-221: задача создаётся сразу в спринте борда (текущий — активный либо
|
|
155
|
+
// плановый), чтобы появиться на доске, а не падать в бэклог. Валидируется в
|
|
156
|
+
// POST /tasks (спринт должен быть в scope борда, как в TaskUpdate).
|
|
157
|
+
sprintId: z.string().optional(),
|
|
158
|
+
// PRJ-10: per-task override workspace.autoCreateBranchOnTask. Если
|
|
159
|
+
// omitted — берётся ws-default. Если false — ветка не создаётся даже
|
|
160
|
+
// когда у проекта есть привязанная репа. Игнорируется если у проекта
|
|
161
|
+
// нет effective repos (всё равно бранчить нечего).
|
|
162
|
+
createBranch: z.boolean().optional(),
|
|
163
|
+
});
|
|
164
|
+
export const TaskUpdate = z.object({
|
|
165
|
+
title: z.string().min(1).max(200).optional(),
|
|
166
|
+
description: z.unknown().optional(),
|
|
167
|
+
projectId: z.string().nullable().optional(),
|
|
168
|
+
parentId: z.string().nullable().optional(),
|
|
169
|
+
statusId: z.string().optional(),
|
|
170
|
+
priority: TaskPriority.optional(),
|
|
171
|
+
assigneeId: z.string().nullable().optional(),
|
|
172
|
+
assigneeTeamId: z.string().nullable().optional(),
|
|
173
|
+
deadline: z.string().datetime().nullable().optional(),
|
|
174
|
+
estimateHours: z.number().positive().max(2000).nullable().optional(),
|
|
175
|
+
// PRJ-74: null = снять дату («Очистить» в DateChip). Раньше null
|
|
176
|
+
// отбрасывался и очистка молча не работала.
|
|
177
|
+
startAt: z.string().datetime().nullable().optional(),
|
|
178
|
+
endAt: z.string().datetime().nullable().optional(),
|
|
179
|
+
labelIds: z.array(z.string()).optional(),
|
|
180
|
+
boardOrder: z.number().nullable().optional(),
|
|
181
|
+
// PRJ-7: assignee'ское «закрепить в Today до». Drag в полку Сегодня
|
|
182
|
+
// сетит endOfToday автоматически. null = снять закрепление.
|
|
183
|
+
pinnedUntil: z.string().datetime().nullable().optional(),
|
|
184
|
+
// Текущий спринт задачи (один). null = убрать из спринта. Валидируется на
|
|
185
|
+
// бэке: проект задачи должен входить в scope борда спринта.
|
|
186
|
+
sprintId: z.string().nullable().optional(),
|
|
187
|
+
});
|
|
188
|
+
// ——— Workspace hierarchy toggle ————————————————————————————————
|
|
189
|
+
export const HierarchyMode = z.enum(["flat", "projects", "teams_and_projects"]);
|
|
190
|
+
export const WorkspaceUpdate = z.object({
|
|
191
|
+
name: z.string().min(1).max(80).optional(),
|
|
192
|
+
// Slug is validated server-side for uniqueness; shape-check here so
|
|
193
|
+
// garbage shapes get rejected early.
|
|
194
|
+
slug: z.string().min(2).max(60).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, "lowercase-alnum-and-dashes").optional(),
|
|
195
|
+
hierarchyMode: HierarchyMode.optional(),
|
|
196
|
+
// Префикс для slug'ов задач без проекта (PROJ-42). Заглавные латинские,
|
|
197
|
+
// 2-5 символов. Уникальность проверяется на сервере (против Project.taskPrefix
|
|
198
|
+
// в этом же ws).
|
|
199
|
+
taskPrefix: z.string().regex(/^[A-Z][A-Z0-9]{1,4}$/, "uppercase-alnum-2-5").optional(),
|
|
200
|
+
// Аватарка ws — относительный URL загруженного файла (через POST /uploads).
|
|
201
|
+
// null/empty = снять аватарку, вернёмся к initial-кружку.
|
|
202
|
+
avatarUrl: z.string().nullable().optional(),
|
|
203
|
+
// TAS-245: тёмная версия логотипа + фавиконка воркспейса (white-label).
|
|
204
|
+
logoDarkUrl: z.string().nullable().optional(),
|
|
205
|
+
faviconUrl: z.string().nullable().optional(),
|
|
206
|
+
// GitHub-policy: удалять source-ветку после merge'а PR'а в default-ветку.
|
|
207
|
+
// Default true. Перебивается native auto-delete'ом GitHub'а на уровне репо.
|
|
208
|
+
githubDeleteBranchAfterMerge: z.boolean().optional(),
|
|
209
|
+
// PRJ-10: при создании task в проекте с привязанной репой —
|
|
210
|
+
// авто-создавать git-ветку. Default true (выставляется в schema).
|
|
211
|
+
autoCreateBranchOnTask: z.boolean().optional(),
|
|
212
|
+
// TAS-355: раскрытие карточек задач Telegram-ботом в этом ws. Default true.
|
|
213
|
+
telegramCardsEnabled: z.boolean().optional(),
|
|
214
|
+
// TAS-378: уровень OG-unfurl карточек задач в чатах/мессенджерах. Default "off".
|
|
215
|
+
taskUnfurlLevel: z.enum(["off", "minimal", "full"]).optional(),
|
|
216
|
+
});
|
|
217
|
+
// ——— Task comments ————————————————————————————————————————————
|
|
218
|
+
// Body is a Tiptap doc (JSON) so comments can carry mentions/formatting.
|
|
219
|
+
// Validated structurally by Prisma's Json column + the editor; here we
|
|
220
|
+
// just refuse non-objects.
|
|
221
|
+
// TAS-167: вложения опционально на отправке комментария.
|
|
222
|
+
// SECURITY: url/thumbUrl хранятся verbatim и потом рендерятся как href/src.
|
|
223
|
+
// Все легитимные вложения — наши же uploads (/api/files/<id>, /api/files/<id>?v=thumb,
|
|
224
|
+
// /api/uploads/<ws>/<file>) — относительные, same-origin, ведущий "/". Поэтому
|
|
225
|
+
// требуем same-origin: начинается с "/" и НЕ с "//" (protocol-relative). Это
|
|
226
|
+
// блокирует javascript:, data:, http(s)://evil и //evil → закрывает stored-XSS.
|
|
227
|
+
const sameOriginUrl = (u) => u.startsWith("/") && !u.startsWith("//");
|
|
228
|
+
const SAME_ORIGIN_MSG = "url must be a relative /api path";
|
|
229
|
+
export const ChatAttachmentInput = z.object({
|
|
230
|
+
uploadId: z.string().optional(),
|
|
231
|
+
url: z.string().min(1).refine(sameOriginUrl, SAME_ORIGIN_MSG),
|
|
232
|
+
thumbUrl: z.string().refine(sameOriginUrl, SAME_ORIGIN_MSG).nullish(),
|
|
233
|
+
mimetype: z.string(),
|
|
234
|
+
size: z.number().int().nonnegative(),
|
|
235
|
+
fileName: z.string(),
|
|
236
|
+
kind: z.enum(["image", "file"]),
|
|
237
|
+
// Клиент шлёт null для не-картинок (dims недоступны) — nullish: null|undefined ок.
|
|
238
|
+
width: z.number().int().nullish(),
|
|
239
|
+
height: z.number().int().nullish(),
|
|
240
|
+
});
|
|
241
|
+
export const TaskCommentCreate = z.object({
|
|
242
|
+
body: z.unknown(),
|
|
243
|
+
parentCommentId: z.string().optional(),
|
|
244
|
+
attachments: z.array(ChatAttachmentInput).optional(),
|
|
245
|
+
});
|
|
246
|
+
export const TaskCommentUpdate = z.object({
|
|
247
|
+
body: z.unknown(),
|
|
248
|
+
// TAS-167: при редактировании клиент шлёт ПОЛНЫЙ желаемый список вложений
|
|
249
|
+
// (содержимое трея). undefined = не трогать; [] = удалить все. См. PATCH-хендлер.
|
|
250
|
+
attachments: z.array(ChatAttachmentInput).optional(),
|
|
251
|
+
});
|
|
252
|
+
// ——— Calls (Stage 1a) ——————————————————————————————————————————
|
|
253
|
+
// CI-50: конкретные STT/LLM-провайдеры и платформенные ключи живут в
|
|
254
|
+
// непубликуемом @taskless/platform (packages/platform) — не здесь. Ниже
|
|
255
|
+
// остаются только generic-типы контракта клиент↔сервер (без вендорской
|
|
256
|
+
// идентичности), которыми пользуется и обычный веб-клиент.
|
|
257
|
+
// PRJ4-51 (эпик PRJ4-46, этап 1 «Provider Config», задача A2): AI-провайдеры
|
|
258
|
+
// воркспейса — типы для сервиса-резолвера и endpoints. Схема данных — A1
|
|
259
|
+
// (PRJ4-47). Провайдеры — строки (не enum) на уровне БД, чтобы добавить
|
|
260
|
+
// нового = zod-правка, не миграция; здесь фиксируем валидируемый список.
|
|
261
|
+
export const AiCategory = z.enum(["llm", "stt"]);
|
|
262
|
+
// Источник рабочего ключа категории по приоритету резолвера:
|
|
263
|
+
// env — суперадмин-тумблер useEnv{Llm,Stt}Key: ключ из окружения, безлимит;
|
|
264
|
+
// own — свой ключ воркспейса, безлимит;
|
|
265
|
+
// demo — общий демо-ключ платформы, пока лимит (деньги+срок) не исчерпан;
|
|
266
|
+
// off — ничего нет, AI-категория не работает.
|
|
267
|
+
export const AiKeySource = z.enum(["env", "own", "demo", "off"]);
|
|
268
|
+
// PRJ4-55 (этап 2b): здоровье своего ключа по фоновому пробному вызову.
|
|
269
|
+
// untested — ещё не проверяли (или ключ только что сменили);
|
|
270
|
+
// ok — пробный вызов прошёл;
|
|
271
|
+
// broken — auth-fail (неверный ключ). Прочие сбои статус не меняют.
|
|
272
|
+
export const AiKeyTestStatus = z.enum(["untested", "ok", "broken"]);
|
|
273
|
+
// Настройки провайдера воркспейса (owner/admin): сменить активного провайдера
|
|
274
|
+
// и/или задать/очистить свой ключ. apiKey: undefined = не трогать, null/"" =
|
|
275
|
+
// удалить свой ключ (вернуться к demo/off), строка = задать (шифруется).
|
|
276
|
+
export const WorkspaceAiProviderUpdate = z.object({
|
|
277
|
+
provider: z.string().min(1).max(50).optional(),
|
|
278
|
+
apiKey: z.string().max(500).nullable().optional(),
|
|
279
|
+
});
|
|
280
|
+
export const CallCreate = z.object({
|
|
281
|
+
title: z.string().min(1).max(200),
|
|
282
|
+
scheduledAt: z.string().datetime().optional(),
|
|
283
|
+
allowGuests: z.boolean().optional(),
|
|
284
|
+
guestsRequireKnock: z.boolean().optional(),
|
|
285
|
+
guestsCanChat: z.boolean().optional(),
|
|
286
|
+
aiEnabled: z.boolean().optional(),
|
|
287
|
+
agentTrustGuests: z.boolean().optional(),
|
|
288
|
+
musicEnabled: z.boolean().optional(),
|
|
289
|
+
voiceAssistantEnabled: z.boolean().optional(),
|
|
290
|
+
timelineEnabled: z.boolean().optional(),
|
|
291
|
+
recordingMode: z.enum(["off", "audio", "video"]).optional(),
|
|
292
|
+
durationMinutes: z.number().int().positive().optional(),
|
|
293
|
+
// PRJ3-7: список юзеров для invite'а при создании моментального звонка.
|
|
294
|
+
// Для scheduled meeting'ов — отдельный flow через event participants.
|
|
295
|
+
// Для instant call'ов: API создаёт call + room + нотифицирует этих
|
|
296
|
+
// юзеров (callIncoming если 1 invitee, callJoinNow если >1 — это уже
|
|
297
|
+
// не «1-on-1 звонок», а «зову в групповой»).
|
|
298
|
+
inviteeUserIds: z.array(z.string().min(1)).max(50).optional(),
|
|
299
|
+
// Свободно введённые/вставленные адреса (гости): член ws сольётся в invitees,
|
|
300
|
+
// остальные станут CallGuest-инвайтами. Резолвит сервер.
|
|
301
|
+
inviteEmails: z.array(z.string().email()).max(50).optional(),
|
|
302
|
+
});
|
|
303
|
+
export const CallPatch = z.object({
|
|
304
|
+
title: z.string().min(1).max(200).optional(),
|
|
305
|
+
// Nullable string: null means "cancel the schedule, make it an instant/ad-hoc
|
|
306
|
+
// state (only valid if call hasn't been started yet)".
|
|
307
|
+
scheduledAt: z.string().datetime().nullable().optional(),
|
|
308
|
+
allowGuests: z.boolean().optional(),
|
|
309
|
+
guestsRequireKnock: z.boolean().optional(),
|
|
310
|
+
guestsCanChat: z.boolean().optional(),
|
|
311
|
+
aiEnabled: z.boolean().optional(),
|
|
312
|
+
agentTrustGuests: z.boolean().optional(),
|
|
313
|
+
musicEnabled: z.boolean().optional(),
|
|
314
|
+
voiceAssistantEnabled: z.boolean().optional(),
|
|
315
|
+
timelineEnabled: z.boolean().optional(),
|
|
316
|
+
recordingMode: z.enum(["off", "audio", "video"]).optional(),
|
|
317
|
+
durationMinutes: z.number().int().positive().optional(),
|
|
318
|
+
// PRJ3-20: полный желаемый набор приглашённых (ws-members, без self).
|
|
319
|
+
// undefined → участников не трогаем. Применяется только к запланированному
|
|
320
|
+
// не стартовавшему созвону (есть Event). Diff против EventParticipant.
|
|
321
|
+
inviteeUserIds: z.array(z.string()).max(50).optional(),
|
|
322
|
+
// Полный желаемый набор гостевых email-приглашений (не ws-member'ы —
|
|
323
|
+
// клиент уже слил их в inviteeUserIds). undefined → гостей не трогаем.
|
|
324
|
+
// Diff против CallGuest(status=invited): новые — приглашаются письмом,
|
|
325
|
+
// снятые — invite тихо отзывается (см. PATCH /calls/:id).
|
|
326
|
+
inviteEmails: z.array(z.string().email()).max(50).optional(),
|
|
327
|
+
});
|
|
328
|
+
// ——— Rooms (persistent) ———————————————————————————————————————
|
|
329
|
+
export const RoomKind = z.enum(["team", "project", "custom"]);
|
|
330
|
+
// TAS-223: public — в списке у всех + lazy-join; unlisted («обычный») — не в
|
|
331
|
+
// списке, но доступен по ссылке + autojoin при входе; private — только мемберы.
|
|
332
|
+
export const RoomVisibility = z.enum(["public", "unlisted", "private"]);
|
|
333
|
+
export const RoomCreate = z.object({
|
|
334
|
+
name: z.string().min(1).max(80),
|
|
335
|
+
kind: RoomKind.optional().default("custom"),
|
|
336
|
+
visibility: RoomVisibility.optional().default("public"),
|
|
337
|
+
teamId: z.string().optional(),
|
|
338
|
+
projectId: z.string().optional(),
|
|
339
|
+
// TAS-223: канал создаётся внутри пространства (из скоупа селектора).
|
|
340
|
+
spaceId: z.string().optional(),
|
|
341
|
+
// TAS-223: сразу добавить участников (помимо создателя) — для приватного
|
|
342
|
+
// канала это «кого впустить», для остальных — предзаполнение.
|
|
343
|
+
memberIds: z.array(z.string()).optional(),
|
|
344
|
+
// TAS-245: публичный канал виден во всех пространствах (spaceId=null +
|
|
345
|
+
// commonInSpaces=true). Для обычного/приватного — false.
|
|
346
|
+
commonInSpaces: z.boolean().optional(),
|
|
347
|
+
});
|
|
348
|
+
// PRJ3-23: slug канала — строчная латиница, цифры, одиночные тире-разделители.
|
|
349
|
+
// Без ведущего/хвостового/двойного тире. Пустая строка с фронта = очистка (null).
|
|
350
|
+
export const ROOM_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
351
|
+
export const RoomUpdate = z.object({
|
|
352
|
+
name: z.string().min(1).max(80).optional(),
|
|
353
|
+
archivedAt: z.string().datetime().nullable().optional(),
|
|
354
|
+
// PRJ-58: public — lazy-join'ит любого ws-member, private — только по
|
|
355
|
+
// explicit RoomMember. direct/group всегда private (фронт не отдаст другое).
|
|
356
|
+
visibility: RoomVisibility.optional(),
|
|
357
|
+
slug: z.string().max(60).regex(ROOM_SLUG_RE).nullable().optional(),
|
|
358
|
+
// TAS-223: привязка канала к спейсу (null = общий канал, без спейса).
|
|
359
|
+
spaceId: z.string().nullable().optional(),
|
|
360
|
+
// TAS-223: канал без спейса виден как «общий» во всех пространствах.
|
|
361
|
+
commonInSpaces: z.boolean().optional(),
|
|
362
|
+
});
|
|
363
|
+
// PRJ3-3: channel-level call settings. PATCH'аются owner/admin воркспейса.
|
|
364
|
+
// Зеркалят Call настройки (см. CallPatch ниже).
|
|
365
|
+
export const RoomSettingsUpdate = z.object({
|
|
366
|
+
aiEnabled: z.boolean().optional(),
|
|
367
|
+
musicEnabled: z.boolean().optional(),
|
|
368
|
+
voiceAssistantEnabled: z.boolean().optional(),
|
|
369
|
+
recordingMode: z.enum(["off", "audio", "video"]).optional(),
|
|
370
|
+
allowGuests: z.boolean().optional(),
|
|
371
|
+
guestsRequireKnock: z.boolean().optional(),
|
|
372
|
+
guestsCanChat: z.boolean().optional(),
|
|
373
|
+
});
|
|
374
|
+
// ——— TaskSuggestion (Stage 2b AI extractor) ———————————————————
|
|
375
|
+
// Kind дискриминатор: пока в проде только task_create (post-call extractor),
|
|
376
|
+
// остальные подключатся realtime-агентом для CallTimeline. Расширяется
|
|
377
|
+
// миграцией ALTER TYPE без code-change при добавлении новых.
|
|
378
|
+
export const CallProposalKind = z.enum([
|
|
379
|
+
"task_create",
|
|
380
|
+
"task_update",
|
|
381
|
+
"task_done",
|
|
382
|
+
"meeting",
|
|
383
|
+
"project",
|
|
384
|
+
"comment",
|
|
385
|
+
"absence",
|
|
386
|
+
]);
|
|
387
|
+
export const TaskSuggestionCreate = z.object({
|
|
388
|
+
title: z.string().min(1).max(200),
|
|
389
|
+
description: z.string().max(50_000).optional().default(""),
|
|
390
|
+
priority: TaskPriority.optional().default("medium"),
|
|
391
|
+
suggestedProjectId: z.string().optional(),
|
|
392
|
+
suggestedAssigneeId: z.string().optional(),
|
|
393
|
+
originStartTs: z.string().datetime(),
|
|
394
|
+
originEndTs: z.string().datetime(),
|
|
395
|
+
originSpeakerId: z.string().optional(),
|
|
396
|
+
rationale: z.string().max(500).optional().default(""),
|
|
397
|
+
});
|
|
398
|
+
export const TaskSuggestionBatch = z.object({
|
|
399
|
+
callId: z.string().min(1),
|
|
400
|
+
roomId: z.string().optional(),
|
|
401
|
+
suggestions: z.array(TaskSuggestionCreate).max(50),
|
|
402
|
+
});
|
|
403
|
+
export const TaskSuggestionRejectInput = z.object({
|
|
404
|
+
reason: z.string().max(500).optional(),
|
|
405
|
+
});
|
|
406
|
+
// Optional overrides provided when accepting a suggestion — lets the user
|
|
407
|
+
// tweak the AI's draft before the task is created. Any field left undefined
|
|
408
|
+
// falls back to the suggestion's value.
|
|
409
|
+
export const TaskSuggestionAcceptInput = z.object({
|
|
410
|
+
title: z.string().min(1).max(500).optional(),
|
|
411
|
+
description: z.string().max(20_000).nullable().optional(),
|
|
412
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
413
|
+
statusId: z.string().optional(),
|
|
414
|
+
projectId: z.string().nullable().optional(),
|
|
415
|
+
assigneeId: z.string().nullable().optional(),
|
|
416
|
+
deadline: z.string().nullable().optional(),
|
|
417
|
+
estimateHours: z.number().nullable().optional(),
|
|
418
|
+
// task_create: полный набор контролов (1:1 с задачей).
|
|
419
|
+
teamId: z.string().nullable().optional(),
|
|
420
|
+
labelIds: z.array(z.string()).optional(),
|
|
421
|
+
// comment-ветка без candidateExistingTaskId: создать Task вместо TaskComment
|
|
422
|
+
createTask: z.boolean().optional(),
|
|
423
|
+
// TAS-209: staged-коммент ревьюера (ProseMirror doc) — постится на accept
|
|
424
|
+
// task_update от текущего юзера.
|
|
425
|
+
comment: z.unknown().optional(),
|
|
426
|
+
// TAS-209: отредактированная реплика спикера (ProseMirror doc) — постится
|
|
427
|
+
// от автора-спикера. Если не прислана — бэк берёт suggestion.description.
|
|
428
|
+
speakerComment: z.unknown().optional(),
|
|
429
|
+
// TAS-221: положить наблюдение (решение/обещание) в заметки проекта.
|
|
430
|
+
toNote: z.boolean().optional(),
|
|
431
|
+
});
|
|
432
|
+
// PATCH update — same fields as Accept but applies to the suggestion row
|
|
433
|
+
// itself (doesn't create a task). Use when the reviewer wants to edit now
|
|
434
|
+
// and decide to accept/reject later.
|
|
435
|
+
// NOTE: per-kind filtering happens at MERGE time (mergeSuggestionPayload),
|
|
436
|
+
// not here at zod-validation time — this flat schema intentionally accepts
|
|
437
|
+
// any kind's fields; the server merge picks only the ones relevant to the
|
|
438
|
+
// suggestion's kind.
|
|
439
|
+
export const SuggestionPayloadPatch = z.object({
|
|
440
|
+
// meeting
|
|
441
|
+
meetingAt: z.string().datetime().nullable().optional(),
|
|
442
|
+
durationMinutes: z.number().int().min(0).optional(),
|
|
443
|
+
participantIds: z.array(z.string()).optional(),
|
|
444
|
+
// absence
|
|
445
|
+
absenceKind: z.enum(["vacation", "sick", "trip", "absent", "block"]).optional(),
|
|
446
|
+
startAt: z.string().datetime().nullable().optional(),
|
|
447
|
+
endAt: z.string().datetime().nullable().optional(),
|
|
448
|
+
// comment
|
|
449
|
+
body: z.string().max(50_000).optional(),
|
|
450
|
+
// project
|
|
451
|
+
name: z.string().max(200).optional(),
|
|
452
|
+
summary: z.string().max(50_000).optional(),
|
|
453
|
+
// task_update (TAS-207): редактирование предлагаемых изменений к задаче.
|
|
454
|
+
newTitle: z.string().max(200).optional(),
|
|
455
|
+
// Заголовок «дописать» + описание «заменить/дописать» — чтобы ревьюер мог
|
|
456
|
+
// править предложенное LLM изменение текста (раньше эти поля accept читал из
|
|
457
|
+
// payload, но в PATCH-схеме их не было → правка отрезалась валидатором).
|
|
458
|
+
newTitleAppend: z.string().max(200).optional(),
|
|
459
|
+
newDescription: z.string().max(20_000).optional(),
|
|
460
|
+
newDescriptionAppend: z.string().max(20_000).optional(),
|
|
461
|
+
newPriority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
462
|
+
newDeadline: z.string().nullable().optional(),
|
|
463
|
+
newStatusName: z.string().max(200).optional(),
|
|
464
|
+
newAssigneeName: z.string().max(200).optional(),
|
|
465
|
+
// TAS-221: остальные поля задачи в task_update (имена резолвятся на accept).
|
|
466
|
+
newProjectName: z.string().max(200).optional(),
|
|
467
|
+
newTeamName: z.string().max(200).optional(),
|
|
468
|
+
newSprintName: z.string().max(200).optional(),
|
|
469
|
+
newLabelName: z.string().max(200).optional(),
|
|
470
|
+
newStartAt: z.string().nullable().optional(),
|
|
471
|
+
newEstimate: z.string().max(16).optional(),
|
|
472
|
+
updateSummary: z.string().max(2_000).optional(),
|
|
473
|
+
// task_done: выбранный completed-статус (id), куда закрыть задачу.
|
|
474
|
+
newStatusId: z.string().max(64).optional(),
|
|
475
|
+
// project: базовые поля создания проекта из звонка.
|
|
476
|
+
visibility: z.enum(["public", "private"]).optional(),
|
|
477
|
+
createChannel: z.boolean().optional(),
|
|
478
|
+
// префикс задач проекта (валидируем/дедупим при accept, тут — свободная строка).
|
|
479
|
+
taskPrefix: z.string().max(8).optional(),
|
|
480
|
+
}).strict();
|
|
481
|
+
export const TaskSuggestionUpdateInput = z.object({
|
|
482
|
+
title: z.string().min(1).max(500).optional(),
|
|
483
|
+
description: z.string().max(20_000).nullable().optional(),
|
|
484
|
+
// TAS-207: rich-описание task_create (ProseMirror doc) из RichEditor дровера.
|
|
485
|
+
descriptionDoc: z.unknown().optional(),
|
|
486
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
487
|
+
projectId: z.string().nullable().optional(),
|
|
488
|
+
assigneeId: z.string().nullable().optional(),
|
|
489
|
+
deadline: z.string().nullable().optional(),
|
|
490
|
+
estimateHours: z.number().nullable().optional(),
|
|
491
|
+
// task_create: полный набор контролов задачи (1:1 с TaskView).
|
|
492
|
+
statusId: z.string().nullable().optional(),
|
|
493
|
+
teamId: z.string().nullable().optional(),
|
|
494
|
+
labelIds: z.array(z.string()).optional(),
|
|
495
|
+
payload: SuggestionPayloadPatch.optional(),
|
|
496
|
+
candidateExistingTaskId: z.string().nullable().optional(),
|
|
497
|
+
});
|
|
498
|
+
// ——— Calendar events ——————————————————————————————————————————
|
|
499
|
+
export const EventTypeEnum = z.enum(["meeting", "absent", "task", "external", "block"]);
|
|
500
|
+
export const EventVisibilityEnum = z.enum(["personal", "team", "workspace"]);
|
|
501
|
+
export const EventCreate = z.object({
|
|
502
|
+
type: EventTypeEnum,
|
|
503
|
+
title: z.string().min(1).max(200),
|
|
504
|
+
description: z.string().max(50_000).optional(),
|
|
505
|
+
startAt: z.string().datetime(),
|
|
506
|
+
endAt: z.string().datetime(),
|
|
507
|
+
allDay: z.boolean().optional(),
|
|
508
|
+
recurrenceRule: z.string().max(500).optional().nullable(),
|
|
509
|
+
recurrenceUntil: z.string().datetime().optional().nullable(),
|
|
510
|
+
visibility: EventVisibilityEnum.optional(),
|
|
511
|
+
teamId: z.string().optional().nullable(),
|
|
512
|
+
projectId: z.string().optional().nullable(),
|
|
513
|
+
participantIds: z.array(z.string()).optional(),
|
|
514
|
+
taskTemplate: z.object({
|
|
515
|
+
title: z.string().min(1).max(200),
|
|
516
|
+
description: z.string().max(50_000).optional(),
|
|
517
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
518
|
+
assigneeId: z.string().optional().nullable(),
|
|
519
|
+
}).optional(),
|
|
520
|
+
autoCreateCall: z.boolean().optional(),
|
|
521
|
+
// Админ может ставить отсутствие/событие за другого юзера. Сервер
|
|
522
|
+
// принимает только если текущий юзер — admin/owner; иначе игнорирует
|
|
523
|
+
// (использует self).
|
|
524
|
+
forUserId: z.string().optional(),
|
|
525
|
+
});
|
|
526
|
+
export const EventPatch = EventCreate.partial().omit({ taskTemplate: true });
|
|
527
|
+
export const OccurrencePatch = z.object({
|
|
528
|
+
startAt: z.string().datetime().optional(),
|
|
529
|
+
endAt: z.string().datetime().optional(),
|
|
530
|
+
title: z.string().max(200).optional(),
|
|
531
|
+
description: z.string().max(50_000).optional(),
|
|
532
|
+
cancelled: z.boolean().optional(),
|
|
533
|
+
});
|
|
534
|
+
// ——— Boards ——————————————————————————————————————————————————
|
|
535
|
+
// Подмножество фильтров, сохраняемое в борде (без assignee — он = groupBy/scope;
|
|
536
|
+
// без search — транзиентный).
|
|
537
|
+
// «Вселенная» борда: задаёт, какие исполнители / приоритеты / метки вообще
|
|
538
|
+
// доступны на борде. Пустой/отсутствующий массив = без ограничения (все).
|
|
539
|
+
// Эти фильтры не показываются чипами — они молча сужают выборку и
|
|
540
|
+
// ограничивают набор опций в ad-hoc-фильтрах. Статусы задаются columns,
|
|
541
|
+
// дата/деплой/автор — только ad-hoc (в конфиг борда не входят).
|
|
542
|
+
export const BoardFilters = z.object({
|
|
543
|
+
assigneeFilter: z.array(z.string()).optional(),
|
|
544
|
+
priorityFilter: z.array(z.string()).optional(),
|
|
545
|
+
labelFilter: z.array(z.string()).optional(),
|
|
546
|
+
}).strict();
|
|
547
|
+
// Оверрайд показа одного статуса на конкретном борде (не меняет сам
|
|
548
|
+
// workflow-статус — только то, как он выглядит на этом борде: своё
|
|
549
|
+
// название колонки, свой цвет, своя иконка).
|
|
550
|
+
export const ColumnOverride = z.object({
|
|
551
|
+
alias: z.string().min(1).max(40).nullable().optional(),
|
|
552
|
+
color: z.string().regex(/^#[0-9a-f]{6}$/i).nullable().optional(),
|
|
553
|
+
icon: z.enum(STATUS_ICONS).nullable().optional(),
|
|
554
|
+
}).strict();
|
|
555
|
+
// карта statusId → оверрайд показа для конкретного борда
|
|
556
|
+
export const ColumnOverrides = z.record(z.string(), ColumnOverride);
|
|
557
|
+
export const BoardCreate = z.object({
|
|
558
|
+
name: z.string().min(1).max(120),
|
|
559
|
+
slug: z.string().min(1).max(80).regex(/^[a-zA-Z0-9-]+$/, "alnum-and-dashes").optional(),
|
|
560
|
+
// TAS-223: пространство борда (null/omitted = workspace-level).
|
|
561
|
+
spaceId: z.string().nullable().optional(),
|
|
562
|
+
projectIds: z.array(z.string()).min(1, "min-one-project"),
|
|
563
|
+
includeSubprojects: z.boolean().optional(),
|
|
564
|
+
columns: z.array(z.string()).optional(), // statusId[]; пусто → дефолт на бэке
|
|
565
|
+
columnOverrides: ColumnOverrides.optional(),
|
|
566
|
+
copyFromBoardId: z.string().optional(), // «скопировать колонки из борда»
|
|
567
|
+
groupBy: z.enum(["none", "assignee"]).optional(),
|
|
568
|
+
filters: BoardFilters.nullable().optional(),
|
|
569
|
+
editorIds: z.array(z.string()).optional(), // доверенные редакторы
|
|
570
|
+
sprintsEnabled: z.boolean().optional(),
|
|
571
|
+
});
|
|
572
|
+
export const BoardUpdate = z.object({
|
|
573
|
+
name: z.string().min(1).max(120).optional(),
|
|
574
|
+
slug: z.string().min(1).max(80).regex(/^[a-zA-Z0-9-]+$/, "alnum-and-dashes").optional(),
|
|
575
|
+
// TAS-223: привязка/отвязка борда к пространству (null = открепить).
|
|
576
|
+
spaceId: z.string().nullable().optional(),
|
|
577
|
+
projectIds: z.array(z.string()).min(1).optional(),
|
|
578
|
+
includeSubprojects: z.boolean().optional(),
|
|
579
|
+
columns: z.array(z.string()).optional(),
|
|
580
|
+
columnOverrides: ColumnOverrides.optional(),
|
|
581
|
+
groupBy: z.enum(["none", "assignee"]).optional(),
|
|
582
|
+
filters: BoardFilters.nullable().optional(),
|
|
583
|
+
editorIds: z.array(z.string()).optional(), // доверенные редакторы
|
|
584
|
+
sprintsEnabled: z.boolean().optional(),
|
|
585
|
+
}).strict();
|
|
586
|
+
export const SprintCreate = z.object({
|
|
587
|
+
name: z.string().min(1).max(120).optional(), // пусто → "Спринт N" на бэке
|
|
588
|
+
goal: z.string().max(2000).nullable().optional(),
|
|
589
|
+
startsAt: z.string().datetime().nullable().optional(),
|
|
590
|
+
endsAt: z.string().datetime().nullable().optional(),
|
|
591
|
+
}).strict();
|
|
592
|
+
export const SprintUpdate = z.object({
|
|
593
|
+
name: z.string().min(1).max(120).optional(),
|
|
594
|
+
goal: z.string().max(2000).nullable().optional(),
|
|
595
|
+
startsAt: z.string().datetime().nullable().optional(),
|
|
596
|
+
endsAt: z.string().datetime().nullable().optional(),
|
|
597
|
+
}).strict();
|
|
598
|
+
export const SprintComplete = z.object({
|
|
599
|
+
carryTo: z.enum(["next", "new", "backlog"]),
|
|
600
|
+
}).strict();
|
|
601
|
+
// TAS-242: массовое управление членством людей в командах/пространствах.
|
|
602
|
+
// Дискриминатор `action`; teamId требуется для team.*, spaceId — для space.*.
|
|
603
|
+
export const CompositionBulk = z.object({
|
|
604
|
+
userIds: z.array(z.string().min(1)).min(1, "min-one-user"),
|
|
605
|
+
action: z.enum([
|
|
606
|
+
"team.add", "team.remove", "team.setLead", "team.unsetLead",
|
|
607
|
+
"space.add", "space.remove",
|
|
608
|
+
]),
|
|
609
|
+
teamId: z.string().optional(),
|
|
610
|
+
spaceId: z.string().optional(),
|
|
611
|
+
}).strict().refine((v) => (v.action.startsWith("team.") ? !!v.teamId : !!v.spaceId), { message: "target-id-required" });
|
|
612
|
+
// Массовое обновление задач (Inbox bulk-edit). За один вызов меняется одно
|
|
613
|
+
// поле (UI шлёт по одному действию), но схема допускает любой их поднабор.
|
|
614
|
+
export const BulkTaskUpdate = z.object({
|
|
615
|
+
taskIds: z.array(z.string()).min(1, "min-one-task"),
|
|
616
|
+
patch: z.object({
|
|
617
|
+
projectId: z.string().nullable().optional(),
|
|
618
|
+
sprintId: z.string().nullable().optional(),
|
|
619
|
+
statusId: z.string().optional(),
|
|
620
|
+
assigneeId: z.string().nullable().optional(),
|
|
621
|
+
priority: TaskPriority.optional(),
|
|
622
|
+
}).strict(),
|
|
623
|
+
}).strict();
|
|
624
|
+
// PRJ4-58 (этап A): системные баннеры платформы. Кнопка баннера — ярлык +
|
|
625
|
+
// вариант + действие. Пока единственное действие — навигация по внутреннему
|
|
626
|
+
// пути (action.to). Держим action объектом с дискриминатором type, чтобы позже
|
|
627
|
+
// добавить другие типы (dismiss, external, …) без ломки формы.
|
|
628
|
+
export const BannerButton = z.object({
|
|
629
|
+
label: z.string().trim().min(1).max(80),
|
|
630
|
+
variant: z.enum(["primary", "secondary"]),
|
|
631
|
+
action: z.object({
|
|
632
|
+
type: z.literal("navigate"),
|
|
633
|
+
to: z.string().trim().min(1).max(500),
|
|
634
|
+
}).strict(),
|
|
635
|
+
}).strict();
|
|
636
|
+
// Создать можно только custom-баннер (library — предустановлены сидом). Контент
|
|
637
|
+
// произвольный; enabled/sortOrder опциональны (дефолты — как в БД).
|
|
638
|
+
export const CreateBanner = z.object({
|
|
639
|
+
icon: z.string().trim().max(80).nullable().optional(),
|
|
640
|
+
text: z.string().trim().min(1).max(2000),
|
|
641
|
+
buttons: z.array(BannerButton).max(4).optional(),
|
|
642
|
+
enabled: z.boolean().optional(),
|
|
643
|
+
sortOrder: z.number().int().optional(),
|
|
644
|
+
}).strict();
|
|
645
|
+
// PATCH: любое подмножество полей. Для library-баннеров роут разрешает менять
|
|
646
|
+
// только enabled (правку прочего отвергает 400) — сама схема допускает все поля,
|
|
647
|
+
// разграничение по kind живёт в роуте.
|
|
648
|
+
export const UpdateBanner = z.object({
|
|
649
|
+
icon: z.string().trim().max(80).nullable().optional(),
|
|
650
|
+
text: z.string().trim().min(1).max(2000).optional(),
|
|
651
|
+
buttons: z.array(BannerButton).max(4).optional(),
|
|
652
|
+
enabled: z.boolean().optional(),
|
|
653
|
+
sortOrder: z.number().int().optional(),
|
|
654
|
+
}).strict();
|
|
655
|
+
//# sourceMappingURL=schemas.js.map
|