Repository navigation
Add award RFPs #1271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add award RFPs #1271
Changes from all commits
1774179
9180108
dc35eaa
1e227bf
8a56ed1
bcb9b1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { useUser } from '@/contexts/UserContext'; | |
| import { useAgentChatList, type UseAgentChatListResult } from '@/hooks/useAgentChat'; | ||
| import { useChatSession, type ChatSession } from '@/hooks/useChatSession'; | ||
| import { getChatTransport } from '@/services/chatTransport'; | ||
| import { GrantService } from '@/services/grant.service'; | ||
| import type { AgentChat, ChatNoteRef } from '@/types/agentChat'; | ||
| import type { SelectedGrantDetails } from '@/types/grant'; | ||
| import type { Note } from '@/types/note'; | ||
|
|
@@ -118,6 +119,25 @@ export function useAIModeChat(): AIModeChatState { | |
| const intent = urlIntent ?? rememberedIntent; | ||
| const [selectedGrant, setSelectedGrant] = useState<SelectedGrantDetails | null>(null); | ||
|
|
||
| // An RFP page's Apply leads here with the RFP named (`?rfp=<id>`): it starts | ||
| // out picked, as if chosen on this screen, and can still be changed. The | ||
| // RFP is read signed in, so a visitor who logs in here gets it then. | ||
| const { user, isLoading: userLoading } = useUser(); | ||
| const signedInUserId = user?.id ?? null; | ||
| const urlGrantId = target.kind === 'new' ? (target.grantId ?? null) : null; | ||
| useEffect(() => { | ||
| if (urlGrantId == null || signedInUserId == null) return; | ||
| let cancelled = false; | ||
| GrantService.getSelectedGrant(urlGrantId) | ||
| .then((grant) => { | ||
| if (!cancelled) setSelectedGrant(grant); | ||
| }) | ||
| .catch((error) => console.error('Error loading the RFP to apply to:', error)); | ||
|
Comment on lines
+128
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait for the URL-selected RFP before starting a draft. If a user starts a blank draft or sends the first message before
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [urlGrantId, signedInUserId]); | ||
|
|
||
| // ---- the first message of a new draft ---- | ||
| // It creates the document, then a chat on it, then goes out there. A note | ||
| // created by an attempt whose chat then failed is kept here, so a retry | ||
|
|
@@ -307,7 +327,6 @@ export function useAIModeChat(): AIModeChatState { | |
| // ---- a signed-out visitor ---- | ||
| // What would start a draft asks them to log in, and keeps what they typed | ||
| // (or the suggestion they picked) in the box for when they are back. | ||
| const { user, isLoading: userLoading } = useUser(); | ||
| const { showAuthModal } = useAuthModalContext(); | ||
| const signedOut = user == null && !userLoading; | ||
| const { setDraft } = session; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { isAwardMechanism } from '@/components/Funding/lib/fundingMechanism'; | ||
| import { buildWorkUrl } from '@/utils/url'; | ||
| import { isFoundationUser } from '@/components/Bounty/lib/bountyUtil'; | ||
| import type { CurrencyAmount } from '@/utils/currency'; | ||
|
|
@@ -148,8 +149,11 @@ | |
| } | ||
|
|
||
| if (entry.contentType === 'USDFUNDRAISECONTRIBUTION' || entry.contentType === 'PURCHASE') { | ||
| return isFundingPoolContribution(entry) | ||
| ? { actor, verb: 'contributed', suffix: ' to the funding pool' } | ||
| if (isFundingPoolContribution(entry)) { | ||
| return { actor, verb: 'contributed', suffix: ' to the funding pool' }; | ||
| } | ||
| return isAwardMechanism(entry.relatedWork?.fundingMechanism) | ||
| ? { actor, verb: 'awarded this proposal with' } | ||
|
Comment on lines
+155
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| : { actor, verb: 'funded this proposal with' }; | ||
| } | ||
|
|
||
|
|
@@ -570,10 +574,14 @@ | |
| return { label: 'Contributed to a bounty' }; | ||
| } | ||
| if (entry.contentType === 'USDFUNDRAISECONTRIBUTION' || entry.contentType === 'PURCHASE') { | ||
| const awardedProposal = | ||
| !isFundingPoolContribution(entry) && isAwardMechanism(entry.relatedWork?.fundingMechanism); | ||
| return { | ||
| label: isFundingPoolContribution(entry) | ||
| ? 'Added to an RFP’s funding pool' | ||
| : 'Funded a proposal', | ||
| : awardedProposal | ||
| ? 'Awarded a proposal' | ||
| : 'Funded a proposal', | ||
|
Check warning on line 584 in components/Activity/lib/activityDisplay.utils.ts
|
||
| value: contributionValue(getContribution(entry)), | ||
| }; | ||
| } | ||
|
|
@@ -582,7 +590,14 @@ | |
| return { label: 'Opened an RFP', value: amount ? { kind: 'grant', amount } : undefined }; | ||
| } | ||
| if (entry.contentType === 'PREREGISTRATION') { | ||
| return { label: 'Submitted a proposal for funding' }; | ||
| const mechanism = | ||
| entry.relatedWork?.fundingMechanism ?? | ||
| (entry.associatedGrants?.some((grant) => isAwardMechanism(grant.fundingMechanism)) | ||
| ? 'AWARD' | ||
| : undefined); | ||
| if (mechanism === 'AWARD') return { label: 'Submitted a proposal for an award' }; | ||
| if (mechanism === 'FUNDING') return { label: 'Submitted a proposal for funding' }; | ||
| return { label: 'Submitted a proposal' }; | ||
| } | ||
| if (entry.contentType === 'PAPER') { | ||
| return { label: 'Published a preprint' }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| } from './activityDisplay.utils'; | ||
| import { formatCurrency } from '@/utils/currency'; | ||
| import { toOptionalNumber } from '@/utils/number'; | ||
| import { awardRaisedLabel, isAwardMechanism } from '@/components/Funding/lib/fundingMechanism'; | ||
| import { getGrantBadgeAmount, type FundingPool } from '@/types/grant'; | ||
| import type { | ||
| ActivityAction, | ||
|
|
@@ -151,6 +152,21 @@ | |
| }); | ||
| } | ||
|
|
||
| export function getAwardCardStat( | ||
| fundraise: { status?: string; amountRaised?: { usd?: number; rsc?: number } }, | ||
| showUSD: boolean, | ||
| exchangeRate: number | ||
| ): WorkCardStat | null { | ||
| const label = awardRaisedLabel(fundraise); | ||
| if (!label) return null; | ||
| const raised = (showUSD ? fundraise.amountRaised?.usd : fundraise.amountRaised?.rsc) ?? 0; | ||
| return { | ||
| label, | ||
| value: formatAmount(raised, showUSD, exchangeRate, true), | ||
|
Comment on lines
+162
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| accent: true, | ||
| }; | ||
| } | ||
|
|
||
| function resolveReviewScore(entry: FeedEntry): number | null { | ||
| const entryScore = entry.metrics?.reviewScore; | ||
| return entryScore && entryScore > 0 ? entryScore : null; | ||
|
|
@@ -206,15 +222,43 @@ | |
| }; | ||
| } | ||
|
|
||
| /** The entry is a proposal to an award RFP: the funder awards it, so it asks for nothing. */ | ||
| export function isAwardFeedEntry(entry: FeedEntry): boolean { | ||
| if (isAwardMechanism(entry.relatedWork?.fundingMechanism)) return true; | ||
| return entry.associatedGrants?.some((grant) => isAwardMechanism(grant.fundingMechanism)) ?? false; | ||
| } | ||
|
|
||
| function isAwardFundraise( | ||
| fundraise: NonNullable<ActivityWork['fundraise']>, | ||
| entry?: FeedEntry | ||
| ): boolean { | ||
| if (entry != null && isAwardFeedEntry(entry)) return true; | ||
| const goalUsd = fundraise.goalAmount?.usd; | ||
| const goalRsc = fundraise.goalAmount?.rsc; | ||
| if (goalUsd == null && goalRsc == null) return false; | ||
| return (goalUsd ?? 0) <= 0 && (goalRsc ?? 0) <= 0; | ||
| } | ||
|
|
||
| function presentFundraise( | ||
| base: WorkCardPresentation, | ||
| fundraise: NonNullable<ActivityWork['fundraise']>, | ||
| showUSD: boolean, | ||
| exchangeRate: number | ||
| exchangeRate: number, | ||
| entry: FeedEntry | ||
| ): WorkCardPresentation { | ||
| const goalUsd = fundraise.goalAmount?.usd ?? 0; | ||
| const goalRsc = fundraise.goalAmount?.rsc ?? 0; | ||
| const raisedUsd = fundraise.amountRaised?.usd ?? 0; | ||
|
|
||
| if (isAwardFundraise(fundraise, entry)) { | ||
| const stat = getAwardCardStat(fundraise, showUSD, exchangeRate); | ||
| return { | ||
| ...base, | ||
| stats: stat ? [stat] : base.stats, | ||
| progress: undefined, | ||
| }; | ||
| } | ||
|
|
||
| const goalAmount = showUSD ? goalUsd : goalRsc; | ||
|
|
||
| return { | ||
|
|
@@ -307,7 +351,7 @@ | |
| const base = buildBasePresentation(entry, work, slot, { isReview }); | ||
|
|
||
| if (slot === 'fundraise' && work.fundraise) { | ||
| return presentFundraise(base, work.fundraise, showUSD, exchangeRate); | ||
| return presentFundraise(base, work.fundraise, showUSD, exchangeRate, entry); | ||
| } | ||
| if (slot === 'grant' && work.grant) { | ||
| return presentGrant(base, work.grant, showUSD, exchangeRate); | ||
|
|
@@ -385,7 +429,7 @@ | |
| return Math.min(1, (work.fundraise.amountRaised?.usd ?? 0) / goal); | ||
| } | ||
|
|
||
| function getSlotStat( | ||
|
Check failure on line 432 in components/Activity/lib/activityWork.utils.ts
|
||
| slot: ActivityBodySlot, | ||
| work: ActivityWork, | ||
| showUSD: boolean, | ||
|
|
@@ -393,6 +437,10 @@ | |
| ): WorkCoverStat | null { | ||
| if (slot === 'fundraise' && work.fundraise) { | ||
| const { goalAmount } = work.fundraise; | ||
| if (isAwardFundraise(work.fundraise)) { | ||
| const stat = getAwardCardStat(work.fundraise, showUSD, exchangeRate); | ||
| return stat ? { label: stat.label, value: stat.value } : null; | ||
| } | ||
| const goal = (showUSD ? goalAmount?.usd : goalAmount?.rsc) ?? 0; | ||
| return goal > 0 | ||
| ? { label: 'Raising', value: formatAmount(goal, showUSD, exchangeRate, true) } | ||
|
|
@@ -538,7 +586,7 @@ | |
| : link('View RFP'); | ||
| } | ||
|
|
||
| if (openFundraise) { | ||
| if (openFundraise && !isAwardFundraise(openFundraise, entry)) { | ||
| return { | ||
| kind: 'fund-proposal', | ||
| label: 'Fund proposal', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear the selection when the workspace target no longer names this RFP.
If the user opens another draft through
useFundingDrafting.startNew, the mounted workspace keeps the previousselectedGrant. This effect returns when the new target has nograntId, so the next proposal can be attached to the previous RFP. Reset or scope the selection when the target changes. (raw.githubusercontent.com)