From 6b3a8664914ae479fe4d419b22340cd11f0d8de7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Dec 2024 22:19:55 +0100 Subject: [PATCH] Feature/1011 infra feeds (#1015) * feat: fetch from devhub-cache-api-rs.fly.dev * @Megha-Dev-19 WIP * wip * fmt * wip * events and devhub are ready to be reviewed * feat: infra proposals * fmt * feat: rfps infra * remove comments * fix: spelling * fix: spelling * replace all nearqueryapi in devhub related to proposals and rfps * devhub: simplemde, acceptedTerms, passing instance * fix: devhub * refactor events: deleted SimpleMDE and LinkedProposalsDropdown for both * test: replace all references of queryapi in tests * test: fix linkedProposals and simpleMDE test :) * test: skip discussions test for now * clean up SimpleMDE * infra: SimpleMDE, LinkedDropdown rfp + proposal, Proposal + Rfp.jsx, remove fetchgraphql from common * test: fix events test, 1. had to deploy events with new cors policy, 2. passing instance down to simplemde, 3. mock the test on the right api path. * test: infra -- fix: should show correct linked RFP to a proposal in feed page * test: infra -- fix: should create proposal and link an RFP * remove comments * test: @petersalomonsen fixed! * fmt * test: discussions test back in * test: skip discussions test * revert: changes to rfp comment test * initial commit 1002 * fmt * test for comparing local feed with production * add events committee feed components + by-sort component * fmt * compare links in prod and local * test: update events test * add events committee feed components + by-sort component * fmt * test: update events test * test: comment spec * test: included some test from pr 982 * revert commit * feat: simpleMDE to new api * fmt * feat: linkedproposaldropdown to new api * fmt * test: proposal autolink * fix: simplemde + test * linked dropdowns * feature: update feeds with new api * fmt --------- Co-authored-by: Peter Salomonsen --- .../widget/components/proposals/Feed.jsx | 297 +++++++----------- .../widget/components/rfps/Feed.jsx | 165 ++++------ .../tests/infrastructure/proposal.spec.js | 19 +- 3 files changed, 195 insertions(+), 286 deletions(-) diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx index 5e9dfe196..8ca54446e 100644 --- a/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx +++ b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx @@ -10,7 +10,7 @@ const { proposalFeedAnnouncement, availableCategoryOptions, proposalFeedIndexerQueryName, - indexerHasuraRole, + cacheUrl, isDevhub, isInfra, isEvents, @@ -193,8 +193,7 @@ const FeedItem = ({ proposal, index }) => {
@@ -269,14 +268,18 @@ const getProposal = (proposal_id) => { }); }; -const FeedPage = () => { - const QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql`; +const getRfp = (rfp_id) => { + return Near.asyncView(contract, "get_rfp", { + rfp_id, + }); +}; +const FeedPage = () => { State.init({ data: [], author: "", stage: "", - sort: "", + sort: "id_desc", category: "", input: "", loading: false, @@ -286,114 +289,6 @@ const FeedPage = () => { currentlyDisplaying: 0, }); - const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 20, $where: ${proposalFeedIndexerQueryName}_bool_exp = {}) { - ${proposalFeedIndexerQueryName}( - offset: $offset - limit: $limit - order_by: {proposal_id: desc} - where: $where - ) { - author_id - block_height - name - category - summary - editor_id - proposal_id - ts - timeline - views - labels - linked_rfp - } - ${proposalFeedIndexerQueryName}_aggregate( - order_by: {proposal_id: desc} - where: $where - ) { - aggregate { - count - } - } - }`; - - const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 20, $where: ${rfpFeedIndexerQueryName}_bool_exp = {}) { - ${rfpFeedIndexerQueryName}( - offset: $offset - limit: $limit - order_by: {rfp_id: desc} - where: $where - ) { - name - rfp_id - } - }`; - - function fetchGraphQL(operationsDoc, operationName, variables) { - return asyncFetch(QUERYAPI_ENDPOINT, { - method: "POST", - headers: { "x-hasura-role": indexerHasuraRole }, - body: JSON.stringify({ - query: operationsDoc, - variables: variables, - operationName: operationName, - }), - }); - } - - function separateNumberAndText(str) { - const numberRegex = /\d+/; - - if (numberRegex.test(str)) { - const number = str.match(numberRegex)[0]; - const text = str.replace(numberRegex, "").trim(); - return { number: parseInt(number), text }; - } else { - return { number: null, text: str.trim() }; - } - } - - const buildWhereClause = () => { - let where = {}; - if (state.author) { - where = { author_id: { _eq: state.author }, ...where }; - } - - if (state.category) { - if (isInfra || isEvents) { - where = { labels: { _contains: state.category }, ...where }; - } else { - where = { category: { _eq: state.category }, ...where }; - } - } - - if (state.stage) { - // timeline is stored as jsonb - where = { - timeline: { _cast: { String: { _regex: `${state.stage}` } } }, - ...where, - }; - } - if (state.input) { - const { number, text } = separateNumberAndText(state.input); - if (number) { - where = { proposal_id: { _eq: number }, ...where }; - } - - if (text) { - where = { - _or: [ - { name: { _iregex: `${text}` } }, - { summary: { _iregex: `${text}` } }, - { description: { _iregex: `${text}` } }, - ], - ...where, - }; - } - } - - return where; - }; - const makeMoreItems = () => { State.update({ makeMoreLoader: true }); fetchProposals(state.data.length); @@ -406,7 +301,69 @@ const FeedPage = () => { REJECTED: 1, }; - const fetchProposals = (offset) => { + function searchCacheApi(searchTerm) { + let searchInput = encodeURI(searchTerm); + let searchUrl = `${cacheUrl}/proposals/search/${searchInput}`; + + return asyncFetch(searchUrl, { + method: "GET", + headers: { + accept: "application/json", + }, + }).catch((error) => { + console.log("Error searching cache api", error); + }); + } + + function searchProposals(input) { + if (state.loading) return; + State.update({ loading: true }); + + searchCacheApi(input).then((result) => { + const body = result.body; + const promises = body.records.map((proposal) => { + if (isNumber(proposal.linked_rfp)) { + return getRfp(proposal.linked_rfp).then((rfp) => { + return { ...proposal, rfpData: rfp }; + }); + } else { + return Promise.resolve(proposal); + } + }); + Promise.all(promises).then((proposalsWithRfpData) => { + State.update({ aggregatedCount: body.total_records }); + fetchBlockHeights(proposalsWithRfpData, 0); + }); + }); + } + + function fetchCacheApi(variables) { + let fetchUrl = `${cacheUrl}/proposals?order=${variables.order}&limit=${variables.limit}&offset=${variables.offset}`; + + if (variables.author_id) { + fetchUrl += `&filters.author_id=${variables.author_id}`; + } + if (variables.stage) { + fetchUrl += `&filters.stage=${variables.stage}`; + } + if (variables.category) { + if (isInfra || isEvents) { + fetchUrl += `&filters.labels=${variables.category}`; + } else { + fetchUrl += `&filters.category=${variables.category}`; + } + } + return asyncFetch(fetchUrl, { + method: "GET", + headers: { + accept: "application/json", + }, + }).catch((error) => { + console.log("Error fetching cache api", error); + }); + } + + function fetchProposals(offset) { if (!offset) { offset = 0; } @@ -414,36 +371,30 @@ const FeedPage = () => { State.update({ loading: true }); const FETCH_LIMIT = 20; const variables = { + order: state.sort, limit: FETCH_LIMIT, offset, - where: buildWhereClause(), + category: state.category ? encodeURIComponent(state.category) : "", + author_id: state.author ? encodeURIComponent(state.author) : "", + stage: state.stage ? encodeURIComponent(state.stage) : "", }; - fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => { - if (result.status === 200) { - if (result.body.data) { - const data = result.body.data[proposalFeedIndexerQueryName]; - const totalResult = - result.body.data[`${proposalFeedIndexerQueryName}_aggregate`]; - const promises = data.map((item) => { - if (isNumber(item.linked_rfp)) { - return fetchGraphQL(rfpQuery, "GetLatestSnapshot", { - where: { rfp_id: { _eq: item.linked_rfp } }, - }).then((result) => { - const rfpData = result.body.data?.[rfpFeedIndexerQueryName]; - return { ...item, rfpData: rfpData[0] }; - }); - } else { - return Promise.resolve(item); - } - }); - Promise.all(promises).then((res) => { - State.update({ aggregatedCount: totalResult.aggregate.count }); - fetchBlockHeights(res, offset); + fetchCacheApi(variables).then((result) => { + const body = result.body; + const promises = body.records.map((proposal) => { + if (isNumber(proposal.linked_rfp)) { + return getRfp(proposal.linked_rfp).then((rfp) => { + return { ...proposal, rfpData: rfp }; }); + } else { + return Promise.resolve(proposal); } - } + }); + Promise.all(promises).then((proposalsWithRfpData) => { + State.update({ aggregatedCount: body.total_records }); + fetchBlockHeights(proposalsWithRfpData, offset); + }); }); - }; + } useEffect(() => { State.update({ searchLoader: true }); @@ -455,12 +406,11 @@ const FeedPage = () => { ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))), ].map((i) => JSON.parse(i)); // Sorting in the front end - if (state.sort === "proposal_id" || state.sort === "") { + if (state.sort === "id_desc" || state.sort === "") { items.sort((a, b) => b.proposal_id - a.proposal_id); - } else if (state.sort === "views") { - items.sort((a, b) => b.views - a.views); } + // Show the accepted once before showing rejected proposals items.sort((a, b) => { return statusOrder[a.timeline.status] - statusOrder[b.timeline.status]; }); @@ -469,43 +419,40 @@ const FeedPage = () => { }; const fetchBlockHeights = (data, offset) => { - let promises = data.map((item) => getProposal(item.proposal_id)); - Promise.all(promises).then((blockHeights) => { - data = data.map((item, index) => ({ - ...item, - timeline: JSON.parse(item.timeline), - social_db_post_block_height: - blockHeights[index].social_db_post_block_height, - })); - if (offset) { - let newData = mergeItems(data); - State.update({ - data: newData, - currentlyDisplaying: newData.length, - loading: false, - searchLoader: false, - makeMoreLoader: false, - }); - } else { - let sorted = [...data].sort((a, b) => { - return ( - statusOrder[a.timeline.status] - statusOrder[b.timeline.status] - ); - }); - State.update({ - data: sorted, - currentlyDisplaying: data.length, - loading: false, - searchLoader: false, - makeMoreLoader: false, - }); - } - }); + data = data.map((item, index) => ({ + ...item, + timeline: JSON.parse(item.timeline), + })); + if (offset) { + let newData = mergeItems(data); + State.update({ + data: newData, + currentlyDisplaying: newData.length, + loading: false, + searchLoader: false, + makeMoreLoader: false, + }); + } else { + let sorted = [...data].sort((a, b) => { + return statusOrder[a.timeline.status] - statusOrder[b.timeline.status]; + }); + State.update({ + data: sorted, + currentlyDisplaying: data.length, + loading: false, + searchLoader: false, + makeMoreLoader: false, + }); + } }; useEffect(() => { const handler = setTimeout(() => { - fetchProposals(); + if (state.input) { + searchProposals(state.input); + } else { + fetchProposals(); + } }, 1000); return () => { diff --git a/instances/infrastructure-committee.near/widget/components/rfps/Feed.jsx b/instances/infrastructure-committee.near/widget/components/rfps/Feed.jsx index 9b82d5d1e..2cc9af7f7 100644 --- a/instances/infrastructure-committee.near/widget/components/rfps/Feed.jsx +++ b/instances/infrastructure-committee.near/widget/components/rfps/Feed.jsx @@ -1,7 +1,3 @@ -const { fetchGraphQL } = VM.require( - `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common` -); - const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); href || (href = () => {}); @@ -110,8 +106,6 @@ const Heading = styled.div` const rfpLabelOptions = getGlobalLabels(); const FeedItem = ({ rfp, index }) => { - const accountId = rfp.author_id; - const profile = Social.get(`${accountId}/profile/**`, "final"); // We will have to get the rfp from the contract to get the block height. const blockHeight = parseInt(rfp.social_db_post_block_height); const item = { @@ -169,7 +163,7 @@ const FeedItem = ({ rfp, index }) => { className="d-flex flex-column gap-1" style={{ maxWidth: "70%" }} > -
Summay
+
Summary
{rfp.summary}
@@ -233,7 +227,7 @@ const FeedPage = () => { data: [], cachedItems: {}, stage: "", - sort: "", + sort: "id_desc", label: "", input: "", loading: false, @@ -243,91 +237,56 @@ const FeedPage = () => { isFiltered: false, }); - const queryName = "${REPL_RFP_FEED_INDEXER_QUERY_NAME}"; - - const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) { - ${queryName}( - offset: $offset - limit: $limit - order_by: {rfp_id: desc} - where: $where - ) { - author_id - block_height - name - summary - editor_id - rfp_id - timeline - views - labels - submission_deadline - linked_proposals - } - ${queryName}_aggregate( - order_by: {rfp_id: desc} - where: $where - ) { - aggregate { - count + function searchCacheApi() { + return asyncFetch( + `${REPL_CACHE_URL}/rfps/search/${encodeURI(state.input)}`, + { + method: "GET", + headers: { + accept: "application/json", + }, } - } - }`; + ).catch((error) => { + console.log("Error searching cache api", error); + }); + } - function separateNumberAndText(str) { - const numberRegex = /\d+/; + function searchRfps() { + if (state.loading) return; + State.update({ loading: true }); - if (numberRegex.test(str)) { - const number = str.match(numberRegex)[0]; - const text = str.replace(numberRegex, "").trim(); - return { number: parseInt(number), text }; - } else { - return { number: null, text: str.trim() }; - } + searchCacheApi().then((result) => { + let body = result.body; + State.update({ aggregatedCount: body.total_records }); + fetchBlockHeights(body.records, 0); + }); } - const buildWhereClause = () => { - let where = {}; - - if (state.label) { - where = { labels: { _contains: state.label }, ...where }; - } + function fetchCacheApi(variables) { + let fetchUrl = `${REPL_CACHE_URL}/rfps?order=${variables.order}&limit=${variables.limit}&offset=${variables.offset}`; - if (state.stage) { - // timeline is stored as jsonb - where = { - timeline: { _cast: { String: { _regex: `${state.stage}` } } }, - ...where, - }; + if (variables.stage) { + fetchUrl += `&filters.stage=${variables.stage}`; } - if (state.input) { - const { number, text } = separateNumberAndText(state.input); - if (number) { - where = { rfp_id: { _eq: number }, ...where }; - } - - if (text) { - where = { - _or: [ - { name: { _iregex: `${text}` } }, - { summary: { _iregex: `${text}` } }, - { description: { _iregex: `${text}` } }, - ], - ...where, - }; + if (variables.category) { + if (isInfra || isEvents) { + fetchUrl += `&filters.labels=${variables.category}`; + } else { + fetchUrl += `&filters.category=${variables.category}`; } } - State.update({ isFiltered: Object.keys(where).length > 0 }); - return where; - }; - const buildOrderByClause = () => { - /** - * TODO - * Most commented -> edit contract and indexer - * Unanswered -> 0 comments - */ - }; + State.update({ isFiltered: variables.category || variables.stage }); + + return asyncFetch(fetchUrl, { + method: "GET", + headers: { + accept: "application/json", + }, + }).catch((error) => { + console.log("Error fetching cache api", error); + }); + } const makeMoreItems = () => { if (state.aggregatedCount <= state.currentlyDisplaying) return; @@ -339,25 +298,21 @@ const FeedPage = () => { offset = 0; } if (state.loading) return; + State.update({ loading: true }); + const FETCH_LIMIT = 10; const variables = { + order: state.sort, limit: FETCH_LIMIT, offset, - where: buildWhereClause(), + category: state.category ? encodeURIComponent(state.category) : "", + stage: state.stage ? encodeURIComponent(state.stage) : "", }; - if (typeof fetchGraphQL !== "function") { - return; - } - fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => { - if (result.status === 200) { - if (result.body.data) { - const data = result.body.data?.[queryName]; - const totalResult = result.body.data?.[`${queryName}_aggregate`]; - State.update({ aggregatedCount: totalResult.aggregate.count }); - // Parse timeline - fetchBlockHeights(data, offset); - } - } + + fetchCacheApi(variables).then((result) => { + const body = result.body; + State.update({ aggregatedCount: body.total_records }); + fetchBlockHeights(body.records, offset); }); }; @@ -390,7 +345,21 @@ const FeedPage = () => { useEffect(() => { fetchRfps(); - }, [state.input, state.sort, state.label, state.stage]); + }, [state.sort, state.label, state.stage]); + + useEffect(() => { + const handler = setTimeout(() => { + if (state.input) { + searchRfps(); + } else { + fetchRfps(); + } + }, 1000); + + return () => { + clearTimeout(handler); + }; + }, [state.input]); const mergeItems = (newItems) => { const items = [ diff --git a/playwright-tests/tests/infrastructure/proposal.spec.js b/playwright-tests/tests/infrastructure/proposal.spec.js index 2efc2c451..5a8920622 100644 --- a/playwright-tests/tests/infrastructure/proposal.spec.js +++ b/playwright-tests/tests/infrastructure/proposal.spec.js @@ -28,22 +28,14 @@ test.describe("Wallet is connected as admin", () => { await page.goto("/infrastructure-committee.near/widget/app?page=proposals"); let proposalId; const linkedRfpId = 0; - // add linked RFP to latest proposal await page.route( - "https://near-queryapi.api.pagoda.co/v1/graphql", + "https://infra-cache-api-rs.fly.dev/proposals?order=id_desc&limit=20&offset=0", async (route) => { const response = await route.fetch(); const json = await response.json(); - if ( - json?.data?.[ - "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot" - ] - ) { - json.data[ - "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot" - ] = json.data[ - "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot" - ].map((i, index) => { + + if (json?.records) { + json.records = json.records.map((i, index) => { if (index === 0) { proposalId = i.proposal_id; return { @@ -84,7 +76,8 @@ test.describe("Wallet is connected as admin", () => { await pauseIfVideoRecording(page); if (linkRfp) { await page.getByText("Search RFP").click(); - await page.getByPlaceholder("Search by Id").fill("0"); + let input = page.getByPlaceholder("Search by Id"); + await input.fill("0", { delay: 100 }); await page.getByText("# 0 : A Cool RFP").click(); await expect( await page.getByRole("link", { name: "# 0 : A Cool RFP" })