bugfix/demo-bugs-fixed #19
@@ -25,7 +25,9 @@ import { Badge } from '@/components/ui/shared/Badge';
|
|||||||
import { Input } from '@/components/ui/shared/Input';
|
import { Input } from '@/components/ui/shared/Input';
|
||||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||||
import { Table } from '@/components/ui/shared/Table';
|
import { Table } from '@/components/ui/shared/Table';
|
||||||
import type { ApiError } from '@/types/api';
|
import { ToastStack } from '@/components/ui/shared/Toast';
|
||||||
|
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
|
||||||
|
import { useToast } from '@/lib/hooks/useToast';
|
||||||
|
|
||||||
type StoredInviteLink = {
|
type StoredInviteLink = {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
@@ -54,14 +56,6 @@ function writeStoredInviteLinks(orgId: string, links: Record<string, StoredInvit
|
|||||||
window.localStorage.setItem(inviteLinksStorageKey(orgId), JSON.stringify(links));
|
window.localStorage.setItem(inviteLinksStorageKey(orgId), JSON.stringify(links));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatApiMessage(err: unknown): string {
|
|
||||||
if (!err || typeof err !== 'object') return 'Something went wrong';
|
|
||||||
const m = (err as ApiError).message;
|
|
||||||
if (Array.isArray(m)) return m.join(', ');
|
|
||||||
if (typeof m === 'string') return m;
|
|
||||||
return 'Something went wrong';
|
|
||||||
}
|
|
||||||
|
|
||||||
function canShareStaffInviteLink(member: StaffMemberDto): boolean {
|
function canShareStaffInviteLink(member: StaffMemberDto): boolean {
|
||||||
return !member.isOwner && member.invitationStatus !== 'ACTIVE';
|
return !member.isOwner && member.invitationStatus !== 'ACTIVE';
|
||||||
}
|
}
|
||||||
@@ -136,8 +130,7 @@ export default function StaffPage() {
|
|||||||
unlimited: boolean;
|
unlimited: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const toast = useToast();
|
||||||
const [success, setSuccess] = useState('');
|
|
||||||
|
|
||||||
const [inviteOpen, setInviteOpen] = useState(false);
|
const [inviteOpen, setInviteOpen] = useState(false);
|
||||||
const [inviteEmail, setInviteEmail] = useState('');
|
const [inviteEmail, setInviteEmail] = useState('');
|
||||||
@@ -169,14 +162,14 @@ export default function StaffPage() {
|
|||||||
}, [seats]);
|
}, [seats]);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setError('');
|
toast.setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await staffApi.list();
|
const res = await staffApi.list();
|
||||||
setMembers(res.data.members);
|
setMembers(res.data.members);
|
||||||
setSeats(res.data.seats);
|
setSeats(res.data.seats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Failed to load staff.'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -221,17 +214,11 @@ export default function StaffPage() {
|
|||||||
}
|
}
|
||||||
}, [currentOrganization, router]);
|
}, [currentOrganization, router]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!success) return;
|
|
||||||
const t = setTimeout(() => setSuccess(''), 4000);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [success]);
|
|
||||||
|
|
||||||
async function copyStaffInviteLink(member: StaffMemberDto) {
|
async function copyStaffInviteLink(member: StaffMemberDto) {
|
||||||
if (!canShareStaffInviteLink(member)) return;
|
if (!canShareStaffInviteLink(member)) return;
|
||||||
|
|
||||||
setCopyingInviteMembershipId(member.id);
|
setCopyingInviteMembershipId(member.id);
|
||||||
setError('');
|
toast.setError('');
|
||||||
try {
|
try {
|
||||||
let invitationUrl = pendingInviteLinks[member.id]?.invitationUrl;
|
let invitationUrl = pendingInviteLinks[member.id]?.invitationUrl;
|
||||||
if (!invitationUrl || member.invitationStatus === 'EXPIRED') {
|
if (!invitationUrl || member.invitationStatus === 'EXPIRED') {
|
||||||
@@ -257,7 +244,7 @@ export default function StaffPage() {
|
|||||||
await load();
|
await load();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Could not copy invitation link.'));
|
||||||
} finally {
|
} finally {
|
||||||
setCopyingInviteMembershipId(null);
|
setCopyingInviteMembershipId(null);
|
||||||
}
|
}
|
||||||
@@ -265,7 +252,7 @@ export default function StaffPage() {
|
|||||||
|
|
||||||
async function submitInvite() {
|
async function submitInvite() {
|
||||||
setInviteLoading(true);
|
setInviteLoading(true);
|
||||||
setError('');
|
toast.setError('');
|
||||||
setLastInviteInfo(null);
|
setLastInviteInfo(null);
|
||||||
const displayName = inviteName.trim();
|
const displayName = inviteName.trim();
|
||||||
const displayEmail = inviteEmail.trim();
|
const displayEmail = inviteEmail.trim();
|
||||||
@@ -295,14 +282,13 @@ export default function StaffPage() {
|
|||||||
setPendingInviteLinks(nextLinks);
|
setPendingInviteLinks(nextLinks);
|
||||||
writeStoredInviteLinks(currentOrganization.id, nextLinks);
|
writeStoredInviteLinks(currentOrganization.id, nextLinks);
|
||||||
}
|
}
|
||||||
setSuccess('');
|
|
||||||
setInviteOpen(false);
|
setInviteOpen(false);
|
||||||
setInviteEmail('');
|
setInviteEmail('');
|
||||||
setInviteName('');
|
setInviteName('');
|
||||||
setInvitePerms(emptyFeaturePermissionState());
|
setInvitePerms(emptyFeaturePermissionState());
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Failed to send invitation.'));
|
||||||
} finally {
|
} finally {
|
||||||
setInviteLoading(false);
|
setInviteLoading(false);
|
||||||
}
|
}
|
||||||
@@ -320,17 +306,17 @@ export default function StaffPage() {
|
|||||||
async function submitEdit() {
|
async function submitEdit() {
|
||||||
if (!editing) return;
|
if (!editing) return;
|
||||||
setEditLoading(true);
|
setEditLoading(true);
|
||||||
setError('');
|
toast.setError('');
|
||||||
try {
|
try {
|
||||||
await staffApi.updateMember(editing.id, {
|
await staffApi.updateMember(editing.id, {
|
||||||
name: editName.trim(),
|
name: editName.trim(),
|
||||||
permissionNames: permissionNamesFromFeatureState(editPerms),
|
permissionNames: permissionNamesFromFeatureState(editPerms),
|
||||||
});
|
});
|
||||||
setSuccess('Member updated');
|
toast.showSuccess('Member updated.');
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Failed to update member.'));
|
||||||
} finally {
|
} finally {
|
||||||
setEditLoading(false);
|
setEditLoading(false);
|
||||||
}
|
}
|
||||||
@@ -343,13 +329,13 @@ export default function StaffPage() {
|
|||||||
} else {
|
} else {
|
||||||
if (!confirm(`Remove ${m.name} from this organization?`)) return;
|
if (!confirm(`Remove ${m.name} from this organization?`)) return;
|
||||||
}
|
}
|
||||||
setError('');
|
toast.setError('');
|
||||||
try {
|
try {
|
||||||
await staffApi.removeMember(m.id);
|
await staffApi.removeMember(m.id);
|
||||||
setSuccess('Member removed');
|
toast.showSuccess('Member removed.');
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Failed to remove member.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +369,8 @@ export default function StaffPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ToastStack {...toast.messages} />
|
||||||
|
|
||||||
{seats && (
|
{seats && (
|
||||||
<p className="text-sm text-text-secondary">
|
<p className="text-sm text-text-secondary">
|
||||||
Seats:{' '}
|
Seats:{' '}
|
||||||
@@ -400,18 +388,6 @@ export default function StaffPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-[var(--radius-md)] border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-700 dark:text-red-300">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<div className="rounded-[var(--radius-md)] border border-primary/30 bg-primary-soft/40 px-4 py-3 text-sm text-text-primary">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{lastInviteInfo && (
|
{lastInviteInfo && (
|
||||||
<div className="relative rounded-[var(--radius-md)] border border-border-strong bg-background-secondary/90 px-4 py-3 pr-12 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] space-y-3">
|
<div className="relative rounded-[var(--radius-md)] border border-border-strong bg-background-secondary/90 px-4 py-3 pr-12 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] space-y-3">
|
||||||
<button
|
<button
|
||||||
@@ -453,7 +429,7 @@ export default function StaffPage() {
|
|||||||
}
|
}
|
||||||
void (async () => {
|
void (async () => {
|
||||||
setCopyingInviteMembershipId(lastInviteInfo.membershipId);
|
setCopyingInviteMembershipId(lastInviteInfo.membershipId);
|
||||||
setError('');
|
toast.setError('');
|
||||||
try {
|
try {
|
||||||
const res = await staffApi.getInvitationLink(lastInviteInfo.membershipId);
|
const res = await staffApi.getInvitationLink(lastInviteInfo.membershipId);
|
||||||
if (currentOrganization?.id) {
|
if (currentOrganization?.id) {
|
||||||
@@ -473,7 +449,7 @@ export default function StaffPage() {
|
|||||||
setCopiedInviteMembershipId(lastInviteInfo.membershipId);
|
setCopiedInviteMembershipId(lastInviteInfo.membershipId);
|
||||||
setTimeout(() => setCopiedInviteMembershipId(null), 1500);
|
setTimeout(() => setCopiedInviteMembershipId(null), 1500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(formatApiMessage(e));
|
toast.showError(formatApiErrorMessage(e, 'Could not copy invitation link.'));
|
||||||
} finally {
|
} finally {
|
||||||
setCopyingInviteMembershipId(null);
|
setCopyingInviteMembershipId(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user