fix: store and populate memberKey for share invite link
This commit is contained in:
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+25
-25
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -10,8 +10,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Press+Start+2P&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Press+Start+2P&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<script type="module" crossorigin src="/assets/index-D2ZOrmDk.js"></script>
|
<script type="module" crossorigin src="/assets/index-h2AVDMMH.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-COtT48NQ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C1k3Ryr0.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-50 text-slate-900 min-h-screen font-sans antialiased selection:bg-indigo-500 selection:text-white transition-colors duration-200">
|
<body class="bg-slate-50 text-slate-900 min-h-screen font-sans antialiased selection:bg-indigo-500 selection:text-white transition-colors duration-200">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -960,9 +960,11 @@ export const BoardView: React.FC<BoardViewProps> = ({ boardId, hash: propHash, o
|
|||||||
id: board.id,
|
id: board.id,
|
||||||
title: board.title,
|
title: board.title,
|
||||||
adminKey: role === 'admin' ? key : '***',
|
adminKey: role === 'admin' ? key : '***',
|
||||||
memberKey: role === 'member' ? key : '***',
|
memberKey: role === 'admin' ? (board.memberKey || '') : (role === 'member' ? key : '***'),
|
||||||
adminUrlFragment: `/b/${board.id}#admin=${role === 'admin' ? key : ''}`,
|
adminUrlFragment: role === 'admin' ? `/b/${board.id}#admin=${key}` : '',
|
||||||
memberUrlFragment: `/b/${board.id}#member=${role === 'member' ? key : ''}`,
|
memberUrlFragment: role === 'admin'
|
||||||
|
? (board.memberKey ? `/b/${board.id}#member=${board.memberKey}` : `/b/${board.id}#member=`)
|
||||||
|
: `/b/${board.id}#member=${key}`,
|
||||||
createdAt: board.createdAt,
|
createdAt: board.createdAt,
|
||||||
}}
|
}}
|
||||||
onEnterBoard={() => setShowShareModal(false)}
|
onEnterBoard={() => setShowShareModal(false)}
|
||||||
|
|||||||
@@ -67,14 +67,15 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
if (onLanguageChange) onLanguageChange(next);
|
if (onLanguageChange) onLanguageChange(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveName = (e: React.FormEvent) => {
|
const handleSaveName = (e?: React.FormEvent) => {
|
||||||
e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
if (tempName.trim()) {
|
const trimmed = tempName.trim();
|
||||||
setUserName(tempName.trim());
|
if (trimmed) {
|
||||||
setProfileState(getUserProfile());
|
const updated = setUserName(trimmed);
|
||||||
setIsEditingName(false);
|
setProfileState(updated);
|
||||||
soundFx.playClick();
|
soundFx.playClick();
|
||||||
}
|
}
|
||||||
|
setIsEditingName(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -118,11 +119,19 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
maxLength={18}
|
maxLength={18}
|
||||||
value={tempName}
|
value={tempName}
|
||||||
onChange={(e) => setTempName(e.target.value)}
|
onChange={(e) => setTempName(e.target.value)}
|
||||||
className="bg-white dark:bg-slate-900 border border-indigo-500 text-slate-900 dark:text-indigo-300 text-xs px-2 py-1 rounded-md outline-none w-20 sm:w-32"
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setTempName(profile.userName);
|
||||||
|
setIsEditingName(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-white dark:bg-slate-900 border border-indigo-500 text-slate-900 dark:text-indigo-300 text-xs px-2 py-1 rounded-md outline-none w-24 sm:w-36"
|
||||||
autoFocus
|
autoFocus
|
||||||
onBlur={() => setIsEditingName(false)}
|
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="bg-indigo-600 text-white text-[10px] font-bold px-1.5 py-1 rounded-md">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-bold px-2 py-1.5 rounded-md shadow-sm"
|
||||||
|
>
|
||||||
OK
|
OK
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -130,13 +139,16 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
soundFx.playClick();
|
soundFx.playClick();
|
||||||
|
setTempName(profile.userName);
|
||||||
setIsEditingName(true);
|
setIsEditingName(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 bg-slate-100 dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800 px-2 py-1.5 text-xs text-slate-700 dark:text-slate-200 rounded-lg hover:border-indigo-500 transition-all"
|
className="flex items-center gap-1 bg-slate-100 dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800 px-2 py-1.5 text-xs text-slate-700 dark:text-slate-200 rounded-lg hover:border-indigo-500 transition-all"
|
||||||
title={t('changeName')}
|
title={t('changeName')}
|
||||||
>
|
>
|
||||||
<User size={12} className="text-indigo-600 dark:text-indigo-400 shrink-0" />
|
<User size={12} className="text-indigo-600 dark:text-indigo-400 shrink-0" />
|
||||||
<span className="max-w-[50px] xs:max-w-[70px] sm:max-w-[120px] truncate text-[11px]">{profile.userName}</span>
|
<span className="max-w-[50px] xs:max-w-[70px] sm:max-w-[120px] truncate text-[11px] font-medium">
|
||||||
|
{profile.userName}
|
||||||
|
</span>
|
||||||
<span className="text-[9px] text-slate-400">✏️</span>
|
<span className="text-[9px] text-slate-400">✏️</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export interface Board {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
|
memberKey?: string;
|
||||||
|
memberUrlFragment?: string;
|
||||||
autoMoveStale: boolean;
|
autoMoveStale: boolean;
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ model Board {
|
|||||||
title String @default("Agile Kanban Board")
|
title String @default("Agile Kanban Board")
|
||||||
adminKeyHash String
|
adminKeyHash String
|
||||||
memberKeyHash String
|
memberKeyHash String
|
||||||
|
memberKey String? @default("")
|
||||||
autoMoveStale Boolean @default(false) // Auto move stale cards to Backlog
|
autoMoveStale Boolean @default(false) // Auto move stale cards to Backlog
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ model Board {
|
|||||||
title String @default("Agile Kanban Board")
|
title String @default("Agile Kanban Board")
|
||||||
adminKeyHash String
|
adminKeyHash String
|
||||||
memberKeyHash String
|
memberKeyHash String
|
||||||
|
memberKey String? @default("")
|
||||||
autoMoveStale Boolean @default(false)
|
autoMoveStale Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ export class BoardService {
|
|||||||
const adminKeyHash = hashKey(adminKey);
|
const adminKeyHash = hashKey(adminKey);
|
||||||
const memberKeyHash = hashKey(memberKey);
|
const memberKeyHash = hashKey(memberKey);
|
||||||
|
|
||||||
const board = await prisma.board.create({
|
const board = await (prisma.board as any).create({
|
||||||
data: {
|
data: {
|
||||||
title: title.trim() || 'Agile Kanban Board',
|
title: title.trim() || 'Agile Kanban Board',
|
||||||
adminKeyHash,
|
adminKeyHash,
|
||||||
memberKeyHash,
|
memberKeyHash,
|
||||||
|
memberKey,
|
||||||
autoMoveStale: false,
|
autoMoveStale: false,
|
||||||
columns: {
|
columns: {
|
||||||
create: [
|
create: [
|
||||||
@@ -90,22 +91,20 @@ export class BoardService {
|
|||||||
boardId: col.boardId,
|
boardId: col.boardId,
|
||||||
title: col.title,
|
title: col.title,
|
||||||
order: col.order,
|
order: col.order,
|
||||||
maxWipLimit: col.maxWipLimit || 0,
|
maxWipLimit: col.maxWipLimit,
|
||||||
autoStaleHours: col.autoStaleHours || 0,
|
autoStaleHours: col.autoStaleHours,
|
||||||
createdAt: col.createdAt.toISOString(),
|
|
||||||
updatedAt: col.updatedAt.toISOString(),
|
|
||||||
tickets: col.tickets.map((t) => {
|
tickets: col.tickets.map((t) => {
|
||||||
let parsedTags: string[] = [];
|
let tags: string[] = [];
|
||||||
let parsedSubtasks: Subtask[] = [];
|
let subtasks: Subtask[] = [];
|
||||||
try {
|
try {
|
||||||
parsedTags = JSON.parse(t.tags || '[]');
|
tags = JSON.parse(t.tags || '[]');
|
||||||
} catch {
|
} catch {
|
||||||
parsedTags = [];
|
tags = [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
parsedSubtasks = JSON.parse(t.subtasks || '[]');
|
subtasks = JSON.parse(t.subtasks || '[]');
|
||||||
} catch {
|
} catch {
|
||||||
parsedSubtasks = [];
|
subtasks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -115,8 +114,8 @@ export class BoardService {
|
|||||||
description: t.description,
|
description: t.description,
|
||||||
order: t.order,
|
order: t.order,
|
||||||
priority: t.priority as Priority,
|
priority: t.priority as Priority,
|
||||||
tags: parsedTags,
|
tags,
|
||||||
subtasks: parsedSubtasks,
|
subtasks,
|
||||||
dueDate: t.dueDate || null,
|
dueDate: t.dueDate || null,
|
||||||
isBlocked: t.isBlocked || false,
|
isBlocked: t.isBlocked || false,
|
||||||
blockedReason: t.blockedReason || null,
|
blockedReason: t.blockedReason || null,
|
||||||
@@ -128,10 +127,26 @@ export class BoardService {
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let memberKeyVal = (board as any).memberKey || '';
|
||||||
|
if (!memberKeyVal && role === 'admin') {
|
||||||
|
const newMemberKey = generateSecureKey(32);
|
||||||
|
const newMemberKeyHash = hashKey(newMemberKey);
|
||||||
|
await (prisma.board as any).update({
|
||||||
|
where: { id: boardId },
|
||||||
|
data: {
|
||||||
|
memberKey: newMemberKey,
|
||||||
|
memberKeyHash: newMemberKeyHash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
memberKeyVal = newMemberKey;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: board.id,
|
id: board.id,
|
||||||
title: board.title,
|
title: board.title,
|
||||||
role,
|
role,
|
||||||
|
memberKey: memberKeyVal,
|
||||||
|
memberUrlFragment: memberKeyVal ? `/b/${board.id}#member=${memberKeyVal}` : '',
|
||||||
autoMoveStale: board.autoMoveStale || false,
|
autoMoveStale: board.autoMoveStale || false,
|
||||||
columns,
|
columns,
|
||||||
createdAt: board.createdAt.toISOString(),
|
createdAt: board.createdAt.toISOString(),
|
||||||
@@ -146,10 +161,11 @@ export class BoardService {
|
|||||||
const newMemberKey = generateSecureKey(32);
|
const newMemberKey = generateSecureKey(32);
|
||||||
const newMemberKeyHash = hashKey(newMemberKey);
|
const newMemberKeyHash = hashKey(newMemberKey);
|
||||||
|
|
||||||
await prisma.board.update({
|
await (prisma.board as any).update({
|
||||||
where: { id: boardId },
|
where: { id: boardId },
|
||||||
data: {
|
data: {
|
||||||
memberKeyHash: newMemberKeyHash,
|
memberKeyHash: newMemberKeyHash,
|
||||||
|
memberKey: newMemberKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface BoardPayload {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
|
memberKey?: string;
|
||||||
|
memberUrlFragment?: string;
|
||||||
autoMoveStale: boolean;
|
autoMoveStale: boolean;
|
||||||
columns: ColumnPayload[];
|
columns: ColumnPayload[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user