@burdenoff/microfe-organization 2026.828.1 → 2026.830.2
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/dist/organization-management/AdminOrganizationRoot.js +35 -30
- package/dist/organization-management/AdminOrganizationRoot.js.map +1 -1
- package/dist/organization-management/UserOrganizationRoot.js +32 -27
- package/dist/organization-management/UserOrganizationRoot.js.map +1 -1
- package/dist/organization-management/modules/members/components/BulkInviteForm.js +2 -2
- package/dist/organization-management/modules/members/components/BulkInviteForm.js.map +1 -1
- package/dist/organization-management/modules/members/pages/MyInvitesPage.js +1 -1
- package/dist/organization-management/modules/members/pages/MyInvitesPage.js.map +1 -1
- package/dist/organization-management/modules/members/utils/bulkInviteCsv.js +17 -17
- package/dist/organization-management/modules/members/utils/bulkInviteCsv.js.map +1 -1
- package/dist/organization-management/modules/members/utils/workspaceInvitationDisplay.js +10 -7
- package/dist/organization-management/modules/members/utils/workspaceInvitationDisplay.js.map +1 -1
- package/dist/organization-management/modules/organizations/components/OrganizationForm.js +3 -3
- package/dist/organization-management/modules/organizations/components/OrganizationForm.js.map +1 -1
- package/dist/organization-management/modules/organizations/schemas/index.js +27 -18
- package/dist/organization-management/modules/organizations/schemas/index.js.map +1 -1
- package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js +1 -1
- package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js.map +1 -1
- package/dist/organization-management/modules/workspaces/schemas/index.js +24 -15
- package/dist/organization-management/modules/workspaces/schemas/index.js.map +1 -1
- package/dist/organization-management/shared/components/input/SearchInput.js +1 -1
- package/dist/organization-management/shared/components/input/SearchInput.js.map +1 -1
- package/dist/organization-management/shared/hooks/useTr.js +3 -3
- package/dist/organization-management/shared/hooks/useTr.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulkInviteCsv.js","names":[],"sources":["../../../../../src/organization-management/modules/members/utils/bulkInviteCsv.ts"],"sourcesContent":["/**\n * Minimal CSV parsing + template generation for bulk member invites.\n * Hand-rolled (no csv lib dep) — invite CSVs are small enough to keep\n * the parser inline. Supports double-quoted fields with embedded commas\n * and escaped quotes (RFC 4180 subset).\n *\n * Expected columns (header row required, case-insensitive, any order):\n * email — required\n * role — optional, one of MEMBER | ADMIN | OWNER (case-insensitive)\n * message — optional, free text\n */\n\nimport { UserRole } from '../../../../generated/global-types';\n\nexport interface BulkInviteRow {\n rowNumber: number;\n email: string;\n role?: UserRole;\n message?: string;\n errors: string[];\n}\n\nconst VALID_ROLES = new Set<string>([UserRole.Member, UserRole.OrgAdmin]);\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * RFC 4180-subset CSV row parser. Handles double-quoted fields, embedded\n * commas and `\"\"` escapes. Trims surrounding whitespace per field but\n * preserves whitespace inside quotes.\n */\nfunction splitCsvLine(line: string): string[] {\n const fields: string[] = [];\n let current = '';\n let inQuotes = false;\n for (let i = 0; i < line.length; i += 1) {\n const ch = line[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (line[i + 1] === '\"') {\n current += '\"';\n i += 1;\n } else {\n inQuotes = false;\n }\n } else {\n current += ch;\n }\n } else if (ch === '\"') {\n inQuotes = true;\n } else if (ch === ',') {\n fields.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n fields.push(current);\n return fields.map((f) => f.trim());\n}\n\nexport function parseBulkInviteCsv(text: string): BulkInviteRow[] {\n const lines = text\n .replace(/^\\uFEFF/, '')\n .split(/\\r?\\n/)\n .filter((l) => l.trim().length > 0);\n\n if (lines.length === 0) return [];\n\n const header = splitCsvLine(lines[0]).map((h) => h.toLowerCase());\n const emailIdx = header.indexOf('email');\n if (emailIdx === -1) {\n return [\n {\n rowNumber: 1,\n email: '',\n errors: ['CSV header must include an \"email\" column.'],\n },\n ];\n }\n const roleIdx = header.indexOf('role');\n const messageIdx = header.indexOf('message');\n\n const seen = new Set<string>();\n\n return lines.slice(1).map((line, idx) => {\n const fields = splitCsvLine(line);\n const rowNumber = idx + 2;\n const email = (fields[emailIdx] ?? '').toLowerCase();\n const rawRole = roleIdx >= 0 ? (fields[roleIdx] ?? '').toUpperCase() : '';\n const message = messageIdx >= 0 ? (fields[messageIdx] ?? '') : '';\n const errors: string[] = [];\n\n if (!email) {\n errors.push('Email is required.');\n } else if (!EMAIL_RE.test(email)) {\n errors.push('Invalid email format.');\n } else if (seen.has(email)) {\n errors.push('Duplicate email in this CSV.');\n } else {\n seen.add(email);\n }\n\n let role: UserRole | undefined;\n if (rawRole) {\n if (VALID_ROLES.has(rawRole)) {\n role = rawRole as UserRole;\n } else {\n errors.push(`Invalid role \"${rawRole}\". Use MEMBER or ORG_ADMIN
|
|
1
|
+
{"version":3,"file":"bulkInviteCsv.js","names":[],"sources":["../../../../../src/organization-management/modules/members/utils/bulkInviteCsv.ts"],"sourcesContent":["/**\n * Minimal CSV parsing + template generation for bulk member invites.\n * Hand-rolled (no csv lib dep) — invite CSVs are small enough to keep\n * the parser inline. Supports double-quoted fields with embedded commas\n * and escaped quotes (RFC 4180 subset).\n *\n * Expected columns (header row required, case-insensitive, any order):\n * email — required\n * role — optional, one of MEMBER | ADMIN | OWNER (case-insensitive)\n * message — optional, free text\n */\n\nimport { UserRole } from '../../../../generated/global-types';\nimport type { TranslationFn } from '../../../shared/hooks/useTr';\n\nexport interface BulkInviteRow {\n rowNumber: number;\n email: string;\n role?: UserRole;\n message?: string;\n errors: string[];\n}\n\nconst VALID_ROLES = new Set<string>([UserRole.Member, UserRole.OrgAdmin]);\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * RFC 4180-subset CSV row parser. Handles double-quoted fields, embedded\n * commas and `\"\"` escapes. Trims surrounding whitespace per field but\n * preserves whitespace inside quotes.\n */\nfunction splitCsvLine(line: string): string[] {\n const fields: string[] = [];\n let current = '';\n let inQuotes = false;\n for (let i = 0; i < line.length; i += 1) {\n const ch = line[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (line[i + 1] === '\"') {\n current += '\"';\n i += 1;\n } else {\n inQuotes = false;\n }\n } else {\n current += ch;\n }\n } else if (ch === '\"') {\n inQuotes = true;\n } else if (ch === ',') {\n fields.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n fields.push(current);\n return fields.map((f) => f.trim());\n}\n\nexport function parseBulkInviteCsv(text: string, tr: TranslationFn): BulkInviteRow[] {\n const lines = text\n .replace(/^\\uFEFF/, '')\n .split(/\\r?\\n/)\n .filter((l) => l.trim().length > 0);\n\n if (lines.length === 0) return [];\n\n const header = splitCsvLine(lines[0]).map((h) => h.toLowerCase());\n const emailIdx = header.indexOf('email');\n if (emailIdx === -1) {\n return [\n {\n rowNumber: 1,\n email: '',\n errors: [\n tr(\n 'members.bulkInvite.errors.emailColumnRequired',\n 'CSV header must include an \"email\" column.'\n ),\n ],\n },\n ];\n }\n const roleIdx = header.indexOf('role');\n const messageIdx = header.indexOf('message');\n\n const seen = new Set<string>();\n\n return lines.slice(1).map((line, idx) => {\n const fields = splitCsvLine(line);\n const rowNumber = idx + 2;\n const email = (fields[emailIdx] ?? '').toLowerCase();\n const rawRole = roleIdx >= 0 ? (fields[roleIdx] ?? '').toUpperCase() : '';\n const message = messageIdx >= 0 ? (fields[messageIdx] ?? '') : '';\n const errors: string[] = [];\n\n if (!email) {\n errors.push(tr('members.bulkInvite.errors.emailRequired', 'Email is required.'));\n } else if (!EMAIL_RE.test(email)) {\n errors.push(tr('members.bulkInvite.errors.emailInvalid', 'Invalid email format.'));\n } else if (seen.has(email)) {\n errors.push(tr('members.bulkInvite.errors.emailDuplicate', 'Duplicate email in this CSV.'));\n } else {\n seen.add(email);\n }\n\n let role: UserRole | undefined;\n if (rawRole) {\n if (VALID_ROLES.has(rawRole)) {\n role = rawRole as UserRole;\n } else {\n errors.push(\n tr(\n 'members.bulkInvite.errors.roleInvalid',\n `Invalid role \"${rawRole}\". Use MEMBER or ORG_ADMIN.`\n )\n );\n }\n }\n\n return {\n rowNumber,\n email,\n role,\n message: message || undefined,\n errors,\n };\n });\n}\n\n/**\n * Sample template content + filename. Includes a header row, a couple of\n * placeholder example rows, and a comment-style hint as a trailing row\n * users will likely delete.\n */\nexport const TEMPLATE_FILENAME = 'invite-members-template.csv';\n\nexport const TEMPLATE_CONTENT = [\n 'email,role,message',\n 'alice@example.com,MEMBER,Welcome to the team',\n 'bob@example.com,ORG_ADMIN,',\n 'carol@example.com,,',\n].join('\\n');\n\n/**\n * Map a thrown invite mutation error to a user-facing message, matching the\n * existing single-invite page's mapping (CONFLICT → already invited, etc).\n * Centralised here so single + bulk surfaces stay consistent.\n */\nexport function formatInviteError(error: unknown, tr: TranslationFn): string {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('CONFLICT')) {\n return tr('members.invite.error.conflict', 'An invite already exists for this email address.');\n }\n if (message.includes('already a member')) {\n return tr(\n 'members.invite.error.alreadyMember',\n 'This user is already a member of the organization.'\n );\n }\n return message;\n}\n\nexport function downloadTemplate(): void {\n const blob = new Blob([TEMPLATE_CONTENT], { type: 'text/csv;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = TEMPLATE_FILENAME;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n"],"mappings":";;AAuBA,IAAM,IAAc,IAAI,IAAY,CAAC,EAAS,QAAQ,EAAS,SAAS,CAAC,EAEnE,IAAW;AAOjB,SAAS,EAAa,GAAwB;CAC5C,IAAM,IAAmB,EAAE,EACvB,IAAU,IACV,IAAW;AACf,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,GAAG;EACvC,IAAM,IAAK,EAAK;AAChB,EAAI,IACE,MAAO,OACL,EAAK,IAAI,OAAO,QAClB,KAAW,MACX,KAAK,KAEL,IAAW,KAGb,KAAW,IAEJ,MAAO,OAChB,IAAW,KACF,MAAO,OAChB,EAAO,KAAK,EAAQ,EACpB,IAAU,MAEV,KAAW;;AAIf,QADA,EAAO,KAAK,EAAQ,EACb,EAAO,KAAK,MAAM,EAAE,MAAM,CAAC;;AAGpC,SAAgB,EAAmB,GAAc,GAAoC;CACnF,IAAM,IAAQ,EACX,QAAQ,WAAW,GAAG,CACtB,MAAM,QAAQ,CACd,QAAQ,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE;AAErC,KAAI,EAAM,WAAW,EAAG,QAAO,EAAE;CAEjC,IAAM,IAAS,EAAa,EAAM,GAAG,CAAC,KAAK,MAAM,EAAE,aAAa,CAAC,EAC3D,IAAW,EAAO,QAAQ,QAAQ;AACxC,KAAI,MAAa,GACf,QAAO,CACL;EACE,WAAW;EACX,OAAO;EACP,QAAQ,CACN,EACE,iDACA,+CACD,CACF;EACF,CACF;CAEH,IAAM,IAAU,EAAO,QAAQ,OAAO,EAChC,IAAa,EAAO,QAAQ,UAAU,EAEtC,oBAAO,IAAI,KAAa;AAE9B,QAAO,EAAM,MAAM,EAAE,CAAC,KAAK,GAAM,MAAQ;EACvC,IAAM,IAAS,EAAa,EAAK,EAC3B,IAAY,IAAM,GAClB,KAAS,EAAO,MAAa,IAAI,aAAa,EAC9C,IAAU,KAAW,KAAK,EAAO,MAAY,IAAI,aAAa,GAAG,IACjE,IAAU,KAAc,IAAK,EAAO,MAAe,KAAM,IACzD,IAAmB,EAAE;AAE3B,EAAK,IAEO,EAAS,KAAK,EAAM,GAErB,EAAK,IAAI,EAAM,GACxB,EAAO,KAAK,EAAG,4CAA4C,+BAA+B,CAAC,GAE3F,EAAK,IAAI,EAAM,GAJf,EAAO,KAAK,EAAG,0CAA0C,wBAAwB,CAAC,GAFlF,EAAO,KAAK,EAAG,2CAA2C,qBAAqB,CAAC;EASlF,IAAI;AAcJ,SAbI,MACE,EAAY,IAAI,EAAQ,GAC1B,IAAO,IAEP,EAAO,KACL,EACE,yCACA,iBAAiB,EAAQ,6BAC1B,CACF,GAIE;GACL;GACA;GACA;GACA,SAAS,KAAW,KAAA;GACpB;GACD;GACD;;AAQJ,IAAa,IAAoB,+BAEpB,IAAmB;CAC9B;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;AAOZ,SAAgB,EAAkB,GAAgB,GAA2B;CAC3E,IAAM,IAAU,aAAiB,QAAQ,EAAM,UAAU,OAAO,EAAM;AAUtE,QATI,EAAQ,SAAS,WAAW,GACvB,EAAG,iCAAiC,mDAAmD,GAE5F,EAAQ,SAAS,mBAAmB,GAC/B,EACL,sCACA,qDACD,GAEI;;AAGT,SAAgB,IAAyB;CACvC,IAAM,IAAO,IAAI,KAAK,CAAC,EAAiB,EAAE,EAAE,MAAM,0BAA0B,CAAC,EACvE,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAI,SAAS,cAAc,IAAI;AAMrC,CALA,EAAE,OAAO,GACT,EAAE,WAAW,GACb,SAAS,KAAK,YAAY,EAAE,EAC5B,EAAE,OAAO,EACT,SAAS,KAAK,YAAY,EAAE,EAC5B,IAAI,gBAAgB,EAAI"}
|
|
@@ -13,15 +13,18 @@ function n(t) {
|
|
|
13
13
|
let n = e(t)?.roleNames;
|
|
14
14
|
return Array.isArray(n) && n.every((e) => typeof e == "string") ? n : [];
|
|
15
15
|
}
|
|
16
|
-
function r(t) {
|
|
17
|
-
let
|
|
18
|
-
if (Array.isArray(
|
|
19
|
-
let
|
|
16
|
+
function r(t, n) {
|
|
17
|
+
let r = e(t), i = r?.projectNames;
|
|
18
|
+
if (Array.isArray(i) && i.every((e) => typeof e == "string")) return i;
|
|
19
|
+
let a = r?.projectIds, o = [
|
|
20
20
|
...Array.isArray(t.projectIds) ? t.projectIds : [],
|
|
21
21
|
...t.projectId ? [t.projectId] : [],
|
|
22
|
-
...Array.isArray(
|
|
23
|
-
],
|
|
24
|
-
return
|
|
22
|
+
...Array.isArray(a) ? a.filter((e) => typeof e == "string") : []
|
|
23
|
+
], s = new Set(o).size;
|
|
24
|
+
return s === 0 ? [] : n ? [n("members.myInvites.projectCount", "{{count}} project{{pluralSuffix}} selected", {
|
|
25
|
+
count: s,
|
|
26
|
+
pluralSuffix: s === 1 ? "" : "s"
|
|
27
|
+
})] : [`${s} project${s === 1 ? "" : "s"} selected`];
|
|
25
28
|
}
|
|
26
29
|
//#endregion
|
|
27
30
|
export { r as getProjectDisplayNames, n as getRoleDisplayNames, t as getWorkspaceDisplayName };
|
package/dist/organization-management/modules/members/utils/workspaceInvitationDisplay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceInvitationDisplay.js","names":[],"sources":["../../../../../src/organization-management/modules/members/utils/workspaceInvitationDisplay.ts"],"sourcesContent":["import type { Invitation } from '../../../../generated/wspace-types';\n\ntype InvitationLike = Pick<Invitation, 'workspaceId' | 'roleIds' | 'projectId' | 'context'> & {\n workspaceName?: string;\n projectIds?: string[];\n roleNames?: string[];\n};\n\nfunction readMetadata(invitation: InvitationLike): Record<string, unknown> | null {\n const metadata = invitation.context?.metadata;\n if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {\n return metadata as Record<string, unknown>;\n }\n\n return null;\n}\n\nexport function getWorkspaceDisplayName(invitation: InvitationLike): string {\n if (invitation.workspaceName) {\n return invitation.workspaceName;\n }\n\n const metadata = readMetadata(invitation);\n const workspaceName = metadata?.workspaceName;\n if (typeof workspaceName === 'string' && workspaceName.trim().length > 0) {\n return workspaceName;\n }\n\n return invitation.workspaceId || '—';\n}\n\nexport function getRoleDisplayNames(invitation: InvitationLike): string[] {\n if (Array.isArray(invitation.roleNames) && invitation.roleNames.length > 0) {\n return invitation.roleNames;\n }\n\n const metadata = readMetadata(invitation);\n const roleNames = metadata?.roleNames;\n if (Array.isArray(roleNames) && roleNames.every((value) => typeof value === 'string')) {\n return roleNames;\n }\n\n return [];\n}\n\nexport function getProjectDisplayNames(invitation: InvitationLike): string[] {\n const metadata = readMetadata(invitation);\n const projectNames = metadata?.projectNames;\n if (Array.isArray(projectNames) && projectNames.every((value) => typeof value === 'string')) {\n return projectNames;\n }\n\n const metadataProjectIds = metadata?.projectIds;\n const projectIds = [\n ...(Array.isArray(invitation.projectIds) ? invitation.projectIds : []),\n ...(invitation.projectId ? [invitation.projectId] : []),\n ...(Array.isArray(metadataProjectIds)\n ? metadataProjectIds.filter((value): value is string => typeof value === 'string')\n : []),\n ];\n const projectCount = new Set(projectIds).size;\n\n if (projectCount === 0) {\n return [];\n }\n\n return [`${projectCount} project${projectCount === 1 ? '' : 's'} selected`];\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"workspaceInvitationDisplay.js","names":[],"sources":["../../../../../src/organization-management/modules/members/utils/workspaceInvitationDisplay.ts"],"sourcesContent":["import type { Invitation } from '../../../../generated/wspace-types';\n\nimport type { TranslationFn } from '../../../shared/hooks/useTr';\n\ntype InvitationLike = Pick<Invitation, 'workspaceId' | 'roleIds' | 'projectId' | 'context'> & {\n workspaceName?: string;\n projectIds?: string[];\n roleNames?: string[];\n};\n\nfunction readMetadata(invitation: InvitationLike): Record<string, unknown> | null {\n const metadata = invitation.context?.metadata;\n if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {\n return metadata as Record<string, unknown>;\n }\n\n return null;\n}\n\nexport function getWorkspaceDisplayName(invitation: InvitationLike): string {\n if (invitation.workspaceName) {\n return invitation.workspaceName;\n }\n\n const metadata = readMetadata(invitation);\n const workspaceName = metadata?.workspaceName;\n if (typeof workspaceName === 'string' && workspaceName.trim().length > 0) {\n return workspaceName;\n }\n\n return invitation.workspaceId || '—';\n}\n\nexport function getRoleDisplayNames(invitation: InvitationLike): string[] {\n if (Array.isArray(invitation.roleNames) && invitation.roleNames.length > 0) {\n return invitation.roleNames;\n }\n\n const metadata = readMetadata(invitation);\n const roleNames = metadata?.roleNames;\n if (Array.isArray(roleNames) && roleNames.every((value) => typeof value === 'string')) {\n return roleNames;\n }\n\n return [];\n}\n\nexport function getProjectDisplayNames(invitation: InvitationLike, tr?: TranslationFn): string[] {\n const metadata = readMetadata(invitation);\n const projectNames = metadata?.projectNames;\n if (Array.isArray(projectNames) && projectNames.every((value) => typeof value === 'string')) {\n return projectNames;\n }\n\n const metadataProjectIds = metadata?.projectIds;\n const projectIds = [\n ...(Array.isArray(invitation.projectIds) ? invitation.projectIds : []),\n ...(invitation.projectId ? [invitation.projectId] : []),\n ...(Array.isArray(metadataProjectIds)\n ? metadataProjectIds.filter((value): value is string => typeof value === 'string')\n : []),\n ];\n const projectCount = new Set(projectIds).size;\n\n if (projectCount === 0) {\n return [];\n }\n\n if (!tr) {\n return [`${projectCount} project${projectCount === 1 ? '' : 's'} selected`];\n }\n\n return [\n tr('members.myInvites.projectCount', '{{count}} project{{pluralSuffix}} selected', {\n count: projectCount,\n pluralSuffix: projectCount === 1 ? '' : 's',\n }),\n ];\n}\n"],"mappings":";AAUA,SAAS,EAAa,GAA4D;CAChF,IAAM,IAAW,EAAW,SAAS;AAKrC,QAJI,KAAY,OAAO,KAAa,YAAY,CAAC,MAAM,QAAQ,EAAS,GAC/D,IAGF;;AAGT,SAAgB,EAAwB,GAAoC;AAC1E,KAAI,EAAW,cACb,QAAO,EAAW;CAIpB,IAAM,IADW,EAAa,EAAW,EACT;AAKhC,QAJI,OAAO,KAAkB,YAAY,EAAc,MAAM,CAAC,SAAS,IAC9D,IAGF,EAAW,eAAe;;AAGnC,SAAgB,EAAoB,GAAsC;AACxE,KAAI,MAAM,QAAQ,EAAW,UAAU,IAAI,EAAW,UAAU,SAAS,EACvE,QAAO,EAAW;CAIpB,IAAM,IADW,EAAa,EAAW,EACb;AAK5B,QAJI,MAAM,QAAQ,EAAU,IAAI,EAAU,OAAO,MAAU,OAAO,KAAU,SAAS,GAC5E,IAGF,EAAE;;AAGX,SAAgB,EAAuB,GAA4B,GAA8B;CAC/F,IAAM,IAAW,EAAa,EAAW,EACnC,IAAe,GAAU;AAC/B,KAAI,MAAM,QAAQ,EAAa,IAAI,EAAa,OAAO,MAAU,OAAO,KAAU,SAAS,CACzF,QAAO;CAGT,IAAM,IAAqB,GAAU,YAC/B,IAAa;EACjB,GAAI,MAAM,QAAQ,EAAW,WAAW,GAAG,EAAW,aAAa,EAAE;EACrE,GAAI,EAAW,YAAY,CAAC,EAAW,UAAU,GAAG,EAAE;EACtD,GAAI,MAAM,QAAQ,EAAmB,GACjC,EAAmB,QAAQ,MAA2B,OAAO,KAAU,SAAS,GAChF,EAAE;EACP,EACK,IAAe,IAAI,IAAI,EAAW,CAAC;AAUzC,QARI,MAAiB,IACZ,EAAE,GAGN,IAIE,CACL,EAAG,kCAAkC,8CAA8C;EACjF,OAAO;EACP,cAAc,MAAiB,IAAI,KAAK;EACzC,CAAC,CACH,GARQ,CAAC,GAAG,EAAa,UAAU,MAAiB,IAAI,KAAK,IAAI,WAAW"}
|
|
@@ -107,7 +107,7 @@ function h({ organization: h, tenantId: g, tenantName: _, hideTenantSelector: v
|
|
|
107
107
|
]);
|
|
108
108
|
let z = () => {
|
|
109
109
|
try {
|
|
110
|
-
return (h ? a : i).parse(T), O({}), !0;
|
|
110
|
+
return (h ? a(S) : i(S)).parse(T), O({}), !0;
|
|
111
111
|
} catch (e) {
|
|
112
112
|
if (e instanceof r) {
|
|
113
113
|
let t = {};
|
|
@@ -282,7 +282,7 @@ function h({ organization: h, tenantId: g, tenantName: _, hideTenantSelector: v
|
|
|
282
282
|
}),
|
|
283
283
|
"aria-label": S("organizations.form.logoUrl", "Logo URL"),
|
|
284
284
|
className: "\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n ",
|
|
285
|
-
placeholder: "https://example.com/logo.png",
|
|
285
|
+
placeholder: S("organizations.form.logoPlaceholder", "https://example.com/logo.png"),
|
|
286
286
|
disabled: x,
|
|
287
287
|
autoComplete: "url"
|
|
288
288
|
})] }),
|
|
@@ -300,7 +300,7 @@ function h({ organization: h, tenantId: g, tenantName: _, hideTenantSelector: v
|
|
|
300
300
|
}),
|
|
301
301
|
"aria-label": S("organizations.detail.website", "Website"),
|
|
302
302
|
className: "\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n ",
|
|
303
|
-
placeholder: "https://example.com",
|
|
303
|
+
placeholder: S("organizations.form.websitePlaceholder", "https://example.com"),
|
|
304
304
|
disabled: x,
|
|
305
305
|
autoComplete: "url"
|
|
306
306
|
})] }),
|
package/dist/organization-management/modules/organizations/components/OrganizationForm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OrganizationForm.js","names":[],"sources":["../../../../../src/organization-management/modules/organizations/components/OrganizationForm.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { Loader2 } from 'lucide-react';\nimport { ZodError } from 'zod';\nimport { useGetOrganizationBySlugLazyQuery } from '../../../../generated/global-operations';\nimport { useDebouncedValue } from '../../../shared/hooks/useDebouncedValue';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport type { CreateOrganizationInput, UpdateOrganizationInput, Organization } from '../types';\nimport { createOrganizationSchema, updateOrganizationSchema } from '../schemas';\n\ninterface OrganizationFormProps {\n organization?: Organization | null;\n tenantId?: string;\n tenantName?: string;\n /**\n * Hide the tenant field entirely. Set when the user has access to a single\n * tenant (the shared tenant) — there is nothing to choose, so we don't show\n * a picker; `tenantId` is still submitted (and the server defaults to the\n * shared tenant when omitted).\n */\n hideTenantSelector?: boolean;\n onSubmit: (data: CreateOrganizationInput | UpdateOrganizationInput) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\ntype SlugStatus =\n | { tone: 'muted'; message: string }\n | { tone: 'success'; message: string }\n | { tone: 'warning'; message: string };\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nfunction getSlugStatusClassName(tone: SlugStatus['tone']): string {\n switch (tone) {\n case 'success':\n return 'text-[var(--color-status-success-text)]';\n case 'warning':\n return 'text-[var(--color-status-warning-text)]';\n default:\n return 'text-[var(--color-text-muted)]';\n }\n}\n\nexport function OrganizationForm({\n organization,\n tenantId,\n tenantName,\n hideTenantSelector = false,\n onSubmit,\n onCancel,\n loading,\n}: OrganizationFormProps) {\n const tr = useTr();\n const isCreateMode = !organization;\n const currentOrganizationId = organization?.id ?? null;\n // react-doctor-disable-next-line react-doctor/no-event-handler\n const [formData, setFormData] = useState({\n // react-doctor-disable-next-line react-doctor/no-event-handler\n name: organization?.name || '',\n slug: organization?.slug || '',\n description: organization?.description || '',\n logo: organization?.logo || '',\n website: organization?.website || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n tenantId: tenantId || organization?.tenantId || '',\n });\n\n const [errors, setErrors] = useState<Record<string, string>>({});\n // react-doctor-disable-next-line react-doctor/rerender-state-only-in-handlers\n const [hasManualSlugEdits, setHasManualSlugEdits] = useState(false);\n const [slugStatus, setSlugStatus] = useState<SlugStatus>({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n const slugRequestIdRef = useRef(0);\n const lastAdjustedSlugRef = useRef<string | null>(null);\n const lastAdjustedBaseSlugRef = useRef<string | null>(null);\n const debouncedSlug = useDebouncedValue(formData.slug, 350);\n const [fetchOrganizationBySlug] = useGetOrganizationBySlugLazyQuery({\n fetchPolicy: 'network-only',\n });\n\n const tenantDisplayValue = useMemo(() => {\n if (!formData.tenantId) {\n return '';\n }\n\n if (tenantName) {\n return `${tenantName} (${formData.tenantId})`;\n }\n\n return formData.tenantId;\n }, [formData.tenantId, tenantName]);\n\n useEffect(() => {\n if (!isCreateMode || tenantId === undefined) {\n return;\n }\n\n setFormData((previousState) => {\n if (previousState.tenantId === tenantId) {\n return previousState;\n }\n\n return {\n ...previousState,\n tenantId,\n };\n });\n }, [isCreateMode, tenantId]);\n\n useEffect(() => {\n if (!isCreateMode) {\n return;\n }\n\n // react-doctor-disable-next-line react-doctor/no-event-handler\n const generatedSlug = slugify(formData.name);\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (!hasManualSlugEdits) {\n setFormData((previousState) => ({\n ...previousState,\n slug: generatedSlug,\n }));\n }\n\n if (!generatedSlug) {\n setSlugStatus({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n }\n }, [formData.name, hasManualSlugEdits, isCreateMode, tr]);\n\n useEffect(() => {\n if (!isCreateMode) {\n return;\n }\n\n if (!formData.tenantId || !debouncedSlug) {\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setSlugStatus({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n return;\n }\n\n if (lastAdjustedSlugRef.current === debouncedSlug && lastAdjustedBaseSlugRef.current !== null) {\n setSlugStatus({\n tone: 'warning',\n message: `${tr('organizations.form.slugAdjustedTo', 'Slug adjusted to')} “${debouncedSlug}” ${tr('organizations.form.slugBecause', 'because')} “${lastAdjustedBaseSlugRef.current}” ${tr('organizations.form.slugAlreadyTaken', 'is already taken.')}`,\n });\n return;\n }\n\n let isCancelled = false;\n const requestId = ++slugRequestIdRef.current;\n\n const resolveAvailableSlug = async () => {\n setSlugStatus({\n tone: 'muted',\n message: tr('organizations.form.slugChecking', 'Checking slug availability…'),\n });\n\n const baseSlug = debouncedSlug;\n let suffix = 2;\n let candidateSlug = baseSlug;\n\n while (true) {\n const result = await fetchOrganizationBySlug({\n variables: {\n tenantId: formData.tenantId,\n slug: candidateSlug,\n },\n });\n\n if (isCancelled || slugRequestIdRef.current !== requestId) {\n return;\n }\n\n const existingOrganizationId = (\n result.data?.organizationBySlug as { id?: string } | null | undefined\n )?.id;\n if (!existingOrganizationId || existingOrganizationId === currentOrganizationId) {\n if (candidateSlug !== baseSlug) {\n lastAdjustedSlugRef.current = candidateSlug;\n lastAdjustedBaseSlugRef.current = baseSlug;\n setFormData((previousState) => {\n if (previousState.slug !== baseSlug) {\n return previousState;\n }\n\n return {\n ...previousState,\n slug: candidateSlug,\n };\n });\n\n setSlugStatus({\n tone: 'warning',\n message: `${tr('organizations.form.slugAdjustedTo', 'Slug adjusted to')} “${candidateSlug}” ${tr('organizations.form.slugBecause', 'because')} “${baseSlug}” ${tr('organizations.form.slugAlreadyTaken', 'is already taken.')}`,\n });\n } else {\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setSlugStatus({\n tone: 'success',\n message: `${tr('organizations.form.slugPrefix', 'Slug')} “${candidateSlug}” ${tr('organizations.form.slugAvailable', 'is available.')}`,\n });\n }\n\n return;\n }\n\n candidateSlug = `${baseSlug}-${suffix}`;\n suffix += 1;\n }\n };\n\n void resolveAvailableSlug();\n\n return () => {\n isCancelled = true;\n };\n }, [\n currentOrganizationId,\n debouncedSlug,\n fetchOrganizationBySlug,\n formData.tenantId,\n isCreateMode,\n tr,\n ]);\n\n const validate = () => {\n try {\n const schema = organization ? updateOrganizationSchema : createOrganizationSchema;\n schema.parse(formData);\n setErrors({});\n return true;\n } catch (error) {\n if (error instanceof ZodError) {\n const newErrors: Record<string, string> = {};\n error.issues.forEach((issue) => {\n const path = issue.path.join('.');\n newErrors[path] = issue.message;\n });\n setErrors(newErrors);\n }\n return false;\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validate()) {\n return;\n }\n\n const submitData = organization\n ? {\n // Update only changed fields\n name: formData.name !== organization.name ? formData.name : undefined,\n description:\n formData.description !== organization.description ? formData.description : undefined,\n logo: formData.logo !== organization.logo ? formData.logo : undefined,\n website: formData.website !== organization.website ? formData.website : undefined,\n }\n : {\n // Create with all fields\n tenantId: formData.tenantId,\n name: formData.name,\n slug: formData.slug,\n description: formData.description || undefined,\n logo: formData.logo || undefined,\n website: formData.website || undefined,\n };\n\n await onSubmit(submitData);\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Tenant ID (only for create, and only when the user actually has a\n tenant choice — hidden for single-tenant users). */}\n {isCreateMode && !hideTenantSelector && (\n <div>\n <label\n htmlFor=\"tenantId\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.tenant', 'Tenant')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n {tenantId ? (\n <input\n id=\"tenantId\"\n type=\"text\"\n value={tenantDisplayValue}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-muted)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n disabled:cursor-not-allowed\n \"\n disabled\n readOnly\n aria-label={tr('organizations.form.selectedTenant', 'Selected tenant')}\n />\n ) : (\n <input\n id=\"tenantId\"\n type=\"text\"\n value={formData.tenantId}\n onChange={(e) => setFormData({ ...formData, tenantId: e.target.value })}\n aria-label={tr('organizations.form.tenant', 'Tenant')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.tenantPlaceholder', 'Enter tenant ID')}\n disabled={loading}\n required\n autoComplete=\"off\"\n />\n )}\n {errors.tenantId && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.tenantId}</p>\n )}\n </div>\n )}\n\n {/* Name */}\n <div>\n <label\n htmlFor=\"name\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.name', 'Organization Name')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData({ ...formData, name: e.target.value })}\n aria-label={tr('organizations.form.name', 'Organization Name')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.namePlaceholder', 'Enter organization name')}\n disabled={loading}\n required\n autoComplete=\"organization\"\n />\n {errors.name && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.name}</p>\n )}\n </div>\n\n {/* Slug */}\n <div>\n <label\n htmlFor=\"slug\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('common.slug', 'Slug')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"slug\"\n type=\"text\"\n value={formData.slug}\n aria-label={tr('common.slug', 'Slug')}\n onChange={(e) => {\n setHasManualSlugEdits(true);\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setFormData({\n ...formData,\n slug: slugify(e.target.value),\n });\n }}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.slugPlaceholder', 'organization-slug')}\n disabled={loading || !!organization}\n required={!organization}\n autoComplete=\"off\"\n />\n <p\n className={`text-xs mt-1 ${getSlugStatusClassName(\n isCreateMode ? slugStatus.tone : 'muted'\n )}`}\n >\n {isCreateMode\n ? slugStatus.message\n : tr(\n 'organizations.form.slugImmutable',\n 'Lowercase letters, numbers, and hyphens only. Cannot be changed after creation.'\n )}\n </p>\n {errors.slug && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.slug}</p>\n )}\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('common.description', 'Description')}\n </label>\n <textarea\n id=\"description\"\n value={formData.description}\n onChange={(e) => setFormData({ ...formData, description: e.target.value })}\n aria-label={tr('common.description', 'Description')}\n rows={4}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr(\n 'organizations.form.descriptionPlaceholder',\n 'Enter organization description'\n )}\n disabled={loading}\n autoComplete=\"off\"\n />\n </div>\n\n {/* Logo URL */}\n <div>\n <label\n htmlFor=\"logo\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.logoUrl', 'Logo URL')}\n </label>\n <input\n id=\"logo\"\n type=\"url\"\n value={formData.logo}\n onChange={(e) => setFormData({ ...formData, logo: e.target.value })}\n aria-label={tr('organizations.form.logoUrl', 'Logo URL')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder=\"https://example.com/logo.png\"\n disabled={loading}\n autoComplete=\"url\"\n />\n </div>\n\n {/* Website */}\n <div>\n <label\n htmlFor=\"website\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.detail.website', 'Website')}\n </label>\n <input\n id=\"website\"\n type=\"url\"\n value={formData.website}\n onChange={(e) => setFormData({ ...formData, website: e.target.value })}\n aria-label={tr('organizations.detail.website', 'Website')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder=\"https://example.com\"\n disabled={loading}\n autoComplete=\"url\"\n />\n </div>\n\n {/* Form Actions */}\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[220px]\n \"\n >\n {loading && <Loader2 size={16} className=\"animate-spin\" />}\n {organization\n ? tr('organizations.form.updateSubmit', 'Update Organization')\n : tr('organizations.create.button', 'Create Organization')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n \"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAS,EAAQ,GAAuB;AACtC,QAAO,EACJ,aAAa,CACb,MAAM,CACN,QAAQ,iBAAiB,GAAG,CAC5B,QAAQ,QAAQ,IAAI,CACpB,QAAQ,OAAO,IAAI,CACnB,QAAQ,UAAU,GAAG;;AAG1B,SAAS,EAAuB,GAAkC;AAChE,SAAQ,GAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,QACE,QAAO;;;AAIb,SAAgB,EAAiB,EAC/B,iBACA,aACA,eACA,wBAAqB,IACrB,aACA,aACA,cACwB;CACxB,IAAM,IAAK,GAAO,EACZ,IAAe,CAAC,GAChB,IAAwB,GAAc,MAAM,MAE5C,CAAC,GAAU,KAAe,EAAS;EAEvC,MAAM,GAAc,QAAQ;EAC5B,MAAM,GAAc,QAAQ;EAC5B,aAAa,GAAc,eAAe;EAC1C,MAAM,GAAc,QAAQ;EAC5B,SAAS,GAAc,WAAW;EAElC,UAAU,KAAY,GAAc,YAAY;EACjD,CAAC,EAEI,CAAC,GAAQ,KAAa,EAAiC,EAAE,CAAC,EAE1D,CAAC,GAAoB,KAAyB,EAAS,GAAM,EAC7D,CAAC,GAAY,KAAiB,EAAqB;EACvD,MAAM;EACN,SAAS,EACP,wCACA,qDACD;EACF,CAAC,EACI,IAAmB,EAAO,EAAE,EAC5B,IAAsB,EAAsB,KAAK,EACjD,IAA0B,EAAsB,KAAK,EACrD,IAAgB,EAAkB,EAAS,MAAM,IAAI,EACrD,CAAC,KAA2B,EAAkC,EAClE,aAAa,gBACd,CAAC,EAEI,IAAqB,QACpB,EAAS,WAIV,IACK,GAAG,EAAW,IAAI,EAAS,SAAS,KAGtC,EAAS,WAPP,IAQR,CAAC,EAAS,UAAU,EAAW,CAAC;AAgDnC,CA9CA,QAAgB;AACV,GAAC,KAAgB,MAAa,KAAA,KAIlC,GAAa,MACP,EAAc,aAAa,IACtB,IAGF;GACL,GAAG;GACH;GACD,CACD;IACD,CAAC,GAAc,EAAS,CAAC,EAE5B,QAAgB;AACd,MAAI,CAAC,EACH;EAIF,IAAM,IAAgB,EAAQ,EAAS,KAAK;AAY5C,EAXA,EAAoB,UAAU,MAC9B,EAAwB,UAAU,MAG7B,KACH,GAAa,OAAmB;GAC9B,GAAG;GACH,MAAM;GACP,EAAE,EAGA,KACH,EAAc;GACZ,MAAM;GACN,SAAS,EACP,wCACA,qDACD;GACF,CAAC;IAEH;EAAC,EAAS;EAAM;EAAoB;EAAc;EAAG,CAAC,EAEzD,QAAgB;AACd,MAAI,CAAC,EACH;AAGF,MAAI,CAAC,EAAS,YAAY,CAAC,GAAe;AAGxC,GAFA,EAAoB,UAAU,MAC9B,EAAwB,UAAU,MAClC,EAAc;IACZ,MAAM;IACN,SAAS,EACP,wCACA,qDACD;IACF,CAAC;AACF;;AAGF,MAAI,EAAoB,YAAY,KAAiB,EAAwB,YAAY,MAAM;AAC7F,KAAc;IACZ,MAAM;IACN,SAAS,GAAG,EAAG,qCAAqC,mBAAmB,CAAC,IAAI,EAAc,IAAI,EAAG,kCAAkC,UAAU,CAAC,IAAI,EAAwB,QAAQ,IAAI,EAAG,uCAAuC,oBAAoB;IACrP,CAAC;AACF;;EAGF,IAAI,IAAc,IACZ,IAAY,EAAE,EAAiB;AAiErC,UA/D6B,YAAY;AACvC,KAAc;IACZ,MAAM;IACN,SAAS,EAAG,mCAAmC,8BAA8B;IAC9E,CAAC;GAEF,IAAM,IAAW,GACb,IAAS,GACT,IAAgB;AAEpB,YAAa;IACX,IAAM,IAAS,MAAM,EAAwB,EAC3C,WAAW;KACT,UAAU,EAAS;KACnB,MAAM;KACP,EACF,CAAC;AAEF,QAAI,KAAe,EAAiB,YAAY,EAC9C;IAGF,IAAM,IACJ,EAAO,MAAM,oBACZ;AACH,QAAI,CAAC,KAA0B,MAA2B,GAAuB;AAC/E,KAAI,MAAkB,KAmBpB,EAAoB,UAAU,MAC9B,EAAwB,UAAU,MAClC,EAAc;MACZ,MAAM;MACN,SAAS,GAAG,EAAG,iCAAiC,OAAO,CAAC,IAAI,EAAc,IAAI,EAAG,oCAAoC,gBAAgB;MACtI,CAAC,KAvBF,EAAoB,UAAU,GAC9B,EAAwB,UAAU,GAClC,GAAa,MACP,EAAc,SAAS,IAIpB;MACL,GAAG;MACH,MAAM;MACP,GANQ,EAOT,EAEF,EAAc;MACZ,MAAM;MACN,SAAS,GAAG,EAAG,qCAAqC,mBAAmB,CAAC,IAAI,EAAc,IAAI,EAAG,kCAAkC,UAAU,CAAC,IAAI,EAAS,IAAI,EAAG,uCAAuC,oBAAoB;MAC9N,CAAC;AAUJ;;AAIF,IADA,IAAgB,GAAG,EAAS,GAAG,KAC/B,KAAU;;MAIa,QAEd;AACX,OAAc;;IAEf;EACD;EACA;EACA;EACA,EAAS;EACT;EACA;EACD,CAAC;CAEF,IAAM,UAAiB;AACrB,MAAI;AAIF,WAHe,IAAe,IAA2B,GAClD,MAAM,EAAS,EACtB,EAAU,EAAE,CAAC,EACN;WACA,GAAO;AACd,OAAI,aAAiB,GAAU;IAC7B,IAAM,IAAoC,EAAE;AAK5C,IAJA,EAAM,OAAO,SAAS,MAAU;KAC9B,IAAM,IAAO,EAAM,KAAK,KAAK,IAAI;AACjC,OAAU,KAAQ,EAAM;MACxB,EACF,EAAU,EAAU;;AAEtB,UAAO;;;AAiCX,QACE,kBAAC,QAAD;EAAM,UA9Ba,OAAO,MAAuB;AACjD,KAAE,gBAAgB,EAEb,GAAU,IAuBf,MAAM,EAnBa,IACf;IAEE,MAAM,EAAS,SAAS,EAAa,OAAuB,KAAA,IAAhB,EAAS;IACrD,aACE,EAAS,gBAAgB,EAAa,cAAqC,KAAA,IAAvB,EAAS;IAC/D,MAAM,EAAS,SAAS,EAAa,OAAuB,KAAA,IAAhB,EAAS;IACrD,SAAS,EAAS,YAAY,EAAa,UAA6B,KAAA,IAAnB,EAAS;IAC/D,GACD;IAEE,UAAU,EAAS;IACnB,MAAM,EAAS;IACf,MAAM,EAAS;IACf,aAAa,EAAS,eAAe,KAAA;IACrC,MAAM,EAAS,QAAQ,KAAA;IACvB,SAAS,EAAS,WAAW,KAAA;IAC9B,CAEqB;;EAII,WAAU;YAAxC;GAGG,KAAgB,CAAC,KAChB,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,6BAA6B,SAAS;MAAE;MAC5C,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACP,IACC,kBAAC,SAAD;KACE,IAAG;KACH,MAAK;KACL,OAAO;KACP,WAAU;KASV,UAAA;KACA,UAAA;KACA,cAAY,EAAG,qCAAqC,kBAAkB;KACtE,CAAA,GAEF,kBAAC,SAAD;KACE,IAAG;KACH,MAAK;KACL,OAAO,EAAS;KAChB,WAAW,MAAM,EAAY;MAAE,GAAG;MAAU,UAAU,EAAE,OAAO;MAAO,CAAC;KACvE,cAAY,EAAG,6BAA6B,SAAS;KACrD,WAAU;KAUV,aAAa,EAAG,wCAAwC,kBAAkB;KAC1E,UAAU;KACV,UAAA;KACA,cAAa;KACb,CAAA;IAEH,EAAO,YACN,kBAAC,KAAD;KAAG,WAAU;eAAsD,EAAO;KAAa,CAAA;IAErF,EAAA,CAAA;GAIR,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,2BAA2B,oBAAoB;MAAE;MACrD,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACR,kBAAC,SAAD;KACE,IAAG;KACH,MAAK;KACL,OAAO,EAAS;KAChB,WAAW,MAAM,EAAY;MAAE,GAAG;MAAU,MAAM,EAAE,OAAO;MAAO,CAAC;KACnE,cAAY,EAAG,2BAA2B,oBAAoB;KAC9D,WAAU;KAUV,aAAa,EAAG,sCAAsC,0BAA0B;KAChF,UAAU;KACV,UAAA;KACA,cAAa;KACb,CAAA;IACD,EAAO,QACN,kBAAC,KAAD;KAAG,WAAU;eAAsD,EAAO;KAAS,CAAA;IAEjF,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,eAAe,OAAO;MAAE;MAC5B,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACR,kBAAC,SAAD;KACE,IAAG;KACH,MAAK;KACL,OAAO,EAAS;KAChB,cAAY,EAAG,eAAe,OAAO;KACrC,WAAW,MAAM;AAIf,MAHA,EAAsB,GAAK,EAC3B,EAAoB,UAAU,MAC9B,EAAwB,UAAU,MAClC,EAAY;OACV,GAAG;OACH,MAAM,EAAQ,EAAE,OAAO,MAAM;OAC9B,CAAC;;KAEJ,WAAU;KAUV,aAAa,EAAG,sCAAsC,oBAAoB;KAC1E,UAAU,KAAW,CAAC,CAAC;KACvB,UAAU,CAAC;KACX,cAAa;KACb,CAAA;IACF,kBAAC,KAAD;KACE,WAAW,gBAAgB,EACzB,IAAe,EAAW,OAAO,QAClC;eAEA,IACG,EAAW,UACX,EACE,oCACA,kFACD;KACH,CAAA;IACH,EAAO,QACN,kBAAC,KAAD;KAAG,WAAU;eAAsD,EAAO;KAAS,CAAA;IAEjF,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAET,EAAG,sBAAsB,cAAc;IAClC,CAAA,EACR,kBAAC,YAAD;IACE,IAAG;IACH,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,aAAa,EAAE,OAAO;KAAO,CAAC;IAC1E,cAAY,EAAG,sBAAsB,cAAc;IACnD,MAAM;IACN,WAAU;IAUV,aAAa,EACX,6CACA,iCACD;IACD,UAAU;IACV,cAAa;IACb,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAET,EAAG,8BAA8B,WAAW;IACvC,CAAA,EACR,kBAAC,SAAD;IACE,IAAG;IACH,MAAK;IACL,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,MAAM,EAAE,OAAO;KAAO,CAAC;IACnE,cAAY,EAAG,8BAA8B,WAAW;IACxD,WAAU;IAUV,aAAY;IACZ,UAAU;IACV,cAAa;IACb,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAET,EAAG,gCAAgC,UAAU;IACxC,CAAA,EACR,kBAAC,SAAD;IACE,IAAG;IACH,MAAK;IACL,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,SAAS,EAAE,OAAO;KAAO,CAAC;IACtE,cAAY,EAAG,gCAAgC,UAAU;IACzD,WAAU;IAUV,aAAY;IACZ,UAAU;IACV,cAAa;IACb,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,WAAU;eAHZ,CAgBG,KAAW,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,EACzD,IACG,EAAG,mCAAmC,sBAAsB,GAC5D,EAAG,+BAA+B,sBAAsB,CACrD;QAET,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU;KACV,WAAU;eAcT,EAAG,iBAAiB,SAAS;KACvB,CAAA,CACL;;GACD"}
|
|
1
|
+
{"version":3,"file":"OrganizationForm.js","names":[],"sources":["../../../../../src/organization-management/modules/organizations/components/OrganizationForm.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { Loader2 } from 'lucide-react';\nimport { ZodError } from 'zod';\nimport { useGetOrganizationBySlugLazyQuery } from '../../../../generated/global-operations';\nimport { useDebouncedValue } from '../../../shared/hooks/useDebouncedValue';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport type { CreateOrganizationInput, UpdateOrganizationInput, Organization } from '../types';\nimport { createOrganizationSchema, updateOrganizationSchema } from '../schemas';\n\ninterface OrganizationFormProps {\n organization?: Organization | null;\n tenantId?: string;\n tenantName?: string;\n /**\n * Hide the tenant field entirely. Set when the user has access to a single\n * tenant (the shared tenant) — there is nothing to choose, so we don't show\n * a picker; `tenantId` is still submitted (and the server defaults to the\n * shared tenant when omitted).\n */\n hideTenantSelector?: boolean;\n onSubmit: (data: CreateOrganizationInput | UpdateOrganizationInput) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\ntype SlugStatus =\n | { tone: 'muted'; message: string }\n | { tone: 'success'; message: string }\n | { tone: 'warning'; message: string };\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nfunction getSlugStatusClassName(tone: SlugStatus['tone']): string {\n switch (tone) {\n case 'success':\n return 'text-[var(--color-status-success-text)]';\n case 'warning':\n return 'text-[var(--color-status-warning-text)]';\n default:\n return 'text-[var(--color-text-muted)]';\n }\n}\n\nexport function OrganizationForm({\n organization,\n tenantId,\n tenantName,\n hideTenantSelector = false,\n onSubmit,\n onCancel,\n loading,\n}: OrganizationFormProps) {\n const tr = useTr();\n const isCreateMode = !organization;\n const currentOrganizationId = organization?.id ?? null;\n // react-doctor-disable-next-line react-doctor/no-event-handler\n const [formData, setFormData] = useState({\n // react-doctor-disable-next-line react-doctor/no-event-handler\n name: organization?.name || '',\n slug: organization?.slug || '',\n description: organization?.description || '',\n logo: organization?.logo || '',\n website: organization?.website || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n tenantId: tenantId || organization?.tenantId || '',\n });\n\n const [errors, setErrors] = useState<Record<string, string>>({});\n // react-doctor-disable-next-line react-doctor/rerender-state-only-in-handlers\n const [hasManualSlugEdits, setHasManualSlugEdits] = useState(false);\n const [slugStatus, setSlugStatus] = useState<SlugStatus>({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n const slugRequestIdRef = useRef(0);\n const lastAdjustedSlugRef = useRef<string | null>(null);\n const lastAdjustedBaseSlugRef = useRef<string | null>(null);\n const debouncedSlug = useDebouncedValue(formData.slug, 350);\n const [fetchOrganizationBySlug] = useGetOrganizationBySlugLazyQuery({\n fetchPolicy: 'network-only',\n });\n\n const tenantDisplayValue = useMemo(() => {\n if (!formData.tenantId) {\n return '';\n }\n\n if (tenantName) {\n return `${tenantName} (${formData.tenantId})`;\n }\n\n return formData.tenantId;\n }, [formData.tenantId, tenantName]);\n\n useEffect(() => {\n if (!isCreateMode || tenantId === undefined) {\n return;\n }\n\n setFormData((previousState) => {\n if (previousState.tenantId === tenantId) {\n return previousState;\n }\n\n return {\n ...previousState,\n tenantId,\n };\n });\n }, [isCreateMode, tenantId]);\n\n useEffect(() => {\n if (!isCreateMode) {\n return;\n }\n\n // react-doctor-disable-next-line react-doctor/no-event-handler\n const generatedSlug = slugify(formData.name);\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (!hasManualSlugEdits) {\n setFormData((previousState) => ({\n ...previousState,\n slug: generatedSlug,\n }));\n }\n\n if (!generatedSlug) {\n setSlugStatus({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n }\n }, [formData.name, hasManualSlugEdits, isCreateMode, tr]);\n\n useEffect(() => {\n if (!isCreateMode) {\n return;\n }\n\n if (!formData.tenantId || !debouncedSlug) {\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setSlugStatus({\n tone: 'muted',\n message: tr(\n 'organizations.form.slugAutoGenerated',\n 'Slug is auto-generated from the organization name.'\n ),\n });\n return;\n }\n\n if (lastAdjustedSlugRef.current === debouncedSlug && lastAdjustedBaseSlugRef.current !== null) {\n setSlugStatus({\n tone: 'warning',\n message: `${tr('organizations.form.slugAdjustedTo', 'Slug adjusted to')} “${debouncedSlug}” ${tr('organizations.form.slugBecause', 'because')} “${lastAdjustedBaseSlugRef.current}” ${tr('organizations.form.slugAlreadyTaken', 'is already taken.')}`,\n });\n return;\n }\n\n let isCancelled = false;\n const requestId = ++slugRequestIdRef.current;\n\n const resolveAvailableSlug = async () => {\n setSlugStatus({\n tone: 'muted',\n message: tr('organizations.form.slugChecking', 'Checking slug availability…'),\n });\n\n const baseSlug = debouncedSlug;\n let suffix = 2;\n let candidateSlug = baseSlug;\n\n while (true) {\n const result = await fetchOrganizationBySlug({\n variables: {\n tenantId: formData.tenantId,\n slug: candidateSlug,\n },\n });\n\n if (isCancelled || slugRequestIdRef.current !== requestId) {\n return;\n }\n\n const existingOrganizationId = (\n result.data?.organizationBySlug as { id?: string } | null | undefined\n )?.id;\n if (!existingOrganizationId || existingOrganizationId === currentOrganizationId) {\n if (candidateSlug !== baseSlug) {\n lastAdjustedSlugRef.current = candidateSlug;\n lastAdjustedBaseSlugRef.current = baseSlug;\n setFormData((previousState) => {\n if (previousState.slug !== baseSlug) {\n return previousState;\n }\n\n return {\n ...previousState,\n slug: candidateSlug,\n };\n });\n\n setSlugStatus({\n tone: 'warning',\n message: `${tr('organizations.form.slugAdjustedTo', 'Slug adjusted to')} “${candidateSlug}” ${tr('organizations.form.slugBecause', 'because')} “${baseSlug}” ${tr('organizations.form.slugAlreadyTaken', 'is already taken.')}`,\n });\n } else {\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setSlugStatus({\n tone: 'success',\n message: `${tr('organizations.form.slugPrefix', 'Slug')} “${candidateSlug}” ${tr('organizations.form.slugAvailable', 'is available.')}`,\n });\n }\n\n return;\n }\n\n candidateSlug = `${baseSlug}-${suffix}`;\n suffix += 1;\n }\n };\n\n void resolveAvailableSlug();\n\n return () => {\n isCancelled = true;\n };\n }, [\n currentOrganizationId,\n debouncedSlug,\n fetchOrganizationBySlug,\n formData.tenantId,\n isCreateMode,\n tr,\n ]);\n\n const validate = () => {\n try {\n const schema = organization ? updateOrganizationSchema(tr) : createOrganizationSchema(tr);\n schema.parse(formData);\n setErrors({});\n return true;\n } catch (error) {\n if (error instanceof ZodError) {\n const newErrors: Record<string, string> = {};\n error.issues.forEach((issue) => {\n const path = issue.path.join('.');\n newErrors[path] = issue.message;\n });\n setErrors(newErrors);\n }\n return false;\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validate()) {\n return;\n }\n\n const submitData = organization\n ? {\n // Update only changed fields\n name: formData.name !== organization.name ? formData.name : undefined,\n description:\n formData.description !== organization.description ? formData.description : undefined,\n logo: formData.logo !== organization.logo ? formData.logo : undefined,\n website: formData.website !== organization.website ? formData.website : undefined,\n }\n : {\n // Create with all fields\n tenantId: formData.tenantId,\n name: formData.name,\n slug: formData.slug,\n description: formData.description || undefined,\n logo: formData.logo || undefined,\n website: formData.website || undefined,\n };\n\n await onSubmit(submitData);\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Tenant ID (only for create, and only when the user actually has a\n tenant choice — hidden for single-tenant users). */}\n {isCreateMode && !hideTenantSelector && (\n <div>\n <label\n htmlFor=\"tenantId\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.tenant', 'Tenant')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n {tenantId ? (\n <input\n id=\"tenantId\"\n type=\"text\"\n value={tenantDisplayValue}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-muted)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n disabled:cursor-not-allowed\n \"\n disabled\n readOnly\n aria-label={tr('organizations.form.selectedTenant', 'Selected tenant')}\n />\n ) : (\n <input\n id=\"tenantId\"\n type=\"text\"\n value={formData.tenantId}\n onChange={(e) => setFormData({ ...formData, tenantId: e.target.value })}\n aria-label={tr('organizations.form.tenant', 'Tenant')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.tenantPlaceholder', 'Enter tenant ID')}\n disabled={loading}\n required\n autoComplete=\"off\"\n />\n )}\n {errors.tenantId && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.tenantId}</p>\n )}\n </div>\n )}\n\n {/* Name */}\n <div>\n <label\n htmlFor=\"name\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.name', 'Organization Name')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData({ ...formData, name: e.target.value })}\n aria-label={tr('organizations.form.name', 'Organization Name')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.namePlaceholder', 'Enter organization name')}\n disabled={loading}\n required\n autoComplete=\"organization\"\n />\n {errors.name && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.name}</p>\n )}\n </div>\n\n {/* Slug */}\n <div>\n <label\n htmlFor=\"slug\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('common.slug', 'Slug')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"slug\"\n type=\"text\"\n value={formData.slug}\n aria-label={tr('common.slug', 'Slug')}\n onChange={(e) => {\n setHasManualSlugEdits(true);\n lastAdjustedSlugRef.current = null;\n lastAdjustedBaseSlugRef.current = null;\n setFormData({\n ...formData,\n slug: slugify(e.target.value),\n });\n }}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.slugPlaceholder', 'organization-slug')}\n disabled={loading || !!organization}\n required={!organization}\n autoComplete=\"off\"\n />\n <p\n className={`text-xs mt-1 ${getSlugStatusClassName(\n isCreateMode ? slugStatus.tone : 'muted'\n )}`}\n >\n {isCreateMode\n ? slugStatus.message\n : tr(\n 'organizations.form.slugImmutable',\n 'Lowercase letters, numbers, and hyphens only. Cannot be changed after creation.'\n )}\n </p>\n {errors.slug && (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.slug}</p>\n )}\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"description\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('common.description', 'Description')}\n </label>\n <textarea\n id=\"description\"\n value={formData.description}\n onChange={(e) => setFormData({ ...formData, description: e.target.value })}\n aria-label={tr('common.description', 'Description')}\n rows={4}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr(\n 'organizations.form.descriptionPlaceholder',\n 'Enter organization description'\n )}\n disabled={loading}\n autoComplete=\"off\"\n />\n </div>\n\n {/* Logo URL */}\n <div>\n <label\n htmlFor=\"logo\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.form.logoUrl', 'Logo URL')}\n </label>\n <input\n id=\"logo\"\n type=\"url\"\n value={formData.logo}\n onChange={(e) => setFormData({ ...formData, logo: e.target.value })}\n aria-label={tr('organizations.form.logoUrl', 'Logo URL')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.logoPlaceholder', 'https://example.com/logo.png')}\n disabled={loading}\n autoComplete=\"url\"\n />\n </div>\n\n {/* Website */}\n <div>\n <label\n htmlFor=\"website\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('organizations.detail.website', 'Website')}\n </label>\n <input\n id=\"website\"\n type=\"url\"\n value={formData.website}\n onChange={(e) => setFormData({ ...formData, website: e.target.value })}\n aria-label={tr('organizations.detail.website', 'Website')}\n className=\"\n w-full px-3 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('organizations.form.websitePlaceholder', 'https://example.com')}\n disabled={loading}\n autoComplete=\"url\"\n />\n </div>\n\n {/* Form Actions */}\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[220px]\n \"\n >\n {loading && <Loader2 size={16} className=\"animate-spin\" />}\n {organization\n ? tr('organizations.form.updateSubmit', 'Update Organization')\n : tr('organizations.create.button', 'Create Organization')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n \"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { OrganizationStatus as e } from "../../../shared/types/graphql.js";
|
|
2
2
|
import { literalType as t, nativeEnumType as n, objectType as r, recordType as i, stringType as a, unknownType as o } from "../../../../node_modules/zod/v3/types.js";
|
|
3
3
|
//#region src/organization-management/modules/organizations/schemas/index.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
4
|
+
function s(e) {
|
|
5
|
+
return {
|
|
6
|
+
name: a().min(2, e("organizations.validation.nameMin", "Name must be at least 2 characters")).max(100, e("organizations.validation.nameMax", "Name must not exceed 100 characters")).trim(),
|
|
7
|
+
slug: a().min(2, e("organizations.validation.slugMin", "Slug must be at least 2 characters")).max(50, e("organizations.validation.slugMax", "Slug must not exceed 50 characters")).regex(/^[a-z0-9-]+$/, e("organizations.validation.slugPattern", "Slug must contain only lowercase letters, numbers, and hyphens")).trim(),
|
|
8
|
+
description: a().max(500, e("organizations.validation.descriptionMax", "Description must not exceed 500 characters")).optional().or(t("")),
|
|
9
|
+
logo: a().url(e("organizations.validation.urlInvalid", "Must be a valid URL")).optional().or(t("")),
|
|
10
|
+
website: a().url(e("organizations.validation.urlInvalid", "Must be a valid URL")).optional().or(t("")),
|
|
11
|
+
metadata: i(a(), o()).optional()
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function c(t) {
|
|
15
|
+
return r({
|
|
16
|
+
tenantId: a().min(1, t("organizations.validation.tenantRequired", "Tenant is required")),
|
|
17
|
+
...s(t),
|
|
18
|
+
status: n(e).optional()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function l(e) {
|
|
22
|
+
let t = s(e);
|
|
23
|
+
return r({
|
|
24
|
+
name: t.name.optional(),
|
|
25
|
+
description: t.description,
|
|
26
|
+
logo: t.logo,
|
|
27
|
+
website: t.website,
|
|
28
|
+
metadata: t.metadata
|
|
29
|
+
});
|
|
30
|
+
}
|
|
22
31
|
//#endregion
|
|
23
32
|
export { c as createOrganizationSchema, l as updateOrganizationSchema };
|
|
24
33
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../src/organization-management/modules/organizations/schemas/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { OrganizationStatus } from '../types';\n\n/**\n * Organization Validation Schemas\n * Zod schemas for validating organization form inputs
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../src/organization-management/modules/organizations/schemas/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { OrganizationStatus } from '../types';\nimport type { TranslationFn } from '../../../shared/hooks/useTr';\n\n/**\n * Organization Validation Schemas\n * Zod schemas for validating organization form inputs.\n *\n * Exported as factory functions so validation messages can be translated\n * via the same `tr()` helper used by components.\n */\n\nfunction baseOrganizationSchema(tr: TranslationFn) {\n return {\n name: z\n .string()\n .min(2, tr('organizations.validation.nameMin', 'Name must be at least 2 characters'))\n .max(100, tr('organizations.validation.nameMax', 'Name must not exceed 100 characters'))\n .trim(),\n slug: z\n .string()\n .min(2, tr('organizations.validation.slugMin', 'Slug must be at least 2 characters'))\n .max(50, tr('organizations.validation.slugMax', 'Slug must not exceed 50 characters'))\n .regex(\n /^[a-z0-9-]+$/,\n tr(\n 'organizations.validation.slugPattern',\n 'Slug must contain only lowercase letters, numbers, and hyphens'\n )\n )\n .trim(),\n description: z\n .string()\n .max(\n 500,\n tr('organizations.validation.descriptionMax', 'Description must not exceed 500 characters')\n )\n .optional()\n .or(z.literal('')),\n logo: z\n .string()\n .url(tr('organizations.validation.urlInvalid', 'Must be a valid URL'))\n .optional()\n .or(z.literal('')),\n website: z\n .string()\n .url(tr('organizations.validation.urlInvalid', 'Must be a valid URL'))\n .optional()\n .or(z.literal('')),\n metadata: z.record(z.string(), z.unknown()).optional(),\n };\n}\n\n// Create Organization Schema\nexport function createOrganizationSchema(tr: TranslationFn) {\n return z.object({\n tenantId: z\n .string()\n .min(1, tr('organizations.validation.tenantRequired', 'Tenant is required')),\n ...baseOrganizationSchema(tr),\n status: z.nativeEnum(OrganizationStatus).optional(),\n });\n}\n\n// Update Organization Schema\nexport function updateOrganizationSchema(tr: TranslationFn) {\n const base = baseOrganizationSchema(tr);\n return z.object({\n name: base.name.optional(),\n description: base.description,\n logo: base.logo,\n website: base.website,\n metadata: base.metadata,\n });\n}\n\n// Type exports\nexport type CreateOrganizationFormData = z.infer<ReturnType<typeof createOrganizationSchema>>;\nexport type UpdateOrganizationFormData = z.infer<ReturnType<typeof updateOrganizationSchema>>;\n"],"mappings":";;;AAYA,SAAS,EAAuB,GAAmB;AACjD,QAAO;EACL,MAAM,GACK,CACR,IAAI,GAAG,EAAG,oCAAoC,qCAAqC,CAAC,CACpF,IAAI,KAAK,EAAG,oCAAoC,sCAAsC,CAAC,CACvF,MAAM;EACT,MAAM,GACK,CACR,IAAI,GAAG,EAAG,oCAAoC,qCAAqC,CAAC,CACpF,IAAI,IAAI,EAAG,oCAAoC,qCAAqC,CAAC,CACrF,MACC,gBACA,EACE,wCACA,iEACD,CACF,CACA,MAAM;EACT,aAAa,GACF,CACR,IACC,KACA,EAAG,2CAA2C,6CAA6C,CAC5F,CACA,UAAU,CACV,GAAG,EAAU,GAAG,CAAC;EACpB,MAAM,GACK,CACR,IAAI,EAAG,uCAAuC,sBAAsB,CAAC,CACrE,UAAU,CACV,GAAG,EAAU,GAAG,CAAC;EACpB,SAAS,GACE,CACR,IAAI,EAAG,uCAAuC,sBAAsB,CAAC,CACrE,UAAU,CACV,GAAG,EAAU,GAAG,CAAC;EACpB,UAAU,EAAS,GAAU,EAAE,GAAW,CAAC,CAAC,UAAU;EACvD;;AAIH,SAAgB,EAAyB,GAAmB;AAC1D,QAAO,EAAS;EACd,UAAU,GACC,CACR,IAAI,GAAG,EAAG,2CAA2C,qBAAqB,CAAC;EAC9E,GAAG,EAAuB,EAAG;EAC7B,QAAQ,EAAa,EAAmB,CAAC,UAAU;EACpD,CAAC;;AAIJ,SAAgB,EAAyB,GAAmB;CAC1D,IAAM,IAAO,EAAuB,EAAG;AACvC,QAAO,EAAS;EACd,MAAM,EAAK,KAAK,UAAU;EAC1B,aAAa,EAAK;EAClB,MAAM,EAAK;EACX,SAAS,EAAK;EACd,UAAU,EAAK;EAChB,CAAC"}
|
|
@@ -59,7 +59,7 @@ function g({ workspace: g, tenantId: _, organizationId: v, organizationName: y,
|
|
|
59
59
|
z
|
|
60
60
|
]), V = () => {
|
|
61
61
|
try {
|
|
62
|
-
return (g ? o : a).parse(T), O({}), !0;
|
|
62
|
+
return (g ? o(w) : a(w)).parse(T), O({}), !0;
|
|
63
63
|
} catch (e) {
|
|
64
64
|
if (e instanceof i) {
|
|
65
65
|
let t = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceForm.js","names":[],"sources":["../../../../../src/organization-management/modules/workspaces/components/WorkspaceForm.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Building, Home, Loader2 } from 'lucide-react';\nimport { ZodError } from 'zod';\nimport type { CreateWorkspaceInput, UpdateWorkspaceInput, Workspace } from '../types';\nimport { createWorkspaceSchema, updateWorkspaceSchema } from '../schemas';\nimport { useOrganizations } from '../../organizations/hooks/useOrganizations';\nimport { useOrgsWithPermission } from '../../organizations/hooks/useOrgsWithPermission';\nimport { useTenants } from '../hooks/useWorkspaces';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface WorkspaceFormProps {\n workspace?: Workspace | null;\n tenantId?: string;\n tenantName?: string;\n organizationId?: string;\n organizationName?: string;\n showOrganizationSelector?: boolean;\n onSubmit: (data: CreateWorkspaceInput | UpdateWorkspaceInput) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\nexport function WorkspaceForm({\n workspace,\n tenantId,\n organizationId,\n organizationName,\n showOrganizationSelector = false,\n onSubmit,\n onCancel,\n loading,\n}: WorkspaceFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n // react-doctor-disable-next-line react-doctor/no-event-handler\n name: workspace?.name || '',\n slug: workspace?.slug || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n tenantId: tenantId || workspace?.tenantId || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n organizationId: organizationId || workspace?.organizationId || '',\n });\n\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const shouldLoadTenants = !workspace && showOrganizationSelector && !tenantId;\n const shouldLoadOrganizations = !workspace && showOrganizationSelector && !organizationId;\n\n const { tenants, loading: tenantsLoading } = useTenants(100, 0, shouldLoadTenants);\n\n // Show the tenant picker only when the user genuinely has more than one\n // tenant to choose between. With a single shared tenant there is nothing to\n // choose, so the picker is hidden and the tenant is auto-selected.\n const showTenantPicker = shouldLoadTenants && tenants.length > 1;\n\n // Auto-select the shared (or only) tenant when there is no explicit tenant\n // and no real choice to make, so the workspace lands in the shared tenant\n // without surfacing a picker.\n useEffect(() => {\n if (!shouldLoadTenants || formData.tenantId || tenants.length === 0) {\n return;\n }\n const sharedTenant = tenants.find((tenant) => tenant.type === 'SHARED');\n const resolved = sharedTenant?.id ?? tenants[0]?.id;\n if (resolved) {\n setFormData((previousState) =>\n previousState.tenantId ? previousState : { ...previousState, tenantId: resolved }\n );\n }\n }, [shouldLoadTenants, tenants, formData.tenantId]);\n const { organizations, loading: organizationsLoading } = useOrganizations(\n formData.tenantId ? { tenantId: formData.tenantId } : undefined,\n 100,\n 0,\n shouldLoadOrganizations\n );\n\n // Hide orgs where the user can't actually create a workspace, so the\n // dropdown only surfaces options the gateway @rbac(workspace:create) will\n // accept. Membership alone isn't enough — a Viewer of an org is a member\n // but has workspace=VIEWER (no create). The bulk checkPermissions call is\n // one round-trip regardless of how many orgs are on the list.\n const allOrgIds = useMemo(\n () => organizations.map((organization) => organization.id),\n [organizations]\n );\n const { allowedIds: workspaceCreateAllowedOrgIds, loading: permissionCheckLoading } =\n useOrgsWithPermission(allOrgIds, 'workspace', 'create');\n const filterByPermission = !workspace && showOrganizationSelector && !organizationId;\n const permittedOrganizations = useMemo(\n () =>\n filterByPermission\n ? organizations.filter((organization) => workspaceCreateAllowedOrgIds.has(organization.id))\n : organizations,\n [organizations, workspaceCreateAllowedOrgIds, filterByPermission]\n );\n\n useEffect(() => {\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (tenantId && tenantId !== formData.tenantId) {\n setFormData((previousState) => ({ ...previousState, tenantId }));\n }\n }, [formData.tenantId, tenantId]);\n\n useEffect(() => {\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (organizationId && organizationId !== formData.organizationId) {\n setFormData((previousState) => ({ ...previousState, organizationId }));\n }\n }, [formData.organizationId, organizationId]);\n\n useEffect(() => {\n if (!showOrganizationSelector || organizationId || workspace) {\n return;\n }\n\n setFormData((previousState) => {\n if (!previousState.organizationId) {\n return previousState;\n }\n\n const hasMatchingOrganization = permittedOrganizations.some(\n (organization) => organization.id === previousState.organizationId\n );\n\n if (hasMatchingOrganization) {\n return previousState;\n }\n\n return { ...previousState, organizationId: '' };\n });\n }, [organizationId, permittedOrganizations, showOrganizationSelector, workspace]);\n\n const selectedOrganizationLabel = useMemo(() => {\n if (organizationName) return organizationName;\n\n const selectedOrganization = permittedOrganizations.find(\n (organization) => organization.id === formData.organizationId\n );\n return selectedOrganization?.name ?? formData.organizationId;\n }, [formData.organizationId, organizationName, permittedOrganizations]);\n\n const validate = () => {\n try {\n const schema = workspace ? updateWorkspaceSchema : createWorkspaceSchema;\n schema.parse(formData);\n setErrors({});\n return true;\n } catch (validationError) {\n if (validationError instanceof ZodError) {\n const nextErrors: Record<string, string> = {};\n validationError.issues.forEach((issue) => {\n const path = issue.path.join('.');\n nextErrors[path] = issue.message;\n });\n setErrors(nextErrors);\n }\n\n return false;\n }\n };\n\n const handleSubmit = async (event: React.FormEvent) => {\n event.preventDefault();\n\n if (!validate()) {\n return;\n }\n\n const submitData = workspace\n ? {\n name: formData.name !== workspace.name ? formData.name : undefined,\n }\n : {\n tenantId: formData.tenantId,\n organizationId: formData.organizationId,\n name: formData.name,\n slug: formData.slug,\n };\n\n await onSubmit(submitData);\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {!workspace ? (\n <>\n {/* Tenant picker — shown ONLY when the user has more than one tenant\n to choose between. For single-tenant (shared) users the tenant is\n auto-selected and no picker is rendered. */}\n {showTenantPicker ? (\n <div>\n <label\n htmlFor=\"tenantId\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('organizations.form.tenant', 'Tenant')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n\n <div className=\"relative\">\n <select\n id=\"tenantId\"\n value={formData.tenantId}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n tenantId: event.target.value,\n organizationId: '',\n }))\n }\n className=\"w-full appearance-none rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-10 text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n disabled={loading || tenantsLoading}\n required\n >\n <option value=\"\">\n {tr('workspaces.form.selectTenant', 'Select a tenant...')}\n </option>\n {tenants.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name} ({tenant.slug})\n </option>\n ))}\n </select>\n <Home\n size={18}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <div className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\">\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </div>\n {tenantsLoading ? (\n <div className=\"absolute right-10 top-1/2 -translate-y-1/2\">\n <Loader2 size={16} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n ) : null}\n </div>\n {errors.tenantId ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">\n {errors.tenantId}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div>\n <label\n htmlFor=\"organizationId\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('common.organization', 'Organization')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n\n {showOrganizationSelector && !organizationId ? (\n <div className=\"relative\">\n <select\n id=\"organizationId\"\n value={formData.organizationId}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n organizationId: event.target.value,\n }))\n }\n className=\"w-full appearance-none rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-10 text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n disabled={\n loading || organizationsLoading || permissionCheckLoading || !formData.tenantId\n }\n required\n >\n <option value=\"\">\n {!formData.tenantId\n ? tr('workspaces.form.selectTenantFirst', 'Select a tenant first')\n : permittedOrganizations.length === 0 &&\n !organizationsLoading &&\n !permissionCheckLoading\n ? tr(\n 'workspaces.form.noOrganizationsAvailable',\n 'No organizations available — you need workspace create permission'\n )\n : tr('workspaces.form.selectOrganization', 'Select an organization...')}\n </option>\n {permittedOrganizations.map((organization) => (\n <option key={organization.id} value={organization.id}>\n {organization.name} ({organization.slug})\n </option>\n ))}\n </select>\n <Building\n size={18}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <div className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\">\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </div>\n {organizationsLoading || permissionCheckLoading ? (\n <div className=\"absolute right-10 top-1/2 -translate-y-1/2\">\n <Loader2 size={16} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n ) : null}\n </div>\n ) : (\n <div className=\"relative\">\n <input\n id=\"organizationId\"\n type=\"text\"\n value={selectedOrganizationLabel}\n aria-label={tr('common.organization', 'Organization')}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n organizationId: event.target.value,\n }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-4 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.orgIdPlaceholder', 'Enter organization ID')}\n disabled={loading || Boolean(organizationId)}\n required\n autoComplete=\"off\"\n />\n <Building\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n </div>\n )}\n\n {(organizationId || formData.organizationId) && (\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr(\n 'workspaces.form.creatingUnderOrg',\n 'Creating workspace under the selected organization.'\n )}\n </p>\n )}\n {errors.organizationId ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">\n {errors.organizationId}\n </p>\n ) : null}\n </div>\n </>\n ) : null}\n\n <div>\n <label\n htmlFor=\"name\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('workspaces.form.name', 'Workspace Name')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={formData.name}\n aria-label={tr('workspaces.form.name', 'Workspace Name')}\n onChange={(event) =>\n setFormData((previousState) => ({ ...previousState, name: event.target.value }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-3 py-2 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.namePlaceholder', 'Enter workspace name')}\n disabled={loading}\n required\n autoComplete=\"organization-title\"\n />\n {errors.name ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.name}</p>\n ) : null}\n </div>\n\n <div>\n <label\n htmlFor=\"slug\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('common.slug', 'Slug')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"slug\"\n type=\"text\"\n value={formData.slug}\n aria-label={tr('common.slug', 'Slug')}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n slug: event.target.value.toLowerCase().replace(/\\s+/g, '-'),\n }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-3 py-2 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.slugPlaceholder', 'workspace-slug')}\n disabled={loading || Boolean(workspace)}\n required={!workspace}\n autoComplete=\"off\"\n />\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr(\n 'organizations.form.slugImmutable',\n 'Lowercase letters, numbers, and hyphens only. Cannot be changed after creation.'\n )}\n </p>\n {errors.slug ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.slug}</p>\n ) : null}\n </div>\n\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-6 py-2.5 font-medium text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[220px]\"\n >\n {loading ? <Loader2 size={16} className=\"animate-spin\" /> : null}\n {workspace\n ? tr('workspaces.form.updateSubmit', 'Update Workspace')\n : tr('workspaces.create.button', 'Create Workspace')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"rounded-lg border border-[var(--color-border-default)] bg-transparent px-6 py-2.5 font-medium text-[var(--color-text-primary)] transition-all duration-200 hover:bg-[var(--color-bg-subtle)] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkspaceForm.js","names":[],"sources":["../../../../../src/organization-management/modules/workspaces/components/WorkspaceForm.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Building, Home, Loader2 } from 'lucide-react';\nimport { ZodError } from 'zod';\nimport type { CreateWorkspaceInput, UpdateWorkspaceInput, Workspace } from '../types';\nimport { createWorkspaceSchema, updateWorkspaceSchema } from '../schemas';\nimport { useOrganizations } from '../../organizations/hooks/useOrganizations';\nimport { useOrgsWithPermission } from '../../organizations/hooks/useOrgsWithPermission';\nimport { useTenants } from '../hooks/useWorkspaces';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface WorkspaceFormProps {\n workspace?: Workspace | null;\n tenantId?: string;\n tenantName?: string;\n organizationId?: string;\n organizationName?: string;\n showOrganizationSelector?: boolean;\n onSubmit: (data: CreateWorkspaceInput | UpdateWorkspaceInput) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\nexport function WorkspaceForm({\n workspace,\n tenantId,\n organizationId,\n organizationName,\n showOrganizationSelector = false,\n onSubmit,\n onCancel,\n loading,\n}: WorkspaceFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n // react-doctor-disable-next-line react-doctor/no-event-handler\n name: workspace?.name || '',\n slug: workspace?.slug || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n tenantId: tenantId || workspace?.tenantId || '',\n // react-doctor-disable-next-line react-doctor/no-event-handler\n organizationId: organizationId || workspace?.organizationId || '',\n });\n\n const [errors, setErrors] = useState<Record<string, string>>({});\n\n const shouldLoadTenants = !workspace && showOrganizationSelector && !tenantId;\n const shouldLoadOrganizations = !workspace && showOrganizationSelector && !organizationId;\n\n const { tenants, loading: tenantsLoading } = useTenants(100, 0, shouldLoadTenants);\n\n // Show the tenant picker only when the user genuinely has more than one\n // tenant to choose between. With a single shared tenant there is nothing to\n // choose, so the picker is hidden and the tenant is auto-selected.\n const showTenantPicker = shouldLoadTenants && tenants.length > 1;\n\n // Auto-select the shared (or only) tenant when there is no explicit tenant\n // and no real choice to make, so the workspace lands in the shared tenant\n // without surfacing a picker.\n useEffect(() => {\n if (!shouldLoadTenants || formData.tenantId || tenants.length === 0) {\n return;\n }\n const sharedTenant = tenants.find((tenant) => tenant.type === 'SHARED');\n const resolved = sharedTenant?.id ?? tenants[0]?.id;\n if (resolved) {\n setFormData((previousState) =>\n previousState.tenantId ? previousState : { ...previousState, tenantId: resolved }\n );\n }\n }, [shouldLoadTenants, tenants, formData.tenantId]);\n const { organizations, loading: organizationsLoading } = useOrganizations(\n formData.tenantId ? { tenantId: formData.tenantId } : undefined,\n 100,\n 0,\n shouldLoadOrganizations\n );\n\n // Hide orgs where the user can't actually create a workspace, so the\n // dropdown only surfaces options the gateway @rbac(workspace:create) will\n // accept. Membership alone isn't enough — a Viewer of an org is a member\n // but has workspace=VIEWER (no create). The bulk checkPermissions call is\n // one round-trip regardless of how many orgs are on the list.\n const allOrgIds = useMemo(\n () => organizations.map((organization) => organization.id),\n [organizations]\n );\n const { allowedIds: workspaceCreateAllowedOrgIds, loading: permissionCheckLoading } =\n useOrgsWithPermission(allOrgIds, 'workspace', 'create');\n const filterByPermission = !workspace && showOrganizationSelector && !organizationId;\n const permittedOrganizations = useMemo(\n () =>\n filterByPermission\n ? organizations.filter((organization) => workspaceCreateAllowedOrgIds.has(organization.id))\n : organizations,\n [organizations, workspaceCreateAllowedOrgIds, filterByPermission]\n );\n\n useEffect(() => {\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (tenantId && tenantId !== formData.tenantId) {\n setFormData((previousState) => ({ ...previousState, tenantId }));\n }\n }, [formData.tenantId, tenantId]);\n\n useEffect(() => {\n // react-doctor-disable-next-line react-doctor/no-event-handler\n if (organizationId && organizationId !== formData.organizationId) {\n setFormData((previousState) => ({ ...previousState, organizationId }));\n }\n }, [formData.organizationId, organizationId]);\n\n useEffect(() => {\n if (!showOrganizationSelector || organizationId || workspace) {\n return;\n }\n\n setFormData((previousState) => {\n if (!previousState.organizationId) {\n return previousState;\n }\n\n const hasMatchingOrganization = permittedOrganizations.some(\n (organization) => organization.id === previousState.organizationId\n );\n\n if (hasMatchingOrganization) {\n return previousState;\n }\n\n return { ...previousState, organizationId: '' };\n });\n }, [organizationId, permittedOrganizations, showOrganizationSelector, workspace]);\n\n const selectedOrganizationLabel = useMemo(() => {\n if (organizationName) return organizationName;\n\n const selectedOrganization = permittedOrganizations.find(\n (organization) => organization.id === formData.organizationId\n );\n return selectedOrganization?.name ?? formData.organizationId;\n }, [formData.organizationId, organizationName, permittedOrganizations]);\n\n const validate = () => {\n try {\n const schema = workspace ? updateWorkspaceSchema(tr) : createWorkspaceSchema(tr);\n schema.parse(formData);\n setErrors({});\n return true;\n } catch (validationError) {\n if (validationError instanceof ZodError) {\n const nextErrors: Record<string, string> = {};\n validationError.issues.forEach((issue) => {\n const path = issue.path.join('.');\n nextErrors[path] = issue.message;\n });\n setErrors(nextErrors);\n }\n\n return false;\n }\n };\n\n const handleSubmit = async (event: React.FormEvent) => {\n event.preventDefault();\n\n if (!validate()) {\n return;\n }\n\n const submitData = workspace\n ? {\n name: formData.name !== workspace.name ? formData.name : undefined,\n }\n : {\n tenantId: formData.tenantId,\n organizationId: formData.organizationId,\n name: formData.name,\n slug: formData.slug,\n };\n\n await onSubmit(submitData);\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {!workspace ? (\n <>\n {/* Tenant picker — shown ONLY when the user has more than one tenant\n to choose between. For single-tenant (shared) users the tenant is\n auto-selected and no picker is rendered. */}\n {showTenantPicker ? (\n <div>\n <label\n htmlFor=\"tenantId\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('organizations.form.tenant', 'Tenant')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n\n <div className=\"relative\">\n <select\n id=\"tenantId\"\n value={formData.tenantId}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n tenantId: event.target.value,\n organizationId: '',\n }))\n }\n className=\"w-full appearance-none rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-10 text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n disabled={loading || tenantsLoading}\n required\n >\n <option value=\"\">\n {tr('workspaces.form.selectTenant', 'Select a tenant...')}\n </option>\n {tenants.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name} ({tenant.slug})\n </option>\n ))}\n </select>\n <Home\n size={18}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <div className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\">\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </div>\n {tenantsLoading ? (\n <div className=\"absolute right-10 top-1/2 -translate-y-1/2\">\n <Loader2 size={16} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n ) : null}\n </div>\n {errors.tenantId ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">\n {errors.tenantId}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div>\n <label\n htmlFor=\"organizationId\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('common.organization', 'Organization')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n\n {showOrganizationSelector && !organizationId ? (\n <div className=\"relative\">\n <select\n id=\"organizationId\"\n value={formData.organizationId}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n organizationId: event.target.value,\n }))\n }\n className=\"w-full appearance-none rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-10 text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n disabled={\n loading || organizationsLoading || permissionCheckLoading || !formData.tenantId\n }\n required\n >\n <option value=\"\">\n {!formData.tenantId\n ? tr('workspaces.form.selectTenantFirst', 'Select a tenant first')\n : permittedOrganizations.length === 0 &&\n !organizationsLoading &&\n !permissionCheckLoading\n ? tr(\n 'workspaces.form.noOrganizationsAvailable',\n 'No organizations available — you need workspace create permission'\n )\n : tr('workspaces.form.selectOrganization', 'Select an organization...')}\n </option>\n {permittedOrganizations.map((organization) => (\n <option key={organization.id} value={organization.id}>\n {organization.name} ({organization.slug})\n </option>\n ))}\n </select>\n <Building\n size={18}\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <div className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\">\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </div>\n {organizationsLoading || permissionCheckLoading ? (\n <div className=\"absolute right-10 top-1/2 -translate-y-1/2\">\n <Loader2 size={16} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n ) : null}\n </div>\n ) : (\n <div className=\"relative\">\n <input\n id=\"organizationId\"\n type=\"text\"\n value={selectedOrganizationLabel}\n aria-label={tr('common.organization', 'Organization')}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n organizationId: event.target.value,\n }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] py-2 pl-10 pr-4 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.orgIdPlaceholder', 'Enter organization ID')}\n disabled={loading || Boolean(organizationId)}\n required\n autoComplete=\"off\"\n />\n <Building\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n </div>\n )}\n\n {(organizationId || formData.organizationId) && (\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr(\n 'workspaces.form.creatingUnderOrg',\n 'Creating workspace under the selected organization.'\n )}\n </p>\n )}\n {errors.organizationId ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">\n {errors.organizationId}\n </p>\n ) : null}\n </div>\n </>\n ) : null}\n\n <div>\n <label\n htmlFor=\"name\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('workspaces.form.name', 'Workspace Name')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={formData.name}\n aria-label={tr('workspaces.form.name', 'Workspace Name')}\n onChange={(event) =>\n setFormData((previousState) => ({ ...previousState, name: event.target.value }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-3 py-2 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.namePlaceholder', 'Enter workspace name')}\n disabled={loading}\n required\n autoComplete=\"organization-title\"\n />\n {errors.name ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.name}</p>\n ) : null}\n </div>\n\n <div>\n <label\n htmlFor=\"slug\"\n className=\"mb-1 block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr('common.slug', 'Slug')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <input\n id=\"slug\"\n type=\"text\"\n value={formData.slug}\n aria-label={tr('common.slug', 'Slug')}\n onChange={(event) =>\n setFormData((previousState) => ({\n ...previousState,\n slug: event.target.value.toLowerCase().replace(/\\s+/g, '-'),\n }))\n }\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-3 py-2 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:bg-[var(--color-bg-muted)]\"\n placeholder={tr('workspaces.form.slugPlaceholder', 'workspace-slug')}\n disabled={loading || Boolean(workspace)}\n required={!workspace}\n autoComplete=\"off\"\n />\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr(\n 'organizations.form.slugImmutable',\n 'Lowercase letters, numbers, and hyphens only. Cannot be changed after creation.'\n )}\n </p>\n {errors.slug ? (\n <p className=\"mt-1 text-sm text-[var(--color-status-error-text)]\">{errors.slug}</p>\n ) : null}\n </div>\n\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-6 py-2.5 font-medium text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[220px]\"\n >\n {loading ? <Loader2 size={16} className=\"animate-spin\" /> : null}\n {workspace\n ? tr('workspaces.form.updateSubmit', 'Update Workspace')\n : tr('workspaces.create.button', 'Create Workspace')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"rounded-lg border border-[var(--color-border-default)] bg-transparent px-6 py-2.5 font-medium text-[var(--color-text-primary)] transition-all duration-200 hover:bg-[var(--color-bg-subtle)] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { WorkspaceStatus as e } from "../../../shared/types/graphql.js";
|
|
2
2
|
import { nativeEnumType as t, objectType as n, recordType as r, stringType as i, unknownType as a } from "../../../../node_modules/zod/v3/types.js";
|
|
3
3
|
//#region src/organization-management/modules/workspaces/schemas/index.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
4
|
+
function o(e) {
|
|
5
|
+
return {
|
|
6
|
+
name: i().min(2, e("workspaces.validation.nameMin", "Name must be at least 2 characters")).max(100, e("workspaces.validation.nameMax", "Name must not exceed 100 characters")).trim(),
|
|
7
|
+
slug: i().min(2, e("workspaces.validation.slugMin", "Slug must be at least 2 characters")).max(50, e("workspaces.validation.slugMax", "Slug must not exceed 50 characters")).regex(/^[a-z0-9-]+$/, e("workspaces.validation.slugPattern", "Slug must contain only lowercase letters, numbers, and hyphens")).trim(),
|
|
8
|
+
type: i().optional(),
|
|
9
|
+
metadata: r(i(), a()).optional()
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function s(r) {
|
|
13
|
+
return n({
|
|
14
|
+
tenantId: i().min(1, r("workspaces.validation.tenantRequired", "Tenant is required")),
|
|
15
|
+
organizationId: i().min(1, r("workspaces.validation.organizationRequired", "Organization is required")),
|
|
16
|
+
...o(r),
|
|
17
|
+
status: t(e).optional()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function c(e) {
|
|
21
|
+
let t = o(e);
|
|
22
|
+
return n({
|
|
23
|
+
name: t.name.optional(),
|
|
24
|
+
type: t.type,
|
|
25
|
+
metadata: t.metadata
|
|
26
|
+
});
|
|
27
|
+
}
|
|
19
28
|
//#endregion
|
|
20
29
|
export { s as createWorkspaceSchema, c as updateWorkspaceSchema };
|
|
21
30
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../src/organization-management/modules/workspaces/schemas/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { WorkspaceStatus } from '../types';\n\n/**\n * Workspace Validation Schemas\n * Zod schemas for validating workspace form inputs\n *\n * Schema Changes:\n * - Removed description, iconUrl, visibility fields (not in backend schema)\n * - Added type field\n */\n\
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../src/organization-management/modules/workspaces/schemas/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { WorkspaceStatus } from '../types';\nimport type { TranslationFn } from '../../../shared/hooks/useTr';\n\n/**\n * Workspace Validation Schemas\n * Zod schemas for validating workspace form inputs.\n *\n * Exported as factory functions so validation messages can be translated\n * via the same `tr()` helper used by components.\n *\n * Schema Changes:\n * - Removed description, iconUrl, visibility fields (not in backend schema)\n * - Added type field\n */\n\nfunction baseWorkspaceSchema(tr: TranslationFn) {\n return {\n name: z\n .string()\n .min(2, tr('workspaces.validation.nameMin', 'Name must be at least 2 characters'))\n .max(100, tr('workspaces.validation.nameMax', 'Name must not exceed 100 characters'))\n .trim(),\n slug: z\n .string()\n .min(2, tr('workspaces.validation.slugMin', 'Slug must be at least 2 characters'))\n .max(50, tr('workspaces.validation.slugMax', 'Slug must not exceed 50 characters'))\n .regex(\n /^[a-z0-9-]+$/,\n tr(\n 'workspaces.validation.slugPattern',\n 'Slug must contain only lowercase letters, numbers, and hyphens'\n )\n )\n .trim(),\n type: z.string().optional(),\n metadata: z.record(z.string(), z.unknown()).optional(),\n };\n}\n\n// Create Workspace Schema\nexport function createWorkspaceSchema(tr: TranslationFn) {\n return z.object({\n tenantId: z.string().min(1, tr('workspaces.validation.tenantRequired', 'Tenant is required')),\n organizationId: z\n .string()\n .min(1, tr('workspaces.validation.organizationRequired', 'Organization is required')),\n ...baseWorkspaceSchema(tr),\n status: z.nativeEnum(WorkspaceStatus).optional(),\n });\n}\n\n// Update Workspace Schema\nexport function updateWorkspaceSchema(tr: TranslationFn) {\n const base = baseWorkspaceSchema(tr);\n return z.object({\n name: base.name.optional(),\n type: base.type,\n metadata: base.metadata,\n });\n}\n\n// Type exports\nexport type CreateWorkspaceFormData = z.infer<ReturnType<typeof createWorkspaceSchema>>;\nexport type UpdateWorkspaceFormData = z.infer<ReturnType<typeof updateWorkspaceSchema>>;\n"],"mappings":";;;AAgBA,SAAS,EAAoB,GAAmB;AAC9C,QAAO;EACL,MAAM,GACK,CACR,IAAI,GAAG,EAAG,iCAAiC,qCAAqC,CAAC,CACjF,IAAI,KAAK,EAAG,iCAAiC,sCAAsC,CAAC,CACpF,MAAM;EACT,MAAM,GACK,CACR,IAAI,GAAG,EAAG,iCAAiC,qCAAqC,CAAC,CACjF,IAAI,IAAI,EAAG,iCAAiC,qCAAqC,CAAC,CAClF,MACC,gBACA,EACE,qCACA,iEACD,CACF,CACA,MAAM;EACT,MAAM,GAAU,CAAC,UAAU;EAC3B,UAAU,EAAS,GAAU,EAAE,GAAW,CAAC,CAAC,UAAU;EACvD;;AAIH,SAAgB,EAAsB,GAAmB;AACvD,QAAO,EAAS;EACd,UAAU,GAAU,CAAC,IAAI,GAAG,EAAG,wCAAwC,qBAAqB,CAAC;EAC7F,gBAAgB,GACL,CACR,IAAI,GAAG,EAAG,8CAA8C,2BAA2B,CAAC;EACvF,GAAG,EAAoB,EAAG;EAC1B,QAAQ,EAAa,EAAgB,CAAC,UAAU;EACjD,CAAC;;AAIJ,SAAgB,EAAsB,GAAmB;CACvD,IAAM,IAAO,EAAoB,EAAG;AACpC,QAAO,EAAS;EACd,MAAM,EAAK,KAAK,UAAU;EAC1B,MAAM,EAAK;EACX,UAAU,EAAK;EAChB,CAAC"}
|
|
@@ -29,7 +29,7 @@ function a({ value: a, onChange: o, placeholder: s, className: c = "", label: l,
|
|
|
29
29
|
type: "button",
|
|
30
30
|
onClick: () => o(""),
|
|
31
31
|
className: "\n absolute right-3 top-1/2 -translate-y-1/2\n p-1 rounded-md\n text-[var(--color-text-muted)]\n hover:text-[var(--color-text-primary)]\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n ",
|
|
32
|
-
"aria-label": "Clear search",
|
|
32
|
+
"aria-label": d("common.clearSearch", "Clear search"),
|
|
33
33
|
children: /* @__PURE__ */ r(n, { size: 16 })
|
|
34
34
|
})
|
|
35
35
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchInput.js","names":[],"sources":["../../../../../src/organization-management/shared/components/input/SearchInput.tsx"],"sourcesContent":["import { Search, X } from 'lucide-react';\nimport { useTr } from '../../hooks/useTr';\n\ninterface SearchInputProps {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n className?: string;\n label?: string;\n inputId?: string;\n}\n\n/**\n * SearchInput\n * Reusable search input component with clear button\n */\nexport function SearchInput({\n value,\n onChange,\n placeholder,\n className = '',\n label,\n inputId,\n}: SearchInputProps) {\n const tr = useTr();\n const resolvedPlaceholder = placeholder ?? tr('common.search.placeholder', 'Search...');\n return (\n <div className={`relative ${className}`}>\n {label ? (\n <label htmlFor={inputId} className=\"sr-only\">\n {label}\n </label>\n ) : null}\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <input\n id={inputId}\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={resolvedPlaceholder}\n aria-label={label ?? resolvedPlaceholder}\n className=\"\n w-full pl-[3.25rem] pr-10 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n transition-all duration-200\n \"\n />\n {value && (\n <button\n type=\"button\"\n onClick={() => onChange('')}\n className=\"\n absolute right-3 top-1/2 -translate-y-1/2\n p-1 rounded-md\n text-[var(--color-text-muted)]\n hover:text-[var(--color-text-primary)]\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n \"\n aria-label
|
|
1
|
+
{"version":3,"file":"SearchInput.js","names":[],"sources":["../../../../../src/organization-management/shared/components/input/SearchInput.tsx"],"sourcesContent":["import { Search, X } from 'lucide-react';\nimport { useTr } from '../../hooks/useTr';\n\ninterface SearchInputProps {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n className?: string;\n label?: string;\n inputId?: string;\n}\n\n/**\n * SearchInput\n * Reusable search input component with clear button\n */\nexport function SearchInput({\n value,\n onChange,\n placeholder,\n className = '',\n label,\n inputId,\n}: SearchInputProps) {\n const tr = useTr();\n const resolvedPlaceholder = placeholder ?? tr('common.search.placeholder', 'Search...');\n return (\n <div className={`relative ${className}`}>\n {label ? (\n <label htmlFor={inputId} className=\"sr-only\">\n {label}\n </label>\n ) : null}\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]\"\n />\n <input\n id={inputId}\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={resolvedPlaceholder}\n aria-label={label ?? resolvedPlaceholder}\n className=\"\n w-full pl-[3.25rem] pr-10 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n transition-all duration-200\n \"\n />\n {value && (\n <button\n type=\"button\"\n onClick={() => onChange('')}\n className=\"\n absolute right-3 top-1/2 -translate-y-1/2\n p-1 rounded-md\n text-[var(--color-text-muted)]\n hover:text-[var(--color-text-primary)]\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n \"\n aria-label={tr('common.clearSearch', 'Clear search')}\n >\n <X size={16} />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;AAgBA,SAAgB,EAAY,EAC1B,UACA,aACA,gBACA,eAAY,IACZ,UACA,cACmB;CACnB,IAAM,IAAK,GAAO,EACZ,IAAsB,KAAe,EAAG,6BAA6B,YAAY;AACvF,QACE,kBAAC,OAAD;EAAK,WAAW,YAAY;YAA5B;GACG,IACC,kBAAC,SAAD;IAAO,SAAS;IAAS,WAAU;cAChC;IACK,CAAA,GACN;GACJ,kBAAC,GAAD;IACE,MAAM;IACN,WAAU;IACV,CAAA;GACF,kBAAC,SAAD;IACE,IAAI;IACJ,MAAK;IACE;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;IACzC,aAAa;IACb,cAAY,KAAS;IACrB,WAAU;IAUV,CAAA;GACD,KACC,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,GAAG;IAC3B,WAAU;IASV,cAAY,EAAG,sBAAsB,eAAe;cAEpD,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;IACR,CAAA;GAEP"}
|
|
@@ -3,9 +3,9 @@ import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProv
|
|
|
3
3
|
//#region src/organization-management/shared/hooks/useTr.ts
|
|
4
4
|
function n() {
|
|
5
5
|
let { t: n } = t();
|
|
6
|
-
return e((e, t) => {
|
|
7
|
-
let
|
|
8
|
-
return r
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
9
|
}, [n]);
|
|
10
10
|
}
|
|
11
11
|
//#endregion
|