diff --git a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx index a5ed91d5a..90a237d84 100644 --- a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx +++ b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx @@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false); const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); -const { hasModerator } = - VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract") || - (() => {}); +const { hasModerator } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +) || { hasModerator: () => {} }; linkHref || (linkHref = () => {}); diff --git a/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx b/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx index d5e3e37c9..330e4aaaf 100644 --- a/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx +++ b/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx @@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false); const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); -const { hasModerator } = - VM.require("${REPL_EVENTS}/widget/core.adapter.devhub-contract") || - (() => {}); +const { hasModerator } = VM.require( + "${REPL_EVENTS}/widget/core.adapter.devhub-contract" +) || { hasModerator: () => {} }; linkHref || (linkHref = () => {}); diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx new file mode 100644 index 000000000..5e9dfe196 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx @@ -0,0 +1,678 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || { + href: () => {}, +}; + +const instance = props.instance ?? ""; + +const { + contract, + rfpFeedIndexerQueryName, + proposalFeedAnnouncement, + availableCategoryOptions, + proposalFeedIndexerQueryName, + indexerHasuraRole, + isDevhub, + isInfra, + isEvents, +} = VM.require(`${instance}/widget/config.data`); + +const loader = ( +
+ +
+); + +if (!contract) { + return loader; +} + +function isNumber(v) { + return typeof v === "number"; +} + +const Container = styled.div` + .full-width-div { + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + } + + .card.no-border { + border-left: none !important; + border-right: none !important; + margin-bottom: -3.5rem; + } + + @media screen and (max-width: 768px) { + font-size: 13px; + } + + .text-sm { + font-size: 13px; + } + + .bg-grey { + background-color: #f4f4f4; + } + + .border-bottom { + border-bottom: 1px solid grey; + } + + .cursor-pointer { + cursor: pointer; + } + + .proposal-card { + border-left: none !important; + border-right: none !important; + border-bottom: none !important; + &:hover { + background-color: #f4f4f4; + } + } + + @media screen and (max-width: 768px) { + .theme-btn { + padding: 0.5rem 0.8rem !important; + min-height: 32px; + } + } + + a.no-space { + display: inline-block; + } + + .text-wrap { + overflow: hidden; + white-space: normal; + } + + .text-center { + text-align: center; + } + + .btn-grey-outline { + background-color: #fafafa; + border: 1px solid #e6e8eb; + color: #11181c; + + &:hover { + background-color: #e6e8eb; + } + + &:active { + border: 2px solid #e6e8eb; + } + } +`; + +const Heading = styled.div` + font-size: 24px; + font-weight: 700; + width: 100%; + + .text-normal { + font-weight: normal !important; + } + + @media screen and (max-width: 768px) { + font-size: 18px; + } +`; + +const FeedItem = ({ proposal, index }) => { + const accountId = proposal.author_id; + const profile = Social.get(`${accountId}/profile/**`, "final"); + // We will have to get the proposal from the contract to get the block height. + const blockHeight = parseInt(proposal.social_db_post_block_height); + const item = { + type: "social", + path: `${contract}/post/main`, + blockHeight: blockHeight, + }; + + const isLinked = isNumber(proposal.linked_rfp); + const rfpData = proposal.rfpData; + + return ( + e.stopPropagation()} + style={{ textDecoration: "none" }} + > +
+
+ +
+
+
{proposal.name}
+ {(isInfra || isEvents) && ( + {}, + availableOptions: availableCategoryOptions, + }} + /> + )} + {isDevhub && ( + + )} +
+ {isLinked && rfpData && ( +
+ + In response to RFP : + + {rfpData.name} + +
+ )} +
+
#{proposal.proposal_id} ・
+
+ By {profile.name ?? accountId} ・{" "} +
+ +
+
+ + + {}, + }} + /> +
+
+
+
+ +
+
+ + ); +}; + +const getProposal = (proposal_id) => { + return Near.asyncView(contract, "get_proposal", { + proposal_id, + }); +}; + +const FeedPage = () => { + const QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql`; + + State.init({ + data: [], + author: "", + stage: "", + sort: "", + category: "", + input: "", + loading: false, + searchLoader: false, + makeMoreLoader: false, + aggregatedCount: null, + 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); + }; + + const statusOrder = { + APPROVED: -1, + REVIEW: 0, + CANCELLED: 1, + REJECTED: 1, + }; + + const fetchProposals = (offset) => { + if (!offset) { + offset = 0; + } + if (state.loading) return; + State.update({ loading: true }); + const FETCH_LIMIT = 20; + const variables = { + limit: FETCH_LIMIT, + offset, + where: buildWhereClause(), + }; + 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); + }); + } + } + }); + }; + + useEffect(() => { + State.update({ searchLoader: true }); + fetchProposals(); + }, [state.author, state.sort, state.category, state.stage]); + + const mergeItems = (newItems) => { + const items = [ + ...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 === "") { + items.sort((a, b) => b.proposal_id - a.proposal_id); + } else if (state.sort === "views") { + items.sort((a, b) => b.views - a.views); + } + + items.sort((a, b) => { + return statusOrder[a.timeline.status] - statusOrder[b.timeline.status]; + }); + + return items; + }; + + 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, + }); + } + }); + }; + + useEffect(() => { + const handler = setTimeout(() => { + fetchProposals(); + }, 1000); + + return () => { + clearTimeout(handler); + }; + }, [state.input]); + + return ( + +
+ + Proposals + + ({state.aggregatedCount ?? state.data.length}){" "} + + +
+ { + State.update({ input }); + }, + onEnter: () => { + fetchProposals(); + }, + }} + /> + { + State.update({ sort: select.value }); + }, + }} + /> +
+ {!isInfra && ( + { + State.update({ category: select.value }); + }, + }} + /> + )} + { + State.update({ stage: select.value }); + }, + }} + /> + { + State.update({ author: select.value }); + }, + }} + /> +
+
+
+ + +
+ +
+ Submit Proposal +
+ ), + classNames: { root: "theme-btn" }, + }} + /> + +
+ +
+ {!Array.isArray(state.data) ? ( + loader + ) : ( +
+
+ {proposalFeedAnnouncement} +
+ {state.aggregatedCount === 0 ? ( + + ) : state.searchLoader ? ( + loader + ) : state.aggregatedCount > 0 ? ( + state.data.map((item, index) => { + return ( +
+ +
+ ); + }) + ) : ( + loader + )} +
+ {state.aggregatedCount > 0 && + state.aggregatedCount > state.data.length && ( +
+ {state.makeMoreLoader ? ( + loader + ) : ( +
+ {!state.loading && ( +
+ makeMoreItems(), + }} + /> +
+ )} +
+ )} +
+ )} +
+
+ )} +
+
+ ); +}; + +return FeedPage(props); diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx index eb97875f5..5691d05c4 100644 --- a/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx +++ b/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx @@ -1,6 +1,6 @@ return (