@burdenoff/microfe-organization 2026.616.1 → 2026.624.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 +4 -4
- package/dist/organization-management/AdminOrganizationRoot.js.map +1 -1
- package/dist/organization-management/UserOrganizationRoot.js +4 -4
- package/dist/organization-management/UserOrganizationRoot.js.map +1 -1
- package/dist/organization-management/modules/members/components/BulkInviteForm.js +157 -176
- package/dist/organization-management/modules/members/components/BulkInviteForm.js.map +1 -1
- package/dist/organization-management/modules/members/components/InviteMemberForm.js +2 -2
- package/dist/organization-management/modules/members/components/InviteMemberForm.js.map +1 -1
- package/dist/organization-management/modules/members/components/MyInviteCard.js +2 -2
- package/dist/organization-management/modules/members/components/MyInviteCard.js.map +1 -1
- package/dist/organization-management/modules/members/components/PendingInvitesList.js +78 -101
- package/dist/organization-management/modules/members/components/PendingInvitesList.js.map +1 -1
- package/dist/organization-management/modules/members/pages/InvitePeoplePage.js +1 -1
- package/dist/organization-management/modules/members/pages/InvitePeoplePage.js.map +1 -1
- package/dist/organization-management/modules/members/pages/MemberDetailPage.js +2 -2
- package/dist/organization-management/modules/members/pages/MemberDetailPage.js.map +1 -1
- package/dist/organization-management/modules/members/pages/MembersListPage.js +3 -3
- package/dist/organization-management/modules/members/pages/MembersListPage.js.map +1 -1
- package/dist/organization-management/modules/members/pages/MyInvitesPage.js +2 -2
- package/dist/organization-management/modules/members/pages/MyInvitesPage.js.map +1 -1
- package/dist/organization-management/modules/members/pages/OrganizationInviteAcceptancePage.js +5 -5
- package/dist/organization-management/modules/members/pages/OrganizationInviteAcceptancePage.js.map +1 -1
- package/dist/organization-management/modules/organizations/components/OrganizationForm.js +2 -2
- package/dist/organization-management/modules/organizations/components/OrganizationForm.js.map +1 -1
- package/dist/organization-management/modules/organizations/pages/OrganizationCreatePage.js +2 -2
- package/dist/organization-management/modules/organizations/pages/OrganizationCreatePage.js.map +1 -1
- package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js +4 -4
- package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js.map +1 -1
- package/dist/organization-management/modules/organizations/pages/OrganizationEditPage.js +2 -2
- package/dist/organization-management/modules/organizations/pages/OrganizationEditPage.js.map +1 -1
- package/dist/organization-management/modules/organizations/pages/OrganizationsListPage.js +5 -5
- package/dist/organization-management/modules/organizations/pages/OrganizationsListPage.js.map +1 -1
- package/dist/organization-management/modules/workspaces/components/WorkspaceCard.js +3 -3
- package/dist/organization-management/modules/workspaces/components/WorkspaceCard.js.map +1 -1
- package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js +2 -2
- package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js.map +1 -1
- package/dist/organization-management/modules/workspaces/pages/WorkspaceCreatePage.js +2 -2
- package/dist/organization-management/modules/workspaces/pages/WorkspaceCreatePage.js.map +1 -1
- package/dist/organization-management/modules/workspaces/pages/WorkspaceDetailPage.js +215 -285
- package/dist/organization-management/modules/workspaces/pages/WorkspaceDetailPage.js.map +1 -1
- package/dist/organization-management/modules/workspaces/pages/WorkspaceEditPage.js +2 -2
- package/dist/organization-management/modules/workspaces/pages/WorkspaceEditPage.js.map +1 -1
- package/dist/organization-management/modules/workspaces/pages/WorkspacesListPage.js +4 -4
- package/dist/organization-management/modules/workspaces/pages/WorkspacesListPage.js.map +1 -1
- package/dist/organization-management/shared/components/feedback/EmptyState.js +2 -2
- package/dist/organization-management/shared/components/feedback/EmptyState.js.map +1 -1
- package/dist/organization-management/shared/components/feedback/ErrorState.js +2 -2
- package/dist/organization-management/shared/components/feedback/ErrorState.js.map +1 -1
- package/dist/organization-management/shared/components/feedback/LoadingState.js +1 -1
- package/dist/organization-management/shared/components/feedback/LoadingState.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/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BulkInviteForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/BulkInviteForm.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport { CheckCircle2, Download, Loader2, Send, Upload, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport {\n type BulkInviteRow,\n downloadTemplate,\n formatInviteError,\n parseBulkInviteCsv,\n} from '../utils/bulkInviteCsv';\n\ninterface BulkInviteFormProps {\n organizationId: string;\n inviteMember: (input: {\n organizationId: string;\n email: string;\n message?: string;\n }) => Promise<unknown>;\n onCancel: () => void;\n loading?: boolean;\n}\n\ntype RowStatus =\n | { kind: 'idle' }\n | { kind: 'sending' }\n | { kind: 'invited' }\n | { kind: 'failed'; message: string };\n\ninterface PreviewRow extends BulkInviteRow {\n status: RowStatus;\n}\n\n/**\n * Bulk-invite via CSV upload. Sequentially fires the per-row invite\n * mutation (concurrency 1) so we don't hammer the gateway and so per-row\n * errors are deterministic. Reuses the same mutation as single-invite,\n * which keeps backend behaviour (refetch, OTel events) identical.\n */\nexport function BulkInviteForm({\n organizationId,\n inviteMember,\n onCancel,\n loading,\n}: BulkInviteFormProps) {\n const tr = useTr();\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [rows, setRows] = useState<PreviewRow[]>([]);\n const [fileName, setFileName] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [dragActive, setDragActive] = useState(false);\n\n const handleFile = useCallback(\n (file: File) => {\n const reader = new FileReader();\n reader.onload = () => {\n const text = typeof reader.result === 'string' ? reader.result : '';\n const parsed = parseBulkInviteCsv(text);\n setRows(parsed.map((r) => ({ ...r, status: { kind: 'idle' } })));\n setFileName(file.name);\n };\n reader.onerror = () => {\n toast.error(tr('members.bulkInvite.readError', 'Failed to read CSV file.'));\n };\n reader.readAsText(file);\n },\n [tr]\n );\n\n const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (file) handleFile(file);\n // Reset so re-uploading the same file fires onChange again\n e.target.value = '';\n };\n\n const onDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setDragActive(false);\n const file = e.dataTransfer.files?.[0];\n if (file) handleFile(file);\n };\n\n const summary = useMemo(() => {\n const total = rows.length;\n const withErrors = rows.filter((r) => r.errors.length > 0).length;\n const sendable = rows.filter((r) => r.errors.length === 0).length;\n const invited = rows.filter((r) => r.status.kind === 'invited').length;\n const failed = rows.filter((r) => r.status.kind === 'failed').length;\n return { total, withErrors, sendable, invited, failed };\n }, [rows]);\n\n const handleSend = async () => {\n if (summary.sendable === 0 || isSubmitting) return;\n setIsSubmitting(true);\n\n const sendableIndices = rows\n .map((r, idx) => ({ r, idx }))\n .filter(({ r }) => r.errors.length === 0 && r.status.kind !== 'invited');\n\n for (const { idx } of sendableIndices) {\n setRows((prev) =>\n prev.map((row, i) => (i === idx ? { ...row, status: { kind: 'sending' } } : row))\n );\n const row = rows[idx];\n try {\n await inviteMember({\n organizationId,\n email: row.email,\n message: row.message,\n });\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'invited' } } : r))\n );\n } catch (err) {\n const message = formatInviteError(err, tr);\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'failed', message } } : r))\n );\n }\n }\n\n setIsSubmitting(false);\n setRows((prev) => {\n const invited = prev.filter((r) => r.status.kind === 'invited').length;\n const failed = prev.filter((r) => r.status.kind === 'failed').length;\n toast.success(\n `${tr('members.bulkInvite.summaryInvited', 'Invited')} ${invited} ${tr('members.bulkInvite.summaryOf', 'of')} ${prev.length}. ${failed} ${tr('members.bulkInvite.summaryFailed', 'failed')}.`\n );\n return prev;\n });\n };\n\n const hasRows = rows.length > 0;\n const canSubmit = summary.sendable > 0 && !isSubmitting && !loading;\n\n return (\n <div className=\"space-y-5\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-[var(--color-text-muted)]\">\n {tr(\n 'members.bulkInvite.description',\n 'Upload a CSV with one row per person. Required column: email. Optional: role, message.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={downloadTemplate}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <Download size={16} aria-hidden=\"true\" />\n {tr('members.bulkInvite.downloadTemplate', 'Download template')}\n </button>\n </div>\n\n <div\n onDragOver={(e) => {\n e.preventDefault();\n setDragActive(true);\n }}\n onDragLeave={() => setDragActive(false)}\n onDrop={onDrop}\n className={`flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed p-8 transition-colors ${\n dragActive\n ? 'border-[var(--color-action-primary-bg)] bg-[var(--color-status-info-bgSubtle)]'\n : 'border-[var(--color-border-default)] bg-[var(--color-bg-canvas)]'\n }`}\n >\n <Upload size={28} className=\"text-[var(--color-text-secondary)]\" aria-hidden=\"true\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary)]\">\n {fileName\n ? fileName\n : tr('members.bulkInvite.dropHere', 'Drop a CSV file here or click to browse')}\n </p>\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.csvOnly', 'CSV files only. Max 1000 rows recommended.')}\n </p>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={onFileChange}\n className=\"hidden\"\n aria-label={tr('members.bulkInvite.fileInputLabel', 'Choose CSV file')}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-4 py-2 text-sm text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90\"\n >\n {tr('members.bulkInvite.chooseFile', 'Choose file')}\n </button>\n </div>\n\n {hasRows && (\n <>\n <div className=\"flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)]\">\n <span>\n {tr('members.bulkInvite.statsTotal', 'Total rows')}: {summary.total}\n </span>\n <span>\n {tr('members.bulkInvite.statsValid', 'Ready')}: {summary.sendable}\n </span>\n {summary.withErrors > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsErrors', 'With errors')}: {summary.withErrors}\n </span>\n )}\n {summary.invited > 0 && (\n <span className=\"text-[var(--color-status-success-text)]\">\n {tr('members.bulkInvite.statsInvited', 'Invited')}: {summary.invited}\n </span>\n )}\n {summary.failed > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsFailed', 'Failed')}: {summary.failed}\n </span>\n )}\n </div>\n\n <div className=\"overflow-hidden rounded-xl border border-[var(--color-border-default)]\">\n <div className=\"overflow-x-auto\">\n <table className=\"min-w-full\">\n <thead className=\"bg-[var(--color-bg-subtle)]\">\n <tr>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n #\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colEmail', 'Email')}\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colRole', 'Role')}\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colStatus', 'Status')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-[var(--color-border-default)]\">\n {rows.map((row) => {\n const hasParseErrors = row.errors.length > 0;\n return (\n <tr\n key={`${row.rowNumber}-${row.email}`}\n className=\"bg-[var(--color-bg-surface)]\"\n >\n <td className=\"px-3 py-2 text-xs text-[var(--color-text-secondary)]\">\n {row.rowNumber}\n </td>\n <td className=\"px-3 py-2 text-sm text-[var(--color-text-primary)]\">\n {row.email || (\n <span className=\"italic text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.empty', '(empty)')}\n </span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-[var(--color-text-secondary)]\">\n {row.role || (\n <span className=\"text-[var(--color-text-muted)]\">MEMBER</span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs\">\n {hasParseErrors ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.errors.join(' ')}\n </span>\n ) : row.status.kind === 'sending' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-text-secondary)]\">\n <Loader2 size={14} className=\"animate-spin\" aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusSending', 'Sending...')}\n </span>\n ) : row.status.kind === 'invited' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-success-text)]\">\n <CheckCircle2 size={14} aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusInvited', 'Invited')}\n </span>\n ) : row.status.kind === 'failed' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.status.message}\n </span>\n ) : (\n <span className=\"text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.statusReady', 'Ready')}\n </span>\n )}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </div>\n </>\n )}\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=\"button\"\n onClick={handleSend}\n disabled={!canSubmit}\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 hover:scale-[1.02] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[200px]\"\n >\n {isSubmitting ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Send size={16} aria-hidden=\"true\" />\n )}\n {tr('members.bulkInvite.send', 'Send')} {summary.sendable}{' '}\n {tr('members.bulkInvite.sendSuffix', 'invites')}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={isSubmitting}\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)] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BulkInviteForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/BulkInviteForm.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport { CheckCircle2, Download, Loader2, Send, Upload, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { ResponsiveTable, type ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport {\n type BulkInviteRow,\n downloadTemplate,\n formatInviteError,\n parseBulkInviteCsv,\n} from '../utils/bulkInviteCsv';\n\ninterface BulkInviteFormProps {\n organizationId: string;\n inviteMember: (input: {\n organizationId: string;\n email: string;\n message?: string;\n }) => Promise<unknown>;\n onCancel: () => void;\n loading?: boolean;\n}\n\ntype RowStatus =\n | { kind: 'idle' }\n | { kind: 'sending' }\n | { kind: 'invited' }\n | { kind: 'failed'; message: string };\n\ninterface PreviewRow extends BulkInviteRow {\n status: RowStatus;\n}\n\n/**\n * Bulk-invite via CSV upload. Sequentially fires the per-row invite\n * mutation (concurrency 1) so we don't hammer the gateway and so per-row\n * errors are deterministic. Reuses the same mutation as single-invite,\n * which keeps backend behaviour (refetch, OTel events) identical.\n */\nexport function BulkInviteForm({\n organizationId,\n inviteMember,\n onCancel,\n loading,\n}: BulkInviteFormProps) {\n const tr = useTr();\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [rows, setRows] = useState<PreviewRow[]>([]);\n const [fileName, setFileName] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [dragActive, setDragActive] = useState(false);\n\n const handleFile = useCallback(\n (file: File) => {\n const reader = new FileReader();\n reader.onload = () => {\n const text = typeof reader.result === 'string' ? reader.result : '';\n const parsed = parseBulkInviteCsv(text);\n setRows(parsed.map((r) => ({ ...r, status: { kind: 'idle' } })));\n setFileName(file.name);\n };\n reader.onerror = () => {\n toast.error(tr('members.bulkInvite.readError', 'Failed to read CSV file.'));\n };\n reader.readAsText(file);\n },\n [tr]\n );\n\n const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (file) handleFile(file);\n // Reset so re-uploading the same file fires onChange again\n e.target.value = '';\n };\n\n const onDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setDragActive(false);\n const file = e.dataTransfer.files?.[0];\n if (file) handleFile(file);\n };\n\n const summary = useMemo(() => {\n const total = rows.length;\n const withErrors = rows.filter((r) => r.errors.length > 0).length;\n const sendable = rows.filter((r) => r.errors.length === 0).length;\n const invited = rows.filter((r) => r.status.kind === 'invited').length;\n const failed = rows.filter((r) => r.status.kind === 'failed').length;\n return { total, withErrors, sendable, invited, failed };\n }, [rows]);\n\n const handleSend = async () => {\n if (summary.sendable === 0 || isSubmitting) return;\n setIsSubmitting(true);\n\n const sendableIndices = rows\n .map((r, idx) => ({ r, idx }))\n .filter(({ r }) => r.errors.length === 0 && r.status.kind !== 'invited');\n\n for (const { idx } of sendableIndices) {\n setRows((prev) =>\n prev.map((row, i) => (i === idx ? { ...row, status: { kind: 'sending' } } : row))\n );\n const row = rows[idx];\n try {\n await inviteMember({\n organizationId,\n email: row.email,\n message: row.message,\n });\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'invited' } } : r))\n );\n } catch (err) {\n const message = formatInviteError(err, tr);\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'failed', message } } : r))\n );\n }\n }\n\n setIsSubmitting(false);\n setRows((prev) => {\n const invited = prev.filter((r) => r.status.kind === 'invited').length;\n const failed = prev.filter((r) => r.status.kind === 'failed').length;\n toast.success(\n `${tr('members.bulkInvite.summaryInvited', 'Invited')} ${invited} ${tr('members.bulkInvite.summaryOf', 'of')} ${prev.length}. ${failed} ${tr('members.bulkInvite.summaryFailed', 'failed')}.`\n );\n return prev;\n });\n };\n\n const hasRows = rows.length > 0;\n const canSubmit = summary.sendable > 0 && !isSubmitting && !loading;\n\n const renderRowStatus = (row: PreviewRow) => {\n const hasParseErrors = row.errors.length > 0;\n if (hasParseErrors) {\n return (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.errors.join(' ')}\n </span>\n );\n }\n if (row.status.kind === 'sending') {\n return (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-text-secondary)]\">\n <Loader2 size={14} className=\"animate-spin\" aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusSending', 'Sending...')}\n </span>\n );\n }\n if (row.status.kind === 'invited') {\n return (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-success-text)]\">\n <CheckCircle2 size={14} aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusInvited', 'Invited')}\n </span>\n );\n }\n if (row.status.kind === 'failed') {\n return (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.status.message}\n </span>\n );\n }\n return (\n <span className=\"text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.statusReady', 'Ready')}\n </span>\n );\n };\n\n const previewColumns: ResponsiveTableColumn<PreviewRow>[] = [\n {\n key: 'rowNumber',\n header: '#',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"text-xs tabular-nums text-[var(--color-text-secondary)]\">\n {row.rowNumber}\n </span>\n ),\n },\n {\n key: 'email',\n header: tr('members.bulkInvite.colEmail', 'Email'),\n priority: 'primary',\n cell: (row) =>\n row.email || (\n <span className=\"italic text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.empty', '(empty)')}\n </span>\n ),\n },\n {\n key: 'role',\n header: tr('members.bulkInvite.colRole', 'Role'),\n priority: 'secondary',\n cell: (row) =>\n row.role || <span className=\"text-[var(--color-text-muted)]\">MEMBER</span>,\n },\n {\n key: 'status',\n header: tr('members.bulkInvite.colStatus', 'Status'),\n priority: 'secondary',\n cell: (row) => renderRowStatus(row),\n },\n ];\n\n return (\n <div className=\"space-y-5\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-[var(--color-text-muted)]\">\n {tr(\n 'members.bulkInvite.description',\n 'Upload a CSV with one row per person. Required column: email. Optional: role, message.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={downloadTemplate}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <Download size={16} aria-hidden=\"true\" />\n {tr('members.bulkInvite.downloadTemplate', 'Download template')}\n </button>\n </div>\n\n <div\n onDragOver={(e) => {\n e.preventDefault();\n setDragActive(true);\n }}\n onDragLeave={() => setDragActive(false)}\n onDrop={onDrop}\n className={`flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed p-8 transition-colors ${\n dragActive\n ? 'border-[var(--color-action-primary-bg)] bg-[var(--color-status-info-bgSubtle)]'\n : 'border-[var(--color-border-default)] bg-[var(--color-bg-canvas)]'\n }`}\n >\n <Upload size={28} className=\"text-[var(--color-text-secondary)]\" aria-hidden=\"true\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary)]\">\n {fileName\n ? fileName\n : tr('members.bulkInvite.dropHere', 'Drop a CSV file here or click to browse')}\n </p>\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.csvOnly', 'CSV files only. Max 1000 rows recommended.')}\n </p>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={onFileChange}\n className=\"hidden\"\n aria-label={tr('members.bulkInvite.fileInputLabel', 'Choose CSV file')}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-4 py-2 text-sm text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90\"\n >\n {tr('members.bulkInvite.chooseFile', 'Choose file')}\n </button>\n </div>\n\n {hasRows && (\n <>\n <div className=\"flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)]\">\n <span>\n {tr('members.bulkInvite.statsTotal', 'Total rows')}: {summary.total}\n </span>\n <span>\n {tr('members.bulkInvite.statsValid', 'Ready')}: {summary.sendable}\n </span>\n {summary.withErrors > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsErrors', 'With errors')}: {summary.withErrors}\n </span>\n )}\n {summary.invited > 0 && (\n <span className=\"text-[var(--color-status-success-text)]\">\n {tr('members.bulkInvite.statsInvited', 'Invited')}: {summary.invited}\n </span>\n )}\n {summary.failed > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsFailed', 'Failed')}: {summary.failed}\n </span>\n )}\n </div>\n\n <div className=\"rounded-xl border border-[var(--color-border-default)] p-2\">\n <ResponsiveTable\n columns={previewColumns}\n data={rows}\n rowKey={(row) => `${row.rowNumber}-${row.email}`}\n />\n </div>\n </>\n )}\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=\"button\"\n onClick={handleSend}\n disabled={!canSubmit}\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-[200px]\"\n >\n {isSubmitting ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Send size={16} aria-hidden=\"true\" />\n )}\n {tr('members.bulkInvite.send', 'Send')} {summary.sendable}{' '}\n {tr('members.bulkInvite.sendSuffix', 'invites')}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={isSubmitting}\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 </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -99,7 +99,7 @@ function s({ organizationId: s, onSubmit: c, onCancel: l, loading: u }) {
|
|
|
99
99
|
children: [/* @__PURE__ */ o("button", {
|
|
100
100
|
type: "submit",
|
|
101
101
|
disabled: u,
|
|
102
|
-
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
|
|
102
|
+
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-[180px]\n ",
|
|
103
103
|
children: [u ? /* @__PURE__ */ a(n, {
|
|
104
104
|
size: 16,
|
|
105
105
|
className: "animate-spin"
|
|
@@ -108,7 +108,7 @@ function s({ organizationId: s, onSubmit: c, onCancel: l, loading: u }) {
|
|
|
108
108
|
type: "button",
|
|
109
109
|
onClick: l,
|
|
110
110
|
disabled: u,
|
|
111
|
-
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
|
|
111
|
+
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 ",
|
|
112
112
|
children: d("common.cancel", "Cancel")
|
|
113
113
|
})]
|
|
114
114
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InviteMemberForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/InviteMemberForm.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Mail, Send } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface InviteMemberFormProps {\n organizationId: string;\n onSubmit: (data: { organizationId: string; email: string; message?: string }) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\n/**\n * Invite Member Form\n * Email-based form for inviting users to an organization.\n * Role defaults to MEMBER on the backend — admin can change it after the user joins.\n */\nexport function InviteMemberForm({\n organizationId,\n onSubmit,\n onCancel,\n loading,\n}: InviteMemberFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n email: '',\n message: '',\n });\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await onSubmit({\n organizationId,\n email: formData.email,\n message: formData.message || undefined,\n });\n setFormData({ email: '', message: '' });\n } catch {\n // Error is surfaced by the parent page via the mutation hook's error state\n }\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Email */}\n <div>\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.emailLabel', 'Email Address')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n <Mail size={16} className=\"text-[var(--color-text-muted)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => setFormData({ ...formData, email: e.target.value })}\n aria-label={tr('members.invite.emailLabel', 'Email Address')}\n className=\"\n w-full pl-10 pr-4 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('members.invite.emailPlaceholder', 'user@example.com')}\n disabled={loading}\n required\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-muted)] mt-1\">\n {tr(\n 'members.invite.emailHelp',\n 'The user will be invited as a Member. You can change their role after they join.'\n )}\n </p>\n </div>\n\n {/* Message (optional) */}\n <div>\n <label\n htmlFor=\"message\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.messageLabel', 'Personal Message')}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({tr('common.optional', 'optional')})\n </span>\n </label>\n <textarea\n id=\"message\"\n value={formData.message}\n onChange={(e) => setFormData({ ...formData, message: e.target.value })}\n aria-label={tr('members.invite.messageLabel', 'Personal Message')}\n className=\"\n w-full px-4 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 resize-none\n \"\n placeholder={tr(\n 'members.invite.messagePlaceholder',\n 'Add a personal message to the invite...'\n )}\n rows={3}\n maxLength={500}\n disabled={loading}\n />\n </div>\n\n {/* 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
|
|
1
|
+
{"version":3,"file":"InviteMemberForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/InviteMemberForm.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Mail, Send } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface InviteMemberFormProps {\n organizationId: string;\n onSubmit: (data: { organizationId: string; email: string; message?: string }) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\n/**\n * Invite Member Form\n * Email-based form for inviting users to an organization.\n * Role defaults to MEMBER on the backend — admin can change it after the user joins.\n */\nexport function InviteMemberForm({\n organizationId,\n onSubmit,\n onCancel,\n loading,\n}: InviteMemberFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n email: '',\n message: '',\n });\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await onSubmit({\n organizationId,\n email: formData.email,\n message: formData.message || undefined,\n });\n setFormData({ email: '', message: '' });\n } catch {\n // Error is surfaced by the parent page via the mutation hook's error state\n }\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Email */}\n <div>\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.emailLabel', 'Email Address')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n <Mail size={16} className=\"text-[var(--color-text-muted)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => setFormData({ ...formData, email: e.target.value })}\n aria-label={tr('members.invite.emailLabel', 'Email Address')}\n className=\"\n w-full pl-10 pr-4 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('members.invite.emailPlaceholder', 'user@example.com')}\n disabled={loading}\n required\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-muted)] mt-1\">\n {tr(\n 'members.invite.emailHelp',\n 'The user will be invited as a Member. You can change their role after they join.'\n )}\n </p>\n </div>\n\n {/* Message (optional) */}\n <div>\n <label\n htmlFor=\"message\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.messageLabel', 'Personal Message')}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({tr('common.optional', 'optional')})\n </span>\n </label>\n <textarea\n id=\"message\"\n value={formData.message}\n onChange={(e) => setFormData({ ...formData, message: e.target.value })}\n aria-label={tr('members.invite.messageLabel', 'Personal Message')}\n className=\"\n w-full px-4 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 resize-none\n \"\n placeholder={tr(\n 'members.invite.messagePlaceholder',\n 'Add a personal message to the invite...'\n )}\n rows={3}\n maxLength={500}\n disabled={loading}\n />\n </div>\n\n {/* 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-[180px]\n \"\n >\n {loading ? <Loader2 size={16} className=\"animate-spin\" /> : <Send size={16} />}\n {tr('members.invite.send', 'Send Invite')}\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":";;;;;AAgBA,SAAgB,EAAiB,EAC/B,mBACA,aACA,aACA,cACwB;CACxB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAU,KAAe,EAAS;EACvC,OAAO;EACP,SAAS;EACV,CAAC;AAgBF,QACE,kBAAC,QAAD;EAAM,UAfa,OAAO,MAAuB;AACjD,KAAE,gBAAgB;AAClB,OAAI;AAMF,IALA,MAAM,EAAS;KACb;KACA,OAAO,EAAS;KAChB,SAAS,EAAS,WAAW,KAAA;KAC9B,CAAC,EACF,EAAY;KAAE,OAAO;KAAI,SAAS;KAAI,CAAC;WACjC;;EAMsB,WAAU;YAAxC;GAEE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,6BAA6B,gBAAgB;MAAE;MACnD,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAM,MAAM;OAAI,WAAU;OAAmC,CAAA;MACzD,CAAA,EACN,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO,EAAS;MAChB,WAAW,MAAM,EAAY;OAAE,GAAG;OAAU,OAAO,EAAE,OAAO;OAAO,CAAC;MACpE,cAAY,EAAG,6BAA6B,gBAAgB;MAC5D,WAAU;MAUV,aAAa,EAAG,mCAAmC,mBAAmB;MACtE,UAAU;MACV,UAAA;MACA,CAAA,CACE;;IACN,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,4BACA,mFACD;KACC,CAAA;IACA,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAFZ;KAIG,EAAG,+BAA+B,mBAAmB;KAAE;KACxD,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAAiD;OAC7C,EAAG,mBAAmB,WAAW;OAAC;OAC/B;;KACD;OACR,kBAAC,YAAD;IACE,IAAG;IACH,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,SAAS,EAAE,OAAO;KAAO,CAAC;IACtE,cAAY,EAAG,+BAA+B,mBAAmB;IACjE,WAAU;IAWV,aAAa,EACX,qCACA,0CACD;IACD,MAAM;IACN,WAAW;IACX,UAAU;IACV,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,WAAU;eAHZ,CAgBG,IAAU,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAC7E,EAAG,uBAAuB,cAAc,CAClC;QAET,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU;KACV,WAAU;eAcT,EAAG,iBAAiB,SAAS;KACvB,CAAA,CACL;;GACD"}
|
|
@@ -47,7 +47,7 @@ function l({ invite: l, onAccept: u, onReject: d, accepting: f, rejecting: p })
|
|
|
47
47
|
type: "button",
|
|
48
48
|
onClick: () => u(l.id),
|
|
49
49
|
disabled: f || p,
|
|
50
|
-
className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n
|
|
50
|
+
className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
|
|
51
51
|
children: [f ? /* @__PURE__ */ s(a, {
|
|
52
52
|
size: 14,
|
|
53
53
|
className: "animate-spin"
|
|
@@ -56,7 +56,7 @@ function l({ invite: l, onAccept: u, onReject: d, accepting: f, rejecting: p })
|
|
|
56
56
|
type: "button",
|
|
57
57
|
onClick: () => d(l.id),
|
|
58
58
|
disabled: f || p,
|
|
59
|
-
className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n
|
|
59
|
+
className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
|
|
60
60
|
children: [p ? /* @__PURE__ */ s(a, {
|
|
61
61
|
size: 14,
|
|
62
62
|
className: "animate-spin"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyInviteCard.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/MyInviteCard.tsx"],"sourcesContent":["import { Check, X, Building2, Clock, Loader2 } from 'lucide-react';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface MyInviteCardProps {\n invite: {\n id: string;\n email: string;\n role: string;\n expiresAt: string;\n message?: string | null;\n createdAt: string;\n organization?: {\n id: string;\n name: string;\n slug: string;\n } | null;\n };\n onAccept: (inviteId: string) => Promise<void>;\n onReject: (inviteId: string) => Promise<void>;\n accepting?: boolean;\n rejecting?: boolean;\n}\n\nexport function MyInviteCard({\n invite,\n onAccept,\n onReject,\n accepting,\n rejecting,\n}: MyInviteCardProps) {\n const tr = useTr();\n const daysUntilExpiry = Math.max(\n 0,\n Math.ceil((new Date(invite.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-5 transition-colors hover:border-[var(--color-border-focus)]\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n {/* Left: Org info */}\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"p-2 rounded-lg bg-[var(--color-bg-subtle)]\">\n <Building2 size={20} className=\"text-[var(--color-text-secondary)]\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"font-semibold text-[var(--color-text-primary)] truncate\">\n {invite.organization?.name ?? tr('common.organization', 'Organization')}\n </h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <MemberRoleBadge role={invite.role} />\n <span className=\"text-xs text-[var(--color-text-muted)] flex items-center gap-1\">\n <Clock size={12} />\n {daysUntilExpiry > 0\n ? `${tr('members.myInvites.expiresIn', 'Expires in')} ${daysUntilExpiry}d`\n : tr('members.myInvites.expiringSoon', 'Expiring soon')}\n </span>\n </div>\n {invite.message && (\n <p className=\"mt-2 text-sm text-[var(--color-text-secondary)] italic\">\n “{invite.message}”\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Actions */}\n <div className=\"flex flex-wrap items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n onClick={() => onAccept(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n
|
|
1
|
+
{"version":3,"file":"MyInviteCard.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/MyInviteCard.tsx"],"sourcesContent":["import { Check, X, Building2, Clock, Loader2 } from 'lucide-react';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface MyInviteCardProps {\n invite: {\n id: string;\n email: string;\n role: string;\n expiresAt: string;\n message?: string | null;\n createdAt: string;\n organization?: {\n id: string;\n name: string;\n slug: string;\n } | null;\n };\n onAccept: (inviteId: string) => Promise<void>;\n onReject: (inviteId: string) => Promise<void>;\n accepting?: boolean;\n rejecting?: boolean;\n}\n\nexport function MyInviteCard({\n invite,\n onAccept,\n onReject,\n accepting,\n rejecting,\n}: MyInviteCardProps) {\n const tr = useTr();\n const daysUntilExpiry = Math.max(\n 0,\n Math.ceil((new Date(invite.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-5 transition-colors hover:border-[var(--color-border-focus)]\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n {/* Left: Org info */}\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"p-2 rounded-lg bg-[var(--color-bg-subtle)]\">\n <Building2 size={20} className=\"text-[var(--color-text-secondary)]\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"font-semibold text-[var(--color-text-primary)] truncate\">\n {invite.organization?.name ?? tr('common.organization', 'Organization')}\n </h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <MemberRoleBadge role={invite.role} />\n <span className=\"text-xs text-[var(--color-text-muted)] flex items-center gap-1\">\n <Clock size={12} />\n {daysUntilExpiry > 0\n ? `${tr('members.myInvites.expiresIn', 'Expires in')} ${daysUntilExpiry}d`\n : tr('members.myInvites.expiringSoon', 'Expiring soon')}\n </span>\n </div>\n {invite.message && (\n <p className=\"mt-2 text-sm text-[var(--color-text-secondary)] italic\">\n “{invite.message}”\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Actions */}\n <div className=\"flex flex-wrap items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n onClick={() => onAccept(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {accepting ? <Loader2 size={14} className=\"animate-spin\" /> : <Check size={14} />}\n {tr('members.myInvites.accept', 'Accept')}\n </button>\n <button\n type=\"button\"\n onClick={() => onReject(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {rejecting ? <Loader2 size={14} className=\"animate-spin\" /> : <X size={14} />}\n {tr('members.myInvites.decline', 'Decline')}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,EAAa,EAC3B,WACA,aACA,aACA,cACA,gBACoB;CACpB,IAAM,IAAK,GAAO,EACZ,IAAkB,KAAK,IAC3B,GACA,KAAK,MAAM,IAAI,KAAK,EAAO,UAAU,CAAC,SAAS,GAAG,KAAK,KAAK,KAAK,MAAO,KAAK,KAAK,IAAI,CACvF;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAW,MAAM;MAAI,WAAU;MAAuC,CAAA;KAClE,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO,cAAc,QAAQ,EAAG,uBAAuB,eAAe;OACpE,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAiB,MAAM,EAAO,MAAQ,CAAA,EACtC,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAClB,IAAkB,IACf,GAAG,EAAG,+BAA+B,aAAa,CAAC,GAAG,EAAgB,KACtE,EAAG,kCAAkC,gBAAgB,CACpD;UACH;;MACL,EAAO,WACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAAsE;QAC5D,EAAO;QAAQ;QACrB;;MAEF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAcG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAChF,EAAG,4BAA4B,SAAS,CAClC;QACT,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAgBG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EAC5E,EAAG,6BAA6B,UAAU,CACpC;OACL;MACF;;EACF,CAAA"}
|
|
@@ -6,141 +6,118 @@ import { InviteStatusBadge as o } from "./InviteStatusBadge.js";
|
|
|
6
6
|
import { MemberRoleBadge as s } from "./MemberRoleBadge.js";
|
|
7
7
|
import { useState as c } from "react";
|
|
8
8
|
import { AlertCircle as l, Loader2 as u, RotateCw as d, X as f } from "lucide-react";
|
|
9
|
-
import {
|
|
9
|
+
import { ResponsiveTable as p } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
11
|
//#region src/organization-management/modules/members/components/PendingInvitesList.tsx
|
|
11
|
-
function
|
|
12
|
-
let
|
|
13
|
-
if (
|
|
12
|
+
function _({ organizationId: _ }) {
|
|
13
|
+
let v = a(), { invites: y, loading: b, error: x, total: S } = n(_, { status: i.Pending }, 50), { cancelInvite: C, loading: w } = t(), { resendInvite: T, loading: E } = r(), [D, O] = c(null);
|
|
14
|
+
if (b && y.length === 0) return /* @__PURE__ */ h("div", {
|
|
14
15
|
className: "flex items-center justify-center py-8",
|
|
15
|
-
children: /* @__PURE__ */
|
|
16
|
+
children: /* @__PURE__ */ h(u, {
|
|
16
17
|
size: 20,
|
|
17
18
|
className: "animate-spin text-[var(--color-text-muted)]"
|
|
18
19
|
})
|
|
19
20
|
});
|
|
20
|
-
if (
|
|
21
|
+
if (x) return /* @__PURE__ */ g("p", {
|
|
21
22
|
className: "py-6 text-center text-sm text-[var(--color-status-error-text)]",
|
|
22
23
|
children: [
|
|
23
|
-
|
|
24
|
+
v("members.myInvites.loadFailed", "Failed to load invites"),
|
|
24
25
|
": ",
|
|
25
|
-
|
|
26
|
+
x.message
|
|
26
27
|
]
|
|
27
28
|
});
|
|
28
|
-
if (
|
|
29
|
+
if (y.length === 0) return /* @__PURE__ */ h("p", {
|
|
29
30
|
className: "text-sm text-[var(--color-text-muted)] text-center py-6",
|
|
30
|
-
children:
|
|
31
|
+
children: v("members.myInvites.emptyTitle", "No pending invites")
|
|
31
32
|
});
|
|
32
|
-
let
|
|
33
|
+
let k = async (t, n) => {
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
+
O(null), await C(t), e.success(`${v("members.invite.cancelSuccess", "Invite cancelled for")} ${n}`);
|
|
35
36
|
} catch (t) {
|
|
36
|
-
let n = t instanceof Error ? t.message :
|
|
37
|
-
|
|
37
|
+
let n = t instanceof Error ? t.message : v("members.invite.cancelFailed", "Failed to cancel invite");
|
|
38
|
+
O(n), e.error(n);
|
|
38
39
|
}
|
|
39
|
-
},
|
|
40
|
+
}, A = async (t, n) => {
|
|
40
41
|
try {
|
|
41
|
-
|
|
42
|
+
O(null), await T(t), e.success(`${v("members.invite.resendSuccess", "Invite re-sent to")} ${n}`);
|
|
42
43
|
} catch (t) {
|
|
43
|
-
let n = t instanceof Error ? t.message :
|
|
44
|
-
|
|
44
|
+
let n = t instanceof Error ? t.message : v("members.invite.resendFailed", "Failed to resend invite");
|
|
45
|
+
O(n), e.error(n);
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
}, j = [
|
|
48
|
+
{
|
|
49
|
+
key: "email",
|
|
50
|
+
header: v("members.invite.emailLabel", "Email Address"),
|
|
51
|
+
priority: "primary",
|
|
52
|
+
cell: (e) => /* @__PURE__ */ h("span", {
|
|
53
|
+
className: "text-[var(--color-text-primary)]",
|
|
54
|
+
children: e.email
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "role",
|
|
59
|
+
header: v("members.list.column.role", "Role"),
|
|
60
|
+
cell: (e) => /* @__PURE__ */ h(s, { role: e.role })
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "status",
|
|
64
|
+
header: v("common.status", "Status"),
|
|
65
|
+
cell: (e) => /* @__PURE__ */ h(o, { status: e.status })
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "expires",
|
|
69
|
+
header: v("members.invite.expires", "Expires"),
|
|
70
|
+
cell: (e) => /* @__PURE__ */ h("span", {
|
|
71
|
+
className: "text-[var(--color-text-muted)]",
|
|
72
|
+
children: new Date(e.expiresAt).toLocaleDateString()
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
return /* @__PURE__ */ g("div", { children: [
|
|
77
|
+
/* @__PURE__ */ g("h3", {
|
|
49
78
|
className: "text-sm font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-3",
|
|
50
79
|
children: [
|
|
51
|
-
|
|
80
|
+
v("members.invite.pendingTitle", "Pending Invites"),
|
|
52
81
|
" (",
|
|
53
|
-
|
|
82
|
+
S,
|
|
54
83
|
")"
|
|
55
84
|
]
|
|
56
85
|
}),
|
|
57
|
-
|
|
86
|
+
D && /* @__PURE__ */ g("div", {
|
|
58
87
|
className: "mb-3 flex items-center gap-2 rounded-lg border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bgSubtle)] p-2.5",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
88
|
+
children: [/* @__PURE__ */ h(l, {
|
|
60
89
|
size: 14,
|
|
61
90
|
className: "shrink-0 text-[var(--color-status-error-text)]"
|
|
62
|
-
}), /* @__PURE__ */
|
|
91
|
+
}), /* @__PURE__ */ h("p", {
|
|
63
92
|
className: "text-sm text-[var(--color-status-error-text)]",
|
|
64
|
-
children:
|
|
93
|
+
children: D
|
|
65
94
|
})]
|
|
66
95
|
}),
|
|
67
|
-
/* @__PURE__ */ p
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
children: g("members.invite.expires", "Expires")
|
|
89
|
-
}),
|
|
90
|
-
/* @__PURE__ */ p("th", {
|
|
91
|
-
className: "text-right px-4 py-2.5 font-medium text-[var(--color-text-secondary)]",
|
|
92
|
-
children: g("common.actions", "Actions")
|
|
93
|
-
})
|
|
94
|
-
]
|
|
95
|
-
}) }), /* @__PURE__ */ p("tbody", { children: _.map((e) => /* @__PURE__ */ m("tr", {
|
|
96
|
-
className: "border-b border-[var(--color-border-default)] last:border-b-0 hover:bg-[var(--color-bg-subtle)] transition-colors",
|
|
97
|
-
children: [
|
|
98
|
-
/* @__PURE__ */ p("td", {
|
|
99
|
-
className: "px-4 py-3 text-[var(--color-text-primary)]",
|
|
100
|
-
children: e.email
|
|
101
|
-
}),
|
|
102
|
-
/* @__PURE__ */ p("td", {
|
|
103
|
-
className: "px-4 py-3",
|
|
104
|
-
children: /* @__PURE__ */ p(s, { role: e.role })
|
|
105
|
-
}),
|
|
106
|
-
/* @__PURE__ */ p("td", {
|
|
107
|
-
className: "px-4 py-3",
|
|
108
|
-
children: /* @__PURE__ */ p(o, { status: e.status })
|
|
109
|
-
}),
|
|
110
|
-
/* @__PURE__ */ p("td", {
|
|
111
|
-
className: "px-4 py-3 text-[var(--color-text-muted)]",
|
|
112
|
-
children: new Date(e.expiresAt).toLocaleDateString()
|
|
113
|
-
}),
|
|
114
|
-
/* @__PURE__ */ p("td", {
|
|
115
|
-
className: "px-4 py-3 text-right",
|
|
116
|
-
children: /* @__PURE__ */ m("div", {
|
|
117
|
-
className: "flex items-center justify-end gap-1",
|
|
118
|
-
children: [/* @__PURE__ */ p("button", {
|
|
119
|
-
type: "button",
|
|
120
|
-
onClick: () => O(e.id, e.email),
|
|
121
|
-
disabled: w,
|
|
122
|
-
className: "p-1.5 rounded-md hover:bg-[var(--color-bg-subtle)] text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors",
|
|
123
|
-
title: g("members.invite.resend", "Resend invite"),
|
|
124
|
-
"aria-label": `${g("members.invite.resendFor", "Resend invite for")} ${e.email}`,
|
|
125
|
-
children: /* @__PURE__ */ p(d, { size: 14 })
|
|
126
|
-
}), /* @__PURE__ */ p("button", {
|
|
127
|
-
type: "button",
|
|
128
|
-
onClick: () => D(e.id, e.email),
|
|
129
|
-
disabled: S,
|
|
130
|
-
className: "p-1.5 rounded-md text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-status-error-bgSubtle)] hover:text-[var(--color-status-error-text)]",
|
|
131
|
-
title: g("members.invite.cancel", "Cancel invite"),
|
|
132
|
-
"aria-label": `${g("members.invite.cancelFor", "Cancel invite for")} ${e.email}`,
|
|
133
|
-
children: /* @__PURE__ */ p(f, { size: 14 })
|
|
134
|
-
})]
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
]
|
|
138
|
-
}, e.id)) })]
|
|
139
|
-
})
|
|
96
|
+
/* @__PURE__ */ h(p, {
|
|
97
|
+
columns: j,
|
|
98
|
+
data: y,
|
|
99
|
+
rowKey: (e) => e.id,
|
|
100
|
+
rowActions: (e) => /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("button", {
|
|
101
|
+
type: "button",
|
|
102
|
+
onClick: () => A(e.id, e.email),
|
|
103
|
+
disabled: E,
|
|
104
|
+
className: "p-1.5 rounded-md hover:bg-[var(--color-bg-subtle)] text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors",
|
|
105
|
+
title: v("members.invite.resend", "Resend invite"),
|
|
106
|
+
"aria-label": `${v("members.invite.resendFor", "Resend invite for")} ${e.email}`,
|
|
107
|
+
children: /* @__PURE__ */ h(d, { size: 14 })
|
|
108
|
+
}), /* @__PURE__ */ h("button", {
|
|
109
|
+
type: "button",
|
|
110
|
+
onClick: () => k(e.id, e.email),
|
|
111
|
+
disabled: w,
|
|
112
|
+
className: "p-1.5 rounded-md text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-status-error-bgSubtle)] hover:text-[var(--color-status-error-text)]",
|
|
113
|
+
title: v("members.invite.cancel", "Cancel invite"),
|
|
114
|
+
"aria-label": `${v("members.invite.cancelFor", "Cancel invite for")} ${e.email}`,
|
|
115
|
+
children: /* @__PURE__ */ h(f, { size: 14 })
|
|
116
|
+
})] })
|
|
140
117
|
})
|
|
141
118
|
] });
|
|
142
119
|
}
|
|
143
120
|
//#endregion
|
|
144
|
-
export {
|
|
121
|
+
export { _ as PendingInvitesList };
|
|
145
122
|
|
|
146
123
|
//# sourceMappingURL=PendingInvitesList.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingInvitesList.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/PendingInvitesList.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { RotateCw, X, Loader2, AlertCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useOrganizationInvites, useCancelInvite, useResendInvite } from '../hooks/useInvites';\nimport { InviteStatusBadge } from './InviteStatusBadge';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { InviteStatus } from '../../../../generated/global-types';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface PendingInvitesListProps {\n organizationId: string;\n}\n\nexport function PendingInvitesList({ organizationId }: PendingInvitesListProps) {\n const tr = useTr();\n const { invites, loading, error, total } = useOrganizationInvites(\n organizationId,\n { status: InviteStatus.Pending },\n 50\n );\n const { cancelInvite, loading: cancelling } = useCancelInvite();\n const { resendInvite, loading: resending } = useResendInvite();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (loading && invites.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 size={20} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <p className=\"py-6 text-center text-sm text-[var(--color-status-error-text)]\">\n {tr('members.myInvites.loadFailed', 'Failed to load invites')}: {error.message}\n </p>\n );\n }\n\n if (invites.length === 0) {\n return (\n <p className=\"text-sm text-[var(--color-text-muted)] text-center py-6\">\n {tr('members.myInvites.emptyTitle', 'No pending invites')}\n </p>\n );\n }\n\n const handleCancel = async (inviteId: string, email: string) => {\n try {\n setActionError(null);\n await cancelInvite(inviteId);\n toast.success(`${tr('members.invite.cancelSuccess', 'Invite cancelled for')} ${email}`);\n } catch (err) {\n const msg =\n err instanceof Error\n ? err.message\n : tr('members.invite.cancelFailed', 'Failed to cancel invite');\n setActionError(msg);\n toast.error(msg);\n }\n };\n\n const handleResend = async (inviteId: string, email: string) => {\n try {\n setActionError(null);\n await resendInvite(inviteId);\n toast.success(`${tr('members.invite.resendSuccess', 'Invite re-sent to')} ${email}`);\n } catch (err) {\n const msg =\n err instanceof Error\n ? err.message\n : tr('members.invite.resendFailed', 'Failed to resend invite');\n setActionError(msg);\n toast.error(msg);\n }\n };\n\n
|
|
1
|
+
{"version":3,"file":"PendingInvitesList.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/PendingInvitesList.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { RotateCw, X, Loader2, AlertCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { ResponsiveTable, type ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { useOrganizationInvites, useCancelInvite, useResendInvite } from '../hooks/useInvites';\nimport { InviteStatusBadge } from './InviteStatusBadge';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { InviteStatus } from '../../../../generated/global-types';\nimport type { OrganizationInvite } from '../../../../generated/global-types';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface PendingInvitesListProps {\n organizationId: string;\n}\n\nexport function PendingInvitesList({ organizationId }: PendingInvitesListProps) {\n const tr = useTr();\n const { invites, loading, error, total } = useOrganizationInvites(\n organizationId,\n { status: InviteStatus.Pending },\n 50\n );\n const { cancelInvite, loading: cancelling } = useCancelInvite();\n const { resendInvite, loading: resending } = useResendInvite();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (loading && invites.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 size={20} className=\"animate-spin text-[var(--color-text-muted)]\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <p className=\"py-6 text-center text-sm text-[var(--color-status-error-text)]\">\n {tr('members.myInvites.loadFailed', 'Failed to load invites')}: {error.message}\n </p>\n );\n }\n\n if (invites.length === 0) {\n return (\n <p className=\"text-sm text-[var(--color-text-muted)] text-center py-6\">\n {tr('members.myInvites.emptyTitle', 'No pending invites')}\n </p>\n );\n }\n\n const handleCancel = async (inviteId: string, email: string) => {\n try {\n setActionError(null);\n await cancelInvite(inviteId);\n toast.success(`${tr('members.invite.cancelSuccess', 'Invite cancelled for')} ${email}`);\n } catch (err) {\n const msg =\n err instanceof Error\n ? err.message\n : tr('members.invite.cancelFailed', 'Failed to cancel invite');\n setActionError(msg);\n toast.error(msg);\n }\n };\n\n const handleResend = async (inviteId: string, email: string) => {\n try {\n setActionError(null);\n await resendInvite(inviteId);\n toast.success(`${tr('members.invite.resendSuccess', 'Invite re-sent to')} ${email}`);\n } catch (err) {\n const msg =\n err instanceof Error\n ? err.message\n : tr('members.invite.resendFailed', 'Failed to resend invite');\n setActionError(msg);\n toast.error(msg);\n }\n };\n\n const columns: ResponsiveTableColumn<OrganizationInvite>[] = [\n {\n key: 'email',\n header: tr('members.invite.emailLabel', 'Email Address'),\n priority: 'primary',\n cell: (invite) => <span className=\"text-[var(--color-text-primary)]\">{invite.email}</span>,\n },\n {\n key: 'role',\n header: tr('members.list.column.role', 'Role'),\n cell: (invite) => <MemberRoleBadge role={invite.role} />,\n },\n {\n key: 'status',\n header: tr('common.status', 'Status'),\n cell: (invite) => <InviteStatusBadge status={invite.status} />,\n },\n {\n key: 'expires',\n header: tr('members.invite.expires', 'Expires'),\n cell: (invite) => (\n <span className=\"text-[var(--color-text-muted)]\">\n {new Date(invite.expiresAt).toLocaleDateString()}\n </span>\n ),\n },\n ];\n\n return (\n <div>\n <h3 className=\"text-sm font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-3\">\n {tr('members.invite.pendingTitle', 'Pending Invites')} ({total})\n </h3>\n\n {actionError && (\n <div className=\"mb-3 flex items-center gap-2 rounded-lg border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bgSubtle)] p-2.5\">\n <AlertCircle size={14} className=\"shrink-0 text-[var(--color-status-error-text)]\" />\n <p className=\"text-sm text-[var(--color-status-error-text)]\">{actionError}</p>\n </div>\n )}\n\n <ResponsiveTable<OrganizationInvite>\n columns={columns}\n data={invites}\n rowKey={(invite) => invite.id}\n rowActions={(invite) => (\n <>\n <button\n type=\"button\"\n onClick={() => handleResend(invite.id, invite.email)}\n disabled={resending}\n className=\"p-1.5 rounded-md hover:bg-[var(--color-bg-subtle)] text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors\"\n title={tr('members.invite.resend', 'Resend invite')}\n aria-label={`${tr('members.invite.resendFor', 'Resend invite for')} ${invite.email}`}\n >\n <RotateCw size={14} />\n </button>\n <button\n type=\"button\"\n onClick={() => handleCancel(invite.id, invite.email)}\n disabled={cancelling}\n className=\"p-1.5 rounded-md text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-status-error-bgSubtle)] hover:text-[var(--color-status-error-text)]\"\n title={tr('members.invite.cancel', 'Cancel invite')}\n aria-label={`${tr('members.invite.cancelFor', 'Cancel invite for')} ${invite.email}`}\n >\n <X size={14} />\n </button>\n </>\n )}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -29,7 +29,7 @@ function g() {
|
|
|
29
29
|
className: "min-h-full bg-[var(--color-bg-canvas)] p-6",
|
|
30
30
|
children: /* @__PURE__ */ m(i, { message: x?.message || g("members.invitePeople.error", "Failed to load organizations for invite.") })
|
|
31
31
|
}) : /* @__PURE__ */ m("div", {
|
|
32
|
-
className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-
|
|
32
|
+
className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-200",
|
|
33
33
|
children: /* @__PURE__ */ h("div", {
|
|
34
34
|
className: "mx-auto max-w-3xl space-y-6",
|
|
35
35
|
children: [/* @__PURE__ */ h("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InvitePeoplePage.js","names":[],"sources":["../../../../../src/organization-management/modules/members/pages/InvitePeoplePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Building, ChevronRight, UserPlus } from 'lucide-react';\nimport { useOrgConsole } from '../../../providers/OrgConsoleProvider';\nimport { useMyOrganizations } from '../../organizations/hooks/useOrganizations';\nimport { useOrgsWithPermission } from '../../organizations/hooks/useOrgsWithPermission';\nimport { LoadingState } from '../../../shared/components/feedback/LoadingState';\nimport { ErrorState } from '../../../shared/components/feedback/ErrorState';\nimport { EmptyState } from '../../../shared/components/feedback/EmptyState';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport { useOrganizationPageView } from '../../../hooks/useOrganizationPageView';\n\n/**\n * InvitePeoplePage\n *\n * Globally-reachable entry point (sidebar nav item) for inviting users.\n * Lists the orgs the current user can invite into, then deep-links into\n * each org's existing `members/invite` page so the invite flow itself\n * remains a single source of truth.\n *\n * If the user has exactly one org with invite permission, redirect\n * straight to that org's invite page — saves a click for the common case.\n */\nexport function InvitePeoplePage() {\n useOrganizationPageView('organizations.invite-people');\n const tr = useTr();\n const { navigate, basePath = '/organizations' } = useOrgConsole();\n const { organizations, loading, error } = useMyOrganizations(100, 0);\n\n const orgIds = useMemo(() => organizations.map((o) => o.id), [organizations]);\n const {\n allowedIds,\n loading: permLoading,\n isError: permError,\n } = useOrgsWithPermission(orgIds, 'member', 'create');\n\n const invitable = useMemo(\n () => organizations.filter((o) => allowedIds.has(o.id)),\n [organizations, allowedIds]\n );\n\n const [autoRedirected, setAutoRedirected] = useState(false);\n useEffect(() => {\n if (autoRedirected) return;\n if (loading || permLoading) return;\n if (invitable.length === 1) {\n setAutoRedirected(true);\n navigate(`${basePath}/${invitable[0].id}/members/invite`);\n }\n }, [autoRedirected, loading, permLoading, invitable, navigate, basePath]);\n\n if (loading || permLoading) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <LoadingState message={tr('common.loading', 'Loading...')} />\n </div>\n );\n }\n\n if (error || permError) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <ErrorState\n message={\n error?.message ||\n tr('members.invitePeople.error', 'Failed to load organizations for invite.')\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-
|
|
1
|
+
{"version":3,"file":"InvitePeoplePage.js","names":[],"sources":["../../../../../src/organization-management/modules/members/pages/InvitePeoplePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Building, ChevronRight, UserPlus } from 'lucide-react';\nimport { useOrgConsole } from '../../../providers/OrgConsoleProvider';\nimport { useMyOrganizations } from '../../organizations/hooks/useOrganizations';\nimport { useOrgsWithPermission } from '../../organizations/hooks/useOrgsWithPermission';\nimport { LoadingState } from '../../../shared/components/feedback/LoadingState';\nimport { ErrorState } from '../../../shared/components/feedback/ErrorState';\nimport { EmptyState } from '../../../shared/components/feedback/EmptyState';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport { useOrganizationPageView } from '../../../hooks/useOrganizationPageView';\n\n/**\n * InvitePeoplePage\n *\n * Globally-reachable entry point (sidebar nav item) for inviting users.\n * Lists the orgs the current user can invite into, then deep-links into\n * each org's existing `members/invite` page so the invite flow itself\n * remains a single source of truth.\n *\n * If the user has exactly one org with invite permission, redirect\n * straight to that org's invite page — saves a click for the common case.\n */\nexport function InvitePeoplePage() {\n useOrganizationPageView('organizations.invite-people');\n const tr = useTr();\n const { navigate, basePath = '/organizations' } = useOrgConsole();\n const { organizations, loading, error } = useMyOrganizations(100, 0);\n\n const orgIds = useMemo(() => organizations.map((o) => o.id), [organizations]);\n const {\n allowedIds,\n loading: permLoading,\n isError: permError,\n } = useOrgsWithPermission(orgIds, 'member', 'create');\n\n const invitable = useMemo(\n () => organizations.filter((o) => allowedIds.has(o.id)),\n [organizations, allowedIds]\n );\n\n const [autoRedirected, setAutoRedirected] = useState(false);\n useEffect(() => {\n if (autoRedirected) return;\n if (loading || permLoading) return;\n if (invitable.length === 1) {\n setAutoRedirected(true);\n navigate(`${basePath}/${invitable[0].id}/members/invite`);\n }\n }, [autoRedirected, loading, permLoading, invitable, navigate, basePath]);\n\n if (loading || permLoading) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <LoadingState message={tr('common.loading', 'Loading...')} />\n </div>\n );\n }\n\n if (error || permError) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <ErrorState\n message={\n error?.message ||\n tr('members.invitePeople.error', 'Failed to load organizations for invite.')\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-200\">\n <div className=\"mx-auto max-w-3xl space-y-6\">\n <div className=\"flex items-center gap-3\">\n <UserPlus size={24} className=\"text-[var(--color-text-primary)]\" aria-hidden=\"true\" />\n <div>\n <h1 className=\"text-2xl font-bold text-[var(--color-text-primary)]\">\n {tr('members.invitePeople.title', 'Invite People')}\n </h1>\n <p className=\"mt-1 text-sm text-[var(--color-text-secondary)]\">\n {tr(\n 'members.invitePeople.subtitle',\n 'Pick an organization to send an invite into. You can also invite from any organization or workspace detail page.'\n )}\n </p>\n </div>\n </div>\n\n {invitable.length === 0 ? (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-6\">\n <EmptyState\n icon={<UserPlus size={40} aria-hidden=\"true\" />}\n title={tr('members.invitePeople.emptyTitle', 'No organizations where you can invite')}\n description={tr(\n 'members.invitePeople.emptyDescription',\n \"You don't currently have permission to invite members into any organization. Ask an organization admin to grant you invite access.\"\n )}\n />\n </div>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-default)] rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] shadow-[var(--shadow-elevation-1)]\">\n {invitable.map((org) => (\n <li key={org.id}>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/${org.id}/members/invite`)}\n className=\"flex w-full items-center justify-between gap-4 px-5 py-4 text-left transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-status-info-bgSubtle)] text-[var(--color-status-info-text)]\">\n <Building size={20} aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-[var(--color-text-primary)]\">\n {org.name}\n </p>\n <p className=\"truncate font-mono text-xs text-[var(--color-text-secondary)]\">\n /{org.slug}\n </p>\n </div>\n </div>\n <ChevronRight\n size={18}\n className=\"shrink-0 text-[var(--color-text-secondary)]\"\n aria-hidden=\"true\"\n />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,SAAgB,IAAmB;AACjC,GAAwB,8BAA8B;CACtD,IAAM,IAAK,GAAO,EACZ,EAAE,aAAU,cAAW,qBAAqB,GAAe,EAC3D,EAAE,kBAAe,YAAS,aAAU,EAAmB,KAAK,EAAE,EAG9D,EACJ,eACA,SAAS,GACT,SAAS,MACP,EALW,QAAc,EAAc,KAAK,MAAM,EAAE,GAAG,EAAE,CAAC,EAAc,CAAC,EAK3C,UAAU,SAAS,EAE/C,IAAY,QACV,EAAc,QAAQ,MAAM,EAAW,IAAI,EAAE,GAAG,CAAC,EACvD,CAAC,GAAe,EAAW,CAC5B,EAEK,CAAC,GAAgB,KAAqB,EAAS,GAAM;AA+B3D,QA9BA,QAAgB;AACV,OACA,KAAW,KACX,EAAU,WAAW,MACvB,EAAkB,GAAK,EACvB,EAAS,GAAG,EAAS,GAAG,EAAU,GAAG,GAAG,iBAAiB;IAE1D;EAAC;EAAgB;EAAS;EAAa;EAAW;EAAU;EAAS,CAAC,EAErE,KAAW,IAEX,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAc,SAAS,EAAG,kBAAkB,aAAa,EAAI,CAAA;EACzD,CAAA,GAIN,KAAS,IAET,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EACE,SACE,GAAO,WACP,EAAG,8BAA8B,2CAA2C,EAE9E,CAAA;EACE,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAU,MAAM;KAAI,WAAU;KAAmC,eAAY;KAAS,CAAA,EACtF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,gBAAgB;KAC/C,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,iCACA,mHACD;KACC,CAAA,CACA,EAAA,CAAA,CACF;OAEL,EAAU,WAAW,IACpB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD;MAAU,MAAM;MAAI,eAAY;MAAS,CAAA;KAC/C,OAAO,EAAG,mCAAmC,wCAAwC;KACrF,aAAa,EACX,yCACA,qIACD;KACD,CAAA;IACE,CAAA,GAEN,kBAAC,MAAD;IAAI,WAAU;cACX,EAAU,KAAK,MACd,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,GAAG,EAAS,GAAG,EAAI,GAAG,iBAAiB;KAC/D,WAAU;eAHZ,CAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAU,MAAM;QAAI,eAAY;QAAS,CAAA;OACrC,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI;QACH,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBAAb,CAA6E,KACzE,EAAI,KACJ;UACA;SACF;SACN,kBAAC,GAAD;MACE,MAAM;MACN,WAAU;MACV,eAAY;MACZ,CAAA,CACK;QACN,EAzBI,EAAI,GAyBR,CACL;IACC,CAAA,CAEH;;EACF,CAAA"}
|
|
@@ -40,13 +40,13 @@ function g() {
|
|
|
40
40
|
}
|
|
41
41
|
})
|
|
42
42
|
}) : /* @__PURE__ */ f("div", {
|
|
43
|
-
className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-
|
|
43
|
+
className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-200",
|
|
44
44
|
children: [/* @__PURE__ */ f("div", {
|
|
45
45
|
className: "mb-6",
|
|
46
46
|
children: [/* @__PURE__ */ f("button", {
|
|
47
47
|
type: "button",
|
|
48
48
|
onClick: () => y(`${b}/${_}/members`),
|
|
49
|
-
className: "inline-flex items-center gap-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]
|
|
49
|
+
className: "inline-flex items-center gap-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-all duration-200 mb-4",
|
|
50
50
|
children: [/* @__PURE__ */ d(c, { size: 20 }), g("members.detail.backToMembers", "Back to Members")]
|
|
51
51
|
}), /* @__PURE__ */ f("div", {
|
|
52
52
|
className: "flex items-start gap-4",
|