isRead working
parent
3ae716a8c4
commit
40e40a3864
|
|
@ -115,6 +115,7 @@ async def get_inbox_read_status(
|
||||||
@router.get("/inbox/all-applications")
|
@router.get("/inbox/all-applications")
|
||||||
async def get_all_applications(
|
async def get_all_applications(
|
||||||
record_id: str | None = Query(None),
|
record_id: str | None = Query(None),
|
||||||
|
isread: bool = Query(default=True),
|
||||||
search: str | None = Query(None),
|
search: str | None = Query(None),
|
||||||
top: int | None = Query(None),
|
top: int | None = Query(None),
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
|
|
@ -123,6 +124,10 @@ async def get_all_applications(
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
service=Email(session=session)
|
service=Email(session=session)
|
||||||
|
if isread==False:
|
||||||
|
items=await service.get_all_applications(top, skip, search, isread=False)
|
||||||
|
total=await service.count_inbox_messages(search, isread=False)
|
||||||
|
return JSONResponse(content={"data":items,"total":total,"status_code":200})
|
||||||
if record_id:
|
if record_id:
|
||||||
item=await service.get_application_by_id(record_id)
|
item=await service.get_application_by_id(record_id)
|
||||||
return JSONResponse(content={"data":item,"total":1,"status_code":200})
|
return JSONResponse(content={"data":item,"total":1,"status_code":200})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# (str, Enum), like EnumRoles and PermissionTag: a bare Enum member is not JSON
|
||||||
|
# serializable, so JSONResponse raises the moment a serializer emits this field.
|
||||||
|
class Candidate_application_Status(str, Enum):
|
||||||
|
PROCESS="PROCESS"
|
||||||
|
PENDING="PENDING"
|
||||||
|
APPROVED="APPROVED"
|
||||||
|
REJECTED="REJECTED"
|
||||||
|
ONHOLD="ONHOLD"
|
||||||
|
CLOSED="CLOSED"
|
||||||
|
|
@ -2,11 +2,11 @@ import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
from inbox.enums import Candidate_application_Status
|
||||||
from sqlalchemy import Column, DateTime, func, or_, update
|
from sqlalchemy import Column, DateTime, func, or_, update
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlmodel import Field, Relationship, SQLModel, select
|
from sqlmodel import Field, Relationship, SQLModel, select, true
|
||||||
|
|
||||||
from users.models import Users
|
from users.models import Users
|
||||||
|
|
||||||
|
|
@ -50,6 +50,7 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
full_email_response: dict[str, Any] | None = Field(
|
full_email_response: dict[str, Any] | None = Field(
|
||||||
default=None, sa_column=Column(JSONB)
|
default=None, sa_column=Column(JSONB)
|
||||||
)
|
)
|
||||||
|
application_status: Candidate_application_Status = Field(default=Candidate_application_Status.CLOSED)
|
||||||
message_subject: str
|
message_subject: str
|
||||||
message_body: str
|
message_body: str
|
||||||
message_sent_time: str
|
message_sent_time: str
|
||||||
|
|
@ -195,7 +196,7 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_inbox_messages(
|
async def get_inbox_messages(
|
||||||
cls, session: AsyncSession, top: int | None, skip: int, search: str | None
|
cls, session: AsyncSession, top: int | None, skip: int, search: str | None, isread: bool=True
|
||||||
):
|
):
|
||||||
statement = select(cls).order_by(cls.message_received_time.desc())
|
statement = select(cls).order_by(cls.message_received_time.desc())
|
||||||
if search:
|
if search:
|
||||||
|
|
@ -204,6 +205,8 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
statement = statement.offset(skip)
|
statement = statement.offset(skip)
|
||||||
if top is not None:
|
if top is not None:
|
||||||
statement = statement.limit(top)
|
statement = statement.limit(top)
|
||||||
|
if isread==False:
|
||||||
|
statement = statement.where(cls.message_read==False)
|
||||||
result = await session.execute(statement)
|
result = await session.execute(statement)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
@ -217,33 +220,36 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
return result.scalars().first()
|
return result.scalars().first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def count_inbox_messages(cls, session: AsyncSession, search: str | None):
|
async def count_inbox_messages(cls, session: AsyncSession, search: str | None, isread: bool=True):
|
||||||
statement = select(func.count()).select_from(cls)
|
statement = select(func.count()).select_from(cls)
|
||||||
if search:
|
if search:
|
||||||
statement = statement.where(cls._search_filter(search))
|
statement = statement.where(cls._search_filter(search))
|
||||||
|
if isread==False:
|
||||||
|
statement = statement.where(cls.message_read==False)
|
||||||
result = await session.execute(statement)
|
result = await session.execute(statement)
|
||||||
return result.scalar_one()
|
return result.scalar_one()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def apply_read_status(cls, session: AsyncSession, changes) -> int:
|
async def apply_read_status(cls, session: AsyncSession, changes) -> int:
|
||||||
"""[{id, isRead, ...}] -> bulk UPDATE message_read. Returns rows touched."""
|
"""[{id, isRead, ...}] -> bulk UPDATE message_read. Returns rows touched.
|
||||||
|
|
||||||
|
read is a ONE-WAY LATCH: only false -> true is applied, never the reverse.
|
||||||
|
mark_message_read writes the local column only — nothing pushes the state
|
||||||
|
back to Outlook — so upstream keeps reporting isRead=false and the
|
||||||
|
every-minute sync_read_status sweep would otherwise revert a mail the user
|
||||||
|
just opened. Cost of the latch: un-reading a mail in Outlook no longer
|
||||||
|
propagates here.
|
||||||
|
"""
|
||||||
if not changes:
|
if not changes:
|
||||||
return 0
|
return 0
|
||||||
read_ids=[c.get("id") for c in changes if c.get("id") and c.get("isRead")]
|
read_ids=[c.get("id") for c in changes if c.get("id") and c.get("isRead")]
|
||||||
unread_ids=[c.get("id") for c in changes if c.get("id") and not c.get("isRead")]
|
if not read_ids:
|
||||||
touched=0
|
return 0
|
||||||
if read_ids:
|
result=await session.execute(
|
||||||
result=await session.execute(
|
update(cls).where(cls.message_id.in_(read_ids)).values(message_read=True)
|
||||||
update(cls).where(cls.message_id.in_(read_ids)).values(message_read=True)
|
)
|
||||||
)
|
|
||||||
touched+=result.rowcount or 0
|
|
||||||
if unread_ids:
|
|
||||||
result=await session.execute(
|
|
||||||
update(cls).where(cls.message_id.in_(unread_ids)).values(message_read=False)
|
|
||||||
)
|
|
||||||
touched+=result.rowcount or 0
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return touched
|
return result.rowcount or 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def mark_message_read(cls, session: AsyncSession, record_id):
|
async def mark_message_read(cls, session: AsyncSession, record_id):
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,15 @@ class Email:
|
||||||
self.token=token or EMAIL_API_TOKEN
|
self.token=token or EMAIL_API_TOKEN
|
||||||
self.pending_match_ids:list[str]=[]
|
self.pending_match_ids:list[str]=[]
|
||||||
|
|
||||||
async def get_all_applications(self,app_id=None):
|
# async def get_all_applications(self,app_id=None):
|
||||||
try:
|
# try:
|
||||||
if app_id:
|
# if app_id:
|
||||||
application_lst=await Inbox_Messages.get_all_applications(self.session,message_id=app_id)
|
# application_lst=await Inbox_Messages.get_all_applications(self.session,message_id=app_id)
|
||||||
else:
|
# else:
|
||||||
application_lst=await Inbox_Messages.get_all_applications(self.session)
|
# application_lst=await Inbox_Messages.get_all_applications(self.session)
|
||||||
return application_lst
|
# return application_lst
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
raise HTTPException(status_code=500,detail=str(e))
|
# raise HTTPException(status_code=500,detail=str(e))
|
||||||
|
|
||||||
async def service_email(self,top,skip):
|
async def service_email(self,top,skip):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -91,8 +91,11 @@ class Email:
|
||||||
item["files"]=files
|
item["files"]=files
|
||||||
return item
|
return item
|
||||||
|
|
||||||
async def get_all_applications(self,top,skip,search=None):
|
async def get_all_applications(self,top,skip,search=None,isread:bool=True):
|
||||||
messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search)
|
if isread==False:
|
||||||
|
messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search,isread)
|
||||||
|
else:
|
||||||
|
messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search)
|
||||||
return [serialize_application(m) for m in messages]
|
return [serialize_application(m) for m in messages]
|
||||||
|
|
||||||
async def get_application_by_id(self,record_id):
|
async def get_application_by_id(self,record_id):
|
||||||
|
|
@ -122,8 +125,11 @@ class Email:
|
||||||
task_ids.append(task.task_id)
|
task_ids.append(task.task_id)
|
||||||
return task_ids
|
return task_ids
|
||||||
|
|
||||||
async def count_inbox_messages(self,search=None):
|
async def count_inbox_messages(self,search=None,isread:bool=True):
|
||||||
return await Inbox_Messages.count_inbox_messages(self.session,search)
|
if isread==False:
|
||||||
|
return await Inbox_Messages.count_inbox_messages(self.session,search,isread=False)
|
||||||
|
else:
|
||||||
|
return await Inbox_Messages.count_inbox_messages(self.session,search)
|
||||||
|
|
||||||
async def mark_read(self,record_id):
|
async def mark_read(self,record_id):
|
||||||
message=await Inbox_Messages.mark_message_read(self.session,record_id)
|
message=await Inbox_Messages.mark_message_read(self.session,record_id)
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@ export function listMessages() {
|
||||||
* Unlike /inbox/fetch this one IS permissioned server-side
|
* Unlike /inbox/fetch this one IS permissioned server-side
|
||||||
* (require_permission(INBOX_VIEW)), so a caller without the tag gets a 403.
|
* (require_permission(INBOX_VIEW)), so a caller without the tag gets a 403.
|
||||||
*/
|
*/
|
||||||
export function listApplications({ search, top, skip, recordId } = {}) {
|
export function listApplications({ search, top, skip, recordId, isread } = {}) {
|
||||||
return request('/inbox/all-applications', {
|
return request('/inbox/all-applications', {
|
||||||
params: { search, top, skip, record_id: recordId },
|
// `isread` is tri-valued on the wire: omit it for every tab (server defaults
|
||||||
|
// to true = no filter), send false for the Unread tab only. buildUrl drops
|
||||||
|
// undefined but keeps false, so `isread: undefined` sends no param at all.
|
||||||
|
params: { search, top, skip, record_id: recordId, isread },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,80 @@ function SourceChip({ item }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /inbox/all-applications -> the shape the application tabs render.
|
||||||
|
*
|
||||||
|
* READ-ONLY: inbox_messages has no columns for processing state, duplicates,
|
||||||
|
* recruiter, phone, experience or an ATS score, so those arrive null and every
|
||||||
|
* mutating action on these tabs is disabled until the endpoints exist.
|
||||||
|
* `processing` is derived from message_read alone, which is why the Imported /
|
||||||
|
* Processed / Rejected / Duplicates tabs read empty.
|
||||||
|
*/
|
||||||
|
async function fetchApplications(params) {
|
||||||
|
const res = await inboxApi.listApplications(params)
|
||||||
|
const rows = Array.isArray(res?.data) ? res.data : []
|
||||||
|
return rows.map((row) => {
|
||||||
|
const name = row.name || row.email || 'Unknown'
|
||||||
|
return {
|
||||||
|
id: String(row.id),
|
||||||
|
name,
|
||||||
|
initials: initialsOf(name),
|
||||||
|
color: avatarColor(name),
|
||||||
|
email: row.email || '',
|
||||||
|
position: row.position || '(no subject)',
|
||||||
|
...sourceFrom(row.source),
|
||||||
|
received: parseDate(row.received),
|
||||||
|
unread: Boolean(row.unread),
|
||||||
|
processing: row.processing || 'Unread',
|
||||||
|
resumeStatus: row.resume_status || 'Pending',
|
||||||
|
attachment: row.attachment,
|
||||||
|
hasAttachment: Boolean(row.has_attachment),
|
||||||
|
resumeText: row.resume_text || '',
|
||||||
|
atsScore: row.ats_score,
|
||||||
|
phone: row.phone,
|
||||||
|
experience: row.experience,
|
||||||
|
recruiter: row.recruiter,
|
||||||
|
duplicate: Boolean(row.duplicate),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /inbox/{record_id}/read — flips message_read false -> true for one row.
|
||||||
|
*
|
||||||
|
* Optimistic, so the row un-bolds on click instead of after the round trip, and
|
||||||
|
* rolls back if the server rejects. Both mailbox caches hold {id, unread} rows,
|
||||||
|
* so one setQueriesData over qk.mailbox.all() covers the Email tab and the
|
||||||
|
* application tabs at once; `processing` is derived from the same column, so it
|
||||||
|
* moves with it.
|
||||||
|
*
|
||||||
|
* NOTE: the route requires INBOX_EDIT while the lists only require INBOX_VIEW,
|
||||||
|
* so a view-only user gets a 403 here and the row snaps back to unread.
|
||||||
|
*/
|
||||||
|
function useMarkRead(toast) {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (recordId) => inboxApi.markRead(recordId),
|
||||||
|
onMutate: async (recordId) => {
|
||||||
|
await qc.cancelQueries({ queryKey: qk.mailbox.all() })
|
||||||
|
const previous = qc.getQueriesData({ queryKey: qk.mailbox.all() })
|
||||||
|
qc.setQueriesData({ queryKey: qk.mailbox.all() }, (rows) => (
|
||||||
|
Array.isArray(rows)
|
||||||
|
? rows.map((r) => (r.id === recordId
|
||||||
|
? { ...r, unread: false, processing: r.processing === 'Unread' ? 'Read' : r.processing }
|
||||||
|
: r))
|
||||||
|
: rows
|
||||||
|
))
|
||||||
|
return { previous }
|
||||||
|
},
|
||||||
|
onError: (err, _recordId, ctx) => {
|
||||||
|
for (const [key, data] of ctx?.previous ?? []) qc.setQueryData(key, data)
|
||||||
|
toast(friendlyAuthError(err, 'Could not mark as read.'), 'error')
|
||||||
|
},
|
||||||
|
onSettled: () => qc.invalidateQueries({ queryKey: qk.mailbox.all() }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default function Inbox() {
|
export default function Inbox() {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -95,47 +169,30 @@ export default function Inbox() {
|
||||||
const [assigning, setAssigning] = useState(null)
|
const [assigning, setAssigning] = useState(null)
|
||||||
const [noting, setNoting] = useState(null)
|
const [noting, setNoting] = useState(null)
|
||||||
|
|
||||||
/**
|
// Only the Unread tab filters server-side; every other tab omits the param and
|
||||||
* GET /inbox/all-applications. READ-ONLY: inbox_messages has no columns for
|
// the backend's default (true) means "no filter".
|
||||||
* processing state, duplicates, recruiter, phone, experience or an ATS score,
|
const isread = tab === 'Unread' ? false : undefined
|
||||||
* so those arrive null and every mutating action on this tab is disabled until
|
|
||||||
* the endpoints exist. `processing` is derived from message_read alone, which
|
|
||||||
* is why the Imported / Processed / Rejected / Duplicates tabs read empty.
|
|
||||||
*/
|
|
||||||
const applicationsQuery = useQuery({
|
const applicationsQuery = useQuery({
|
||||||
queryKey: qk.mailbox.applications(),
|
queryKey: qk.mailbox.applications({ isread }),
|
||||||
queryFn: async () => {
|
queryFn: () => fetchApplications({ isread }),
|
||||||
const res = await inboxApi.listApplications()
|
enabled: tab !== 'Email',
|
||||||
const rows = Array.isArray(res?.data) ? res.data : []
|
})
|
||||||
return rows.map((row) => {
|
|
||||||
const name = row.name || row.email || 'Unknown'
|
/**
|
||||||
return {
|
* The tab badges need whole-table counts, which a server-filtered response
|
||||||
id: String(row.id),
|
* cannot give — and there is no counts endpoint. So the unfiltered set stays
|
||||||
name,
|
* loaded for them. On every tab except Unread this resolves to the SAME query
|
||||||
initials: initialsOf(name),
|
* key as the list above, so React Query serves both from one request.
|
||||||
color: avatarColor(name),
|
*/
|
||||||
email: row.email || '',
|
const countsQuery = useQuery({
|
||||||
position: row.position || '(no subject)',
|
queryKey: qk.mailbox.applications({ isread: undefined }),
|
||||||
...sourceFrom(row.source),
|
queryFn: () => fetchApplications({}),
|
||||||
received: parseDate(row.received),
|
|
||||||
unread: Boolean(row.unread),
|
|
||||||
processing: row.processing || 'Unread',
|
|
||||||
resumeStatus: row.resume_status || 'Pending',
|
|
||||||
attachment: row.attachment,
|
|
||||||
hasAttachment: Boolean(row.has_attachment),
|
|
||||||
resumeText: row.resume_text || '',
|
|
||||||
atsScore: row.ats_score,
|
|
||||||
phone: row.phone,
|
|
||||||
experience: row.experience,
|
|
||||||
recruiter: row.recruiter,
|
|
||||||
duplicate: Boolean(row.duplicate),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
enabled: tab !== 'Email',
|
enabled: tab !== 'Email',
|
||||||
})
|
})
|
||||||
|
|
||||||
const inbox = applicationsQuery.data ?? []
|
const inbox = applicationsQuery.data ?? []
|
||||||
|
const allApplications = countsQuery.data ?? []
|
||||||
|
|
||||||
const emailsQuery = useQuery({
|
const emailsQuery = useQuery({
|
||||||
queryKey: qk.mailbox.messages(),
|
queryKey: qk.mailbox.messages(),
|
||||||
|
|
@ -167,20 +224,25 @@ export default function Inbox() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const counts = useMemo(
|
const counts = useMemo(
|
||||||
|
// Counted off the UNFILTERED set — `inbox` is server-filtered on the Unread
|
||||||
|
// tab, so counting it there would report the unread total for every badge.
|
||||||
() => ({
|
() => ({
|
||||||
'All Applications': inbox.length,
|
'All Applications': allApplications.length,
|
||||||
Unread: inbox.filter((i) => i.processing === 'Unread').length,
|
Unread: allApplications.filter((i) => i.processing === 'Unread').length,
|
||||||
Imported: inbox.filter((i) => i.processing === 'Imported').length,
|
Imported: allApplications.filter((i) => i.processing === 'Imported').length,
|
||||||
Processed: inbox.filter((i) => i.processing === 'Processed').length,
|
Processed: allApplications.filter((i) => i.processing === 'Processed').length,
|
||||||
Rejected: inbox.filter((i) => i.processing === 'Rejected').length,
|
Rejected: allApplications.filter((i) => i.processing === 'Rejected').length,
|
||||||
Duplicates: inbox.filter((i) => i.duplicate).length,
|
Duplicates: allApplications.filter((i) => i.duplicate).length,
|
||||||
Email: (emailsQuery.data ?? []).filter((e) => e.unread).length,
|
Email: (emailsQuery.data ?? []).filter((e) => e.unread).length,
|
||||||
}),
|
}),
|
||||||
[inbox, emailsQuery.data],
|
[allApplications, emailsQuery.data],
|
||||||
)
|
)
|
||||||
|
|
||||||
const list = useMemo(() => {
|
const list = useMemo(() => {
|
||||||
let l = inbox
|
let l = inbox
|
||||||
|
// Unread is already filtered server-side; re-applying it client-side is what
|
||||||
|
// makes the optimistic mark-read drop the row from the list immediately
|
||||||
|
// instead of leaving it until the refetch lands.
|
||||||
if (tab === 'Unread') l = l.filter((i) => i.processing === 'Unread')
|
if (tab === 'Unread') l = l.filter((i) => i.processing === 'Unread')
|
||||||
else if (tab === 'Imported') l = l.filter((i) => i.processing === 'Imported')
|
else if (tab === 'Imported') l = l.filter((i) => i.processing === 'Imported')
|
||||||
else if (tab === 'Processed') l = l.filter((i) => i.processing === 'Processed')
|
else if (tab === 'Processed') l = l.filter((i) => i.processing === 'Processed')
|
||||||
|
|
@ -192,20 +254,14 @@ export default function Inbox() {
|
||||||
|
|
||||||
const selected = inbox.find((i) => i.id === selectedId)
|
const selected = inbox.find((i) => i.id === selectedId)
|
||||||
|
|
||||||
// The one mutation this tab CAN persist. Note it needs INBOX_EDIT while the
|
// The one mutation these tabs CAN persist — everything else on them is
|
||||||
// list only needs INBOX_VIEW, so a view-only user gets a 403 here.
|
// disabled until the endpoints exist.
|
||||||
const markApplicationRead = useMutation({
|
const markRead = useMarkRead(toast)
|
||||||
mutationFn: (recordId) => inboxApi.markRead(recordId),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: qk.mailbox.all() })
|
|
||||||
},
|
|
||||||
onError: (err) => toast(friendlyAuthError(err, 'Could not mark application read.'), 'error'),
|
|
||||||
})
|
|
||||||
|
|
||||||
function select(id) {
|
function select(id) {
|
||||||
setSelectedId(id)
|
setSelectedId(id)
|
||||||
const item = inbox.find((i) => i.id === id)
|
const item = inbox.find((i) => i.id === id)
|
||||||
if (item?.unread) markApplicationRead.mutate(id)
|
if (item?.unread) markRead.mutate(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCandidate(item, job, cs) {
|
function makeCandidate(item, job, cs) {
|
||||||
|
|
@ -571,13 +627,7 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
const selected = emails.find((e) => e.id === selectedId)
|
const selected = emails.find((e) => e.id === selectedId)
|
||||||
const unread = emails.filter((e) => e.unread).length
|
const unread = emails.filter((e) => e.unread).length
|
||||||
|
|
||||||
const markRead = useMutation({
|
const markRead = useMarkRead(toast)
|
||||||
mutationFn: (recordId) => inboxApi.markRead(recordId),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: qk.mailbox.all() })
|
|
||||||
},
|
|
||||||
onError: (err) => toast(friendlyAuthError(err, 'Could not mark message read.'), 'error'),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Refetching the list alone only re-reads rows already in our DB. GET
|
// Refetching the list alone only re-reads rows already in our DB. GET
|
||||||
// /email/fetch is the Graph proxy pull that inserts new mail and enqueues the
|
// /email/fetch is the Graph proxy pull that inserts new mail and enqueues the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue