From 2eeef60adb4665812aea5743afc1db92de42ad70 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Fri, 5 Sep 2025 01:41:40 +0200 Subject: [PATCH] fix link to proposed record --- app/change-request/[uri]/page.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/change-request/[uri]/page.tsx b/app/change-request/[uri]/page.tsx index 455cdd3..99f5d12 100644 --- a/app/change-request/[uri]/page.tsx +++ b/app/change-request/[uri]/page.tsx @@ -169,15 +169,20 @@ export default function ChangeRequestPage() {

Proposed Changes

- - View Proposed Record - - + {(() => { + // Parse the proposed record URI to extract the DID + const proposedUri = changeRequest.proposedRecord + const proposedDid = proposedUri.replace('at://', '').split('/')[0] + return ( + + View Proposed Changes + + + ) + })()}
-- 2.43.0 From 7ce65059954e09b90a795aa66c4caac0d14bf15a Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Fri, 5 Sep 2025 01:46:16 +0200 Subject: [PATCH] implement Accept Changes button --- app/api/change-request/accept/route.ts | 185 +++++++++++++++++++++++++ app/change-request/[uri]/page.tsx | 110 ++++++++++++--- 2 files changed, 277 insertions(+), 18 deletions(-) create mode 100644 app/api/change-request/accept/route.ts diff --git a/app/api/change-request/accept/route.ts b/app/api/change-request/accept/route.ts new file mode 100644 index 0000000..5028eaa --- /dev/null +++ b/app/api/change-request/accept/route.ts @@ -0,0 +1,185 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/session' +import { getGlobalOAuthClient } from '@/lib/auth/client' +import { Agent } from '@atproto/api' +import { getPdsEndpoint } from '@atproto/common-web' +import { IdResolver } from '@atproto/identity' +import { TID } from '@atproto/common' + +async function getSessionAgent(): Promise<{ agent: Agent; oauthSession: any } | null> { + try { + const session = await getSession() + + if (!session.did) { + return null + } + + const client = await getGlobalOAuthClient() + const oauthSession = await client.restore(session.did) + + if (!oauthSession) { + console.log('OAuth session restoration failed') + return null + } + + const agent = new Agent(oauthSession) + return { agent, oauthSession } + } catch (error) { + console.error('Session restore failed:', error) + return null + } +} + +// POST - Accept a change request and apply the proposed changes +export async function POST(request: NextRequest) { + try { + console.log('POST /api/change-request/accept - Starting request') + + const sessionResult = await getSessionAgent() + if (!sessionResult) { + console.log('No agent available - authentication required') + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + const { agent, oauthSession } = sessionResult + const body = await request.json() + const { changeRequestUri } = body + + if (!changeRequestUri) { + return NextResponse.json({ error: 'changeRequestUri required' }, { status: 400 }) + } + + // Parse the change request URI to get DID and rkey + const uriParts = changeRequestUri.replace('at://', '').split('/') + const requesterDid = uriParts[0] + const changeRequestRkey = uriParts[2] + + console.log('Fetching change request:', { requesterDid, changeRequestRkey }) + + // Resolve requester's DID to get their PDS + const resolver = new IdResolver() + const requesterDidDoc = await resolver.did.resolve(requesterDid) + + if (!requesterDidDoc) { + return NextResponse.json({ error: 'Could not resolve requester DID' }, { status: 404 }) + } + + const requesterPdsEndpoint = getPdsEndpoint(requesterDidDoc) + if (!requesterPdsEndpoint) { + return NextResponse.json({ error: 'No PDS endpoint found for requester' }, { status: 404 }) + } + + // Create agent for requester's PDS to fetch the change request + const requesterAgent = new Agent({ service: requesterPdsEndpoint }) + + // Fetch the change request record + const changeRequestResponse = await requesterAgent.com.atproto.repo.getRecord({ + repo: requesterDid, + collection: 'org.impactindexer.changeRequest', + rkey: changeRequestRkey + }) + + if (!changeRequestResponse.success) { + return NextResponse.json({ error: 'Change request not found' }, { status: 404 }) + } + + const changeRequestData = changeRequestResponse.data.value as any + + // Verify that the current user is the target (project owner) + if (changeRequestData.targetDid !== agent.assertDid) { + return NextResponse.json({ + error: 'Unauthorized - you can only accept change requests for your own projects' + }, { status: 403 }) + } + + console.log('Fetching proposed record data...') + + // Fetch the proposed record data + const proposedUri = changeRequestData.proposedRecord + const proposedUriParts = proposedUri.replace('at://', '').split('/') + const proposedRkey = proposedUriParts[2] + + const proposedResponse = await requesterAgent.com.atproto.repo.getRecord({ + repo: requesterDid, + collection: 'org.impactindexer.status', + rkey: proposedRkey + }) + + if (!proposedResponse.success) { + return NextResponse.json({ error: 'Proposed record not found' }, { status: 404 }) + } + + const proposedData = proposedResponse.data.value as any + + console.log('Applying proposed changes to project owner record...') + + // Check if the project owner has an existing record + let existingRecord = null + let rkey = TID.nextStr() + + try { + const listResponse = await agent.com.atproto.repo.listRecords({ + repo: agent.assertDid, + collection: 'org.impactindexer.status', + limit: 1 + }) + + if (listResponse.data.records.length > 0) { + existingRecord = listResponse.data.records[0] + rkey = existingRecord.uri.split('/').pop() || TID.nextStr() + } + } catch (error) { + console.log('No existing record found, will create new one') + } + + // Create the updated record with proposed data, preserving timestamps appropriately + const now = new Date().toISOString() + const updatedRecord = { + $type: 'org.impactindexer.status', + displayName: proposedData.displayName, + description: proposedData.description, + website: proposedData.website, + fundingReceived: proposedData.fundingReceived, + fundingGivenOut: proposedData.fundingGivenOut, + annualBudget: proposedData.annualBudget, + teamSize: proposedData.teamSize, + sustainableRevenuePercent: proposedData.sustainableRevenuePercent, + categories: proposedData.categories, + impactMetrics: proposedData.impactMetrics, + geographicDistribution: proposedData.geographicDistribution, + createdAt: (existingRecord?.value as any)?.createdAt || now, + updatedAt: now, + } + + console.log('Final record to save:', updatedRecord) + + // Remove undefined fields + Object.keys(updatedRecord).forEach(key => { + if ((updatedRecord as any)[key] === undefined) { + delete (updatedRecord as any)[key] + } + }) + + // Save the updated record to the project owner's repository + const response = await agent.com.atproto.repo.putRecord({ + repo: agent.assertDid, + collection: 'org.impactindexer.status', + rkey, + record: updatedRecord, + validate: false, + }) + + console.log('Successfully updated project owner record') + + return NextResponse.json({ + success: true, + updatedRecordUri: response.data.uri, + updatedRecordCid: response.data.cid, + appliedChanges: updatedRecord + }) + + } catch (error) { + console.error('Accept change request failed:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/change-request/[uri]/page.tsx b/app/change-request/[uri]/page.tsx index 99f5d12..da1f3ed 100644 --- a/app/change-request/[uri]/page.tsx +++ b/app/change-request/[uri]/page.tsx @@ -26,6 +26,42 @@ export default function ChangeRequestPage() { const [changeRequest, setChangeRequest] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [accepting, setAccepting] = useState(false) + const [acceptSuccess, setAcceptSuccess] = useState(false) + const [acceptError, setAcceptError] = useState(null) + + const acceptChanges = async () => { + if (!changeRequest || accepting) return + + setAccepting(true) + setAcceptError(null) + + try { + const response = await fetch('/api/change-request/accept', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + changeRequestUri: uri + }) + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to accept changes') + } + + const result = await response.json() + console.log('Changes accepted successfully:', result) + setAcceptSuccess(true) + } catch (error) { + console.error('Error accepting changes:', error) + setAcceptError(error instanceof Error ? error.message : 'Failed to accept changes') + } finally { + setAccepting(false) + } + } useEffect(() => { const fetchChangeRequest = async () => { @@ -223,26 +259,64 @@ export default function ChangeRequestPage() { )} - {/* Future: Action buttons for project owners */} + {/* Action buttons for project owners */}

Project Owner Actions

-

- As the project owner, you can review the proposed changes and decide whether to accept them. -

-
- - -
+ + {acceptSuccess ? ( +
+

+ ✅ Changes Accepted Successfully! +

+

+ The proposed changes have been applied to your project. Your project information has been updated. +

+ + View Updated Project + + +
+ ) : ( + <> +

+ As the project owner, you can review the proposed changes and decide whether to accept them. +

+ + {acceptError && ( +
+

+ Error Accepting Changes +

+

+ {acceptError} +

+
+ )} + +
+ + +
+ + )}
) -- 2.43.0 From 7e4df6c0d0e887ae7b5209de5111bc4fc08f0344 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Fri, 5 Sep 2025 01:46:57 +0200 Subject: [PATCH] fix diff of Categories and lists in general --- app/change-request/[uri]/page.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/change-request/[uri]/page.tsx b/app/change-request/[uri]/page.tsx index da1f3ed..a7a1c20 100644 --- a/app/change-request/[uri]/page.tsx +++ b/app/change-request/[uri]/page.tsx @@ -235,7 +235,20 @@ export default function ChangeRequestPage() { const originalValue = changeRequest.originalData?.[key] const proposedValue = changeRequest.proposedData[key] - if (originalValue === proposedValue) return null + // Deep comparison for arrays and objects + const valuesAreEqual = () => { + if (originalValue === proposedValue) return true + if (Array.isArray(originalValue) && Array.isArray(proposedValue)) { + return originalValue.length === proposedValue.length && + originalValue.every((item, index) => item === proposedValue[index]) + } + if (typeof originalValue === 'object' && typeof proposedValue === 'object') { + return JSON.stringify(originalValue) === JSON.stringify(proposedValue) + } + return false + } + + if (valuesAreEqual()) return null return (
-- 2.43.0