feat: enhance session management by resolving collaborators to users and integrating CollaboratorDisplay component across motivators and sessions pages
This commit is contained in:
@@ -3,7 +3,7 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getMotivatorSessionById } from '@/services/moving-motivators';
|
import { getMotivatorSessionById } from '@/services/moving-motivators';
|
||||||
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
|
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
|
||||||
import { Badge } from '@/components/ui';
|
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
||||||
import { EditableMotivatorTitle } from './EditableTitle';
|
import { EditableMotivatorTitle } from './EditableTitle';
|
||||||
|
|
||||||
interface MotivatorSessionPageProps {
|
interface MotivatorSessionPageProps {
|
||||||
@@ -48,9 +48,13 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
|
|||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-lg text-muted">
|
<div className="mt-2">
|
||||||
👤 {session.participant}
|
<CollaboratorDisplay
|
||||||
</p>
|
collaborator={session.resolvedParticipant}
|
||||||
|
size="lg"
|
||||||
|
showEmail
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant="primary">
|
<Badge variant="primary">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
|
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
|
||||||
import { Card, CardContent, Badge, Button } from '@/components/ui';
|
import { Card, CardContent, Badge, Button, CollaboratorDisplay } from '@/components/ui';
|
||||||
|
|
||||||
export default async function MotivatorsPage() {
|
export default async function MotivatorsPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@@ -95,7 +95,9 @@ function SessionCard({ session: s }: { session: SessionWithMeta }) {
|
|||||||
<h3 className="font-semibold text-foreground line-clamp-1">
|
<h3 className="font-semibold text-foreground line-clamp-1">
|
||||||
{s.title}
|
{s.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted">{s.participant}</p>
|
<div className="mt-1">
|
||||||
|
<CollaboratorDisplay collaborator={s.resolvedParticipant} size="sm" />
|
||||||
|
</div>
|
||||||
{!s.isOwner && (
|
{!s.isOwner && (
|
||||||
<p className="text-xs text-muted mt-1">
|
<p className="text-xs text-muted mt-1">
|
||||||
Par {s.user.name || s.user.email}
|
Par {s.user.name || s.user.email}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, Badge, Button, Modal, ModalFooter, Input } from '@/components/ui';
|
import { Card, Badge, Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui';
|
||||||
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
|
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
|
||||||
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
|
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
|
||||||
|
|
||||||
@@ -20,10 +20,20 @@ interface Share {
|
|||||||
user: ShareUser;
|
user: ShareUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResolvedCollaborator {
|
||||||
|
raw: string;
|
||||||
|
matchedUser: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface SwotSession {
|
interface SwotSession {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
collaborator: string;
|
collaborator: string;
|
||||||
|
resolvedCollaborator: ResolvedCollaborator;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
role: 'OWNER' | 'VIEWER' | 'EDITOR';
|
role: 'OWNER' | 'VIEWER' | 'EDITOR';
|
||||||
@@ -37,6 +47,7 @@ interface MotivatorSession {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
participant: string;
|
participant: string;
|
||||||
|
resolvedParticipant: ResolvedCollaborator;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
role: 'OWNER' | 'VIEWER' | 'EDITOR';
|
role: 'OWNER' | 'VIEWER' | 'EDITOR';
|
||||||
@@ -60,20 +71,45 @@ function getParticipant(session: AnySession): string {
|
|||||||
: (session as MotivatorSession).participant;
|
: (session as MotivatorSession).participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group sessions by participant
|
// Helper to get resolved collaborator from any session
|
||||||
|
function getResolvedCollaborator(session: AnySession): ResolvedCollaborator {
|
||||||
|
return session.workshopType === 'swot'
|
||||||
|
? (session as SwotSession).resolvedCollaborator
|
||||||
|
: (session as MotivatorSession).resolvedParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get display name for grouping - prefer matched user name
|
||||||
|
function getDisplayName(session: AnySession): string {
|
||||||
|
const resolved = getResolvedCollaborator(session);
|
||||||
|
if (resolved.matchedUser?.name) {
|
||||||
|
return resolved.matchedUser.name;
|
||||||
|
}
|
||||||
|
return resolved.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get grouping key - use matched user ID if available, otherwise normalized raw string
|
||||||
|
function getGroupKey(session: AnySession): string {
|
||||||
|
const resolved = getResolvedCollaborator(session);
|
||||||
|
// If we have a matched user, use their ID as key (ensures same person = same group)
|
||||||
|
if (resolved.matchedUser) {
|
||||||
|
return `user:${resolved.matchedUser.id}`;
|
||||||
|
}
|
||||||
|
// Otherwise, normalize the raw string
|
||||||
|
return `raw:${resolved.raw.trim().toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group sessions by participant (using matched user ID when available)
|
||||||
function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> {
|
function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> {
|
||||||
const grouped = new Map<string, AnySession[]>();
|
const grouped = new Map<string, AnySession[]>();
|
||||||
|
|
||||||
sessions.forEach((session) => {
|
sessions.forEach((session) => {
|
||||||
const participant = getParticipant(session).trim().toLowerCase();
|
const key = getGroupKey(session);
|
||||||
const displayName = getParticipant(session).trim();
|
|
||||||
|
|
||||||
// Use normalized key but store with original display name
|
const existing = grouped.get(key);
|
||||||
const existing = grouped.get(participant);
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.push(session);
|
existing.push(session);
|
||||||
} else {
|
} else {
|
||||||
grouped.set(participant, [session]);
|
grouped.set(key, [session]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,16 +191,13 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{sortedPersons.map(([personKey, sessions]) => {
|
{sortedPersons.map(([personKey, sessions]) => {
|
||||||
const displayName = getParticipant(sessions[0]);
|
const resolved = getResolvedCollaborator(sessions[0]);
|
||||||
return (
|
return (
|
||||||
<section key={personKey}>
|
<section key={personKey}>
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-3">
|
||||||
<span className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary text-sm">
|
<CollaboratorDisplay collaborator={resolved} size="md" />
|
||||||
{displayName.charAt(0).toUpperCase()}
|
<Badge variant="primary">
|
||||||
</span>
|
{sessions.length} atelier{sessions.length > 1 ? 's' : ''}
|
||||||
{displayName}
|
|
||||||
<Badge variant="primary" className="ml-2">
|
|
||||||
{sessions.length}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
@@ -336,12 +369,15 @@ function SessionCard({ session }: { session: AnySession }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Participant + Owner info */}
|
{/* Participant + Owner info */}
|
||||||
<p className="text-sm text-muted mb-3 line-clamp-1">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
👤 {participant}
|
<CollaboratorDisplay
|
||||||
|
collaborator={getResolvedCollaborator(session)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
{!session.isOwner && (
|
{!session.isOwner && (
|
||||||
<span className="text-xs"> · par {session.user.name || session.user.email}</span>
|
<span className="text-xs text-muted">· par {session.user.name || session.user.email}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
{/* Footer: Stats + Avatars + Date */}
|
{/* Footer: Stats + Avatars + Date */}
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getSessionById } from '@/services/sessions';
|
|||||||
import { SwotBoard } from '@/components/swot/SwotBoard';
|
import { SwotBoard } from '@/components/swot/SwotBoard';
|
||||||
import { SessionLiveWrapper } from '@/components/collaboration';
|
import { SessionLiveWrapper } from '@/components/collaboration';
|
||||||
import { EditableTitle } from '@/components/session';
|
import { EditableTitle } from '@/components/session';
|
||||||
import { Badge } from '@/components/ui';
|
import { Badge, CollaboratorDisplay } from '@/components/ui';
|
||||||
|
|
||||||
interface SessionPageProps {
|
interface SessionPageProps {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
@@ -49,9 +49,13 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
isOwner={session.isOwner}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-lg text-muted">
|
<div className="mt-2">
|
||||||
👤 {session.collaborator}
|
<CollaboratorDisplay
|
||||||
</p>
|
collaborator={session.resolvedCollaborator}
|
||||||
|
size="lg"
|
||||||
|
showEmail
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant="primary">{session.items.length} items</Badge>
|
<Badge variant="primary">{session.items.length} items</Badge>
|
||||||
|
|||||||
82
src/components/ui/CollaboratorDisplay.tsx
Normal file
82
src/components/ui/CollaboratorDisplay.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { getGravatarUrl } from '@/lib/gravatar';
|
||||||
|
|
||||||
|
interface CollaboratorDisplayProps {
|
||||||
|
collaborator: {
|
||||||
|
raw: string;
|
||||||
|
matchedUser: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
showEmail?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeConfig = {
|
||||||
|
sm: { avatar: 24, text: 'text-sm', gap: 'gap-1.5' },
|
||||||
|
md: { avatar: 32, text: 'text-base', gap: 'gap-2' },
|
||||||
|
lg: { avatar: 40, text: 'text-lg', gap: 'gap-3' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CollaboratorDisplay({
|
||||||
|
collaborator,
|
||||||
|
size = 'md',
|
||||||
|
showEmail = false,
|
||||||
|
}: CollaboratorDisplayProps) {
|
||||||
|
const { raw, matchedUser } = collaborator;
|
||||||
|
const config = sizeConfig[size];
|
||||||
|
|
||||||
|
// If we have a matched user, show their avatar and name
|
||||||
|
if (matchedUser) {
|
||||||
|
const displayName = matchedUser.name || matchedUser.email.split('@')[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center ${config.gap}`}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={getGravatarUrl(matchedUser.email, config.avatar * 2)}
|
||||||
|
alt={displayName}
|
||||||
|
width={config.avatar}
|
||||||
|
height={config.avatar}
|
||||||
|
className="rounded-full border border-border"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className={`${config.text} font-medium text-foreground truncate block`}>
|
||||||
|
{displayName}
|
||||||
|
</span>
|
||||||
|
{showEmail && matchedUser.name && (
|
||||||
|
<span className="text-xs text-muted truncate block">{matchedUser.email}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match - just show the raw name with a default person icon
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center ${config.gap}`}>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-full border border-border bg-card-hover"
|
||||||
|
style={{ width: config.avatar, height: config.avatar }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="text-muted"
|
||||||
|
style={{ width: config.avatar * 0.5, height: config.avatar * 0.5 }}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className={`${config.text} text-muted truncate`}>{raw}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ export { Avatar } from './Avatar';
|
|||||||
export { Badge } from './Badge';
|
export { Badge } from './Badge';
|
||||||
export { Button } from './Button';
|
export { Button } from './Button';
|
||||||
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
||||||
|
export { CollaboratorDisplay } from './CollaboratorDisplay';
|
||||||
export { Input } from './Input';
|
export { Input } from './Input';
|
||||||
export { Modal, ModalFooter } from './Modal';
|
export { Modal, ModalFooter } from './Modal';
|
||||||
export { Textarea } from './Textarea';
|
export { Textarea } from './Textarea';
|
||||||
|
|||||||
@@ -60,6 +60,53 @@ export async function getUserByEmail(email: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if string looks like an email
|
||||||
|
function isEmail(str: string): boolean {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedCollaborator {
|
||||||
|
raw: string; // Original value (name or email)
|
||||||
|
matchedUser: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve collaborator string to user - try email first, then name
|
||||||
|
export async function resolveCollaborator(collaborator: string): Promise<ResolvedCollaborator> {
|
||||||
|
const trimmed = collaborator.trim();
|
||||||
|
|
||||||
|
// 1. Try email match first
|
||||||
|
if (isEmail(trimmed)) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: trimmed.toLowerCase() },
|
||||||
|
select: { id: true, email: true, name: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return { raw: collaborator, matchedUser: user };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback: try matching by name (case-insensitive via raw query for SQLite)
|
||||||
|
// SQLite LIKE is case-insensitive by default for ASCII
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
name: { not: null },
|
||||||
|
},
|
||||||
|
select: { id: true, email: true, name: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedSearch = trimmed.toLowerCase();
|
||||||
|
const userByName = users.find(
|
||||||
|
(u) => u.name?.toLowerCase() === normalizedSearch
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
return { raw: collaborator, matchedUser: userByName };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserById(id: string) {
|
export async function getUserById(id: string) {
|
||||||
return prisma.user.findUnique({
|
return prisma.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import type { ShareRole, MotivatorType } from '@prisma/client';
|
import type { ShareRole, MotivatorType } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -56,9 +57,19 @@ export async function getMotivatorSessionsByUserId(userId: string) {
|
|||||||
sharedAt: s.createdAt,
|
sharedAt: s.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...ownedWithRole, ...sharedWithRole].sort(
|
const allSessions = [...ownedWithRole, ...sharedWithRole].sort(
|
||||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve participants to users
|
||||||
|
const sessionsWithResolved = await Promise.all(
|
||||||
|
allSessions.map(async (s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: await resolveCollaborator(s.participant),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return sessionsWithResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMotivatorSessionById(sessionId: string, userId: string) {
|
export async function getMotivatorSessionById(sessionId: string, userId: string) {
|
||||||
@@ -92,7 +103,10 @@ export async function getMotivatorSessionById(sessionId: string, userId: string)
|
|||||||
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
||||||
const canEdit = isOwner || role === 'EDITOR';
|
const canEdit = isOwner || role === 'EDITOR';
|
||||||
|
|
||||||
return { ...session, isOwner, role, canEdit };
|
// Resolve participant to user if it's an email
|
||||||
|
const resolvedParticipant = await resolveCollaborator(session.participant);
|
||||||
|
|
||||||
|
return { ...session, isOwner, role, canEdit, resolvedParticipant };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user can access session (owner or shared)
|
// Check if user can access session (owner or shared)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import type { SwotCategory, ShareRole } from '@prisma/client';
|
import type { SwotCategory, ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -58,9 +59,19 @@ export async function getSessionsByUserId(userId: string) {
|
|||||||
sharedAt: s.createdAt,
|
sharedAt: s.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...ownedWithRole, ...sharedWithRole].sort(
|
const allSessions = [...ownedWithRole, ...sharedWithRole].sort(
|
||||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve collaborators to users
|
||||||
|
const sessionsWithResolved = await Promise.all(
|
||||||
|
allSessions.map(async (s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedCollaborator: await resolveCollaborator(s.collaborator),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return sessionsWithResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessionById(sessionId: string, userId: string) {
|
export async function getSessionById(sessionId: string, userId: string) {
|
||||||
@@ -104,7 +115,10 @@ export async function getSessionById(sessionId: string, userId: string) {
|
|||||||
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
||||||
const canEdit = isOwner || role === 'EDITOR';
|
const canEdit = isOwner || role === 'EDITOR';
|
||||||
|
|
||||||
return { ...session, isOwner, role, canEdit };
|
// Resolve collaborator to user if it's an email
|
||||||
|
const resolvedCollaborator = await resolveCollaborator(session.collaborator);
|
||||||
|
|
||||||
|
return { ...session, isOwner, role, canEdit, resolvedCollaborator };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user can access session (owner or shared)
|
// Check if user can access session (owner or shared)
|
||||||
|
|||||||
Reference in New Issue
Block a user