Skip to content
2 changes: 1 addition & 1 deletion src/invites/components/InvitesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const InvitesListView = ({ invites }) => {
export const InvitesList = ({ currentUser }) => {
// Get invitations
const [invites] = useQuery(getInvites, {
where: { email: currentUser!.email },
where: { email: currentUser!.email.toLowerCase() },
orderBy: { id: "asc" },
include: { project: true },
})
Expand Down
24 changes: 23 additions & 1 deletion src/invites/queries/getInvites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ interface GetInvitationInput
export default resolver.pipe(
resolver.authorize(),
async ({ where, orderBy, include }: GetInvitationInput) => {
// Normalize email filter to be case-insensitive if present
const normalizedWhere: Prisma.InvitationWhereInput | undefined = where
? { ...where }
: undefined

if (normalizedWhere && typeof (normalizedWhere as any).email === "string") {
;(normalizedWhere as any).email = {
equals: (normalizedWhere as any).email,
mode: "insensitive",
}
} else if (
normalizedWhere &&
(normalizedWhere as any).email &&
typeof (normalizedWhere as any).email === "object" &&
typeof (normalizedWhere as any).email.equals === "string"
) {
;(normalizedWhere as any).email = {
...(normalizedWhere as any).email,
mode: "insensitive",
}
}

const invites = await db.invitation.findMany({
where,
where: normalizedWhere,
orderBy,
include,
})
Expand Down
29 changes: 23 additions & 6 deletions src/notes/components/NotesEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ import { HeadingNode, QuoteNode } from "@lexical/rich-text"
import { CodeNode } from "@lexical/code"
import { LinkNode } from "@lexical/link"

const normalizeVisibility = (
v: "PRIVATE" | "PM_ONLY" | "CONTRIBUTORS" | "SHARED" | undefined
): "PRIVATE" | "PM_ONLY" | "CONTRIBUTORS" => {
if (v === "SHARED") return "CONTRIBUTORS"
if (v === "PRIVATE" || v === "PM_ONLY" || v === "CONTRIBUTORS") return v
return "PRIVATE"
}

function ResetFormatOnEnterPlugin() {
const [editor] = useLexicalComposerContext()
useEffect(() => {
Expand Down Expand Up @@ -198,16 +206,25 @@ export default function NoteEditor({
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null)
const [title, setTitle] = useState<string>(initialTitle || "")
const [visibility, setVisibility] = useState<"PRIVATE" | "PM_ONLY" | "CONTRIBUTORS">(
initialVisibility
normalizeVisibility(initialVisibility as any)
)
const didInitialChange = useRef(false)

useEffect(() => {
setVisibility(normalizeVisibility(initialVisibility as any))
}, [initialVisibility])

const effectiveReadOnly = useMemo(() => {
if (visibility === "CONTRIBUTORS" && canSetContributors) return false
return readOnly
}, [readOnly, visibility, canSetContributors])

const initialConfig = useMemo(
() => ({
namespace: "staple-notes",
nodes: [ListNode, ListItemNode, HeadingNode, QuoteNode, CodeNode, LinkNode],
onError: (e: any) => console.error(e),
editable: !readOnly,
editable: !effectiveReadOnly,
editorState: (editor: any) => {
if (initialJSON) {
editor.setEditorState(editor.parseEditorState(initialJSON))
Expand All @@ -220,7 +237,7 @@ export default function NoteEditor({
}
},
}),
[initialJSON, initialMarkdown, readOnly]
[initialJSON, initialMarkdown, effectiveReadOnly]
)

const save = useCallback(
Expand All @@ -234,7 +251,7 @@ export default function NoteEditor({
})
contentJSON = editorState.toJSON()

const visibilityForMutation = (visibility === "CONTRIBUTORS" ? "SHARED" : visibility) as any
const visibilityForMutation = visibility as any

if (!currentNoteId) {
const created = await createNoteMutation({
Expand Down Expand Up @@ -287,7 +304,7 @@ export default function NoteEditor({
onSaveAndClose={(state) => save(state, true)}
isSaving={isSaving}
lastSavedAt={lastSavedAt}
readOnly={readOnly}
readOnly={effectiveReadOnly}
onClose={onClose}
visibility={visibility}
onVisibilityChange={setVisibility}
Expand All @@ -307,7 +324,7 @@ export default function NoteEditor({
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<OnChangePlugin
onChange={(state) => {
if (readOnly) return
if (effectiveReadOnly) return
if (!didInitialChange.current) {
didInitialChange.current = true
return
Expand Down
22 changes: 15 additions & 7 deletions src/notes/components/NotesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
? [...notes].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
: []

const canEdit = (n: any) => !!(n && (n as any).editable)
const canEditRow = (n: any) => {
if (!n) return false
const ownerEditable = !!n.editable
const pmOverride = !!n.canSetContributors && n.visibility === "CONTRIBUTORS"
return ownerEditable || pmOverride
}

return (
<div className="space-y-4">
Expand Down Expand Up @@ -76,7 +81,11 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
initialTitle={n.title ?? ""}
initialMarkdown={n.contentMarkdown ?? ""}
initialJSON={n.contentJSON}
initialVisibility={n.visibility}
className="shadow"
readOnly={!canEditRow(n)}
canSetContributors={!!n.canSetContributors}
onClose={() => setEditingId(null)}
onSaved={async () => {
setEditingId(null)
await refetch()
Expand Down Expand Up @@ -126,7 +135,7 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
<div className="flex items-center gap-2">
<button
className={`btn ${n.pinned ? "btn-warning" : "btn-secondary"}`}
disabled={!canEdit(n)}
disabled={!canEditRow(n)}
onClick={async () => {
await updateNoteMutation({ id: n.id, pinned: !n.pinned })
await refetch()
Expand All @@ -137,7 +146,7 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
{!n.archived && (
<button
className="btn btn-outline"
disabled={!canEdit(n)}
disabled={!canEditRow(n)}
onClick={async () => {
await updateNoteMutation({ id: n.id, archived: true })
await refetch()
Expand All @@ -149,7 +158,7 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
{n.archived && (
<button
className="btn btn-outline"
disabled={!canEdit(n)}
disabled={!canEditRow(n)}
onClick={async () => {
await updateNoteMutation({ id: n.id, archived: false })
// Optimistically update local cache to reflect unarchive
Expand All @@ -164,17 +173,16 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
</button>
)}
<button className="btn btn-primary" onClick={() => setEditingId(n.id)}>
{canEdit(n) ? "Edit" : "View"}
{canEditRow(n) ? "Edit" : "View"}
</button>
<button
className="btn btn-error"
disabled={!canEditRow(n)}
onClick={async () => {
if (window.confirm("This note will be permanently deleted. Continue?")) {
await deleteNoteMutation({ id: n.id })
// Close modal/editing state regardless of delete success
setEditingId(null)
setCreating(false)
// Optimistically update local cache
await setQueryData((prev) =>
Array.isArray(prev) ? prev.filter((x) => x.id !== n.id) : []
)
Expand Down
7 changes: 6 additions & 1 deletion src/notes/mutations/createNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import db from "db"
import { resolver } from "@blitzjs/rpc"
import { CreateNoteInput } from "src/notes/schemas"
import { NoteVisibility, MemberPrivileges } from "@prisma/client"
import { z } from "zod"

export default resolver.pipe(
resolver.zod(CreateNoteInput),
resolver.zod(
CreateNoteInput.extend({
visibility: z.union([z.nativeEnum(NoteVisibility), z.literal("SHARED")]).optional(),
})
),
resolver.authorize(),
async ({ projectId, ...data }, ctx) => {
const userId = ctx.session.userId!
Expand Down
7 changes: 6 additions & 1 deletion src/notes/mutations/updateNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import db from "db"
import { resolver } from "@blitzjs/rpc"
import { UpdateNoteInput } from "src/notes/schemas"
import { NoteVisibility, MemberPrivileges } from "@prisma/client"
import { z } from "zod"

export default resolver.pipe(
resolver.zod(UpdateNoteInput),
resolver.zod(
UpdateNoteInput.extend({
visibility: z.union([z.nativeEnum(NoteVisibility), z.literal("SHARED")]).optional(),
})
),
resolver.authorize(),
async ({ id, ...data }, ctx) => {
const userId = ctx.session.userId!
Expand Down
7 changes: 6 additions & 1 deletion src/notes/queries/listNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ export default resolver.pipe(

return notes.map((r) => ({
...r,
editable: r.authorId === member.id || (isPM && r.visibility === NoteVisibility.PM_ONLY),
canSetContributors: isPM,
editable:
r.authorId === member.id ||
(isPM &&
(r.visibility === NoteVisibility.PM_ONLY ||
r.visibility === NoteVisibility.CONTRIBUTORS)),
}))
}
)
5 changes: 3 additions & 2 deletions src/notes/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { z } from "zod"
import { NoteVisibility } from "@prisma/client"

export const NoteVisibilityEnum = z.enum(["PRIVATE", "SHARED"])
export const NoteVisibilityEnum = z.nativeEnum(NoteVisibility)

export const CreateNoteInput = z.object({
projectId: z.number().int().positive(),
title: z.string().max(200).optional(),
contentMarkdown: z.string().optional(),
contentJSON: z.any().optional(),
visibility: NoteVisibilityEnum.default("PRIVATE"),
visibility: NoteVisibilityEnum.default(NoteVisibility.PRIVATE),
pinned: z.boolean().optional(),
})

Expand Down
Loading