From f36d34613f9312e1c6440fc97872d32f0013c917 Mon Sep 17 00:00:00 2001 From: T guntenaar Date: Tue, 9 Jul 2024 16:33:54 +0200 Subject: [PATCH 1/4] Show categories blog page (#864) * fix: show categories blog page * remove description * test: check for categories on the main blog page --- .../devhub/entity/addon/blogv2/Page.jsx | 2 -- .../devhub.near/widget/devhub/page/blogv2.jsx | 11 +++++++- .../widget/devhub/page/community/index.jsx | 25 ------------------- playwright-tests/tests/blog/blogv2.spec.js | 9 +++++++ 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/instances/devhub.near/widget/devhub/entity/addon/blogv2/Page.jsx b/instances/devhub.near/widget/devhub/entity/addon/blogv2/Page.jsx index 21b69ae4d..e8b2bef03 100644 --- a/instances/devhub.near/widget/devhub/entity/addon/blogv2/Page.jsx +++ b/instances/devhub.near/widget/devhub/entity/addon/blogv2/Page.jsx @@ -16,7 +16,6 @@ const { data, onEdit, community: handle, isAllowedToEdit } = props; const { category, title, - description, subtitle, publishedAt: date, content, @@ -193,7 +192,6 @@ return ( )}
{formattedDate}
-

{description}

{}, + getCommunity: () => {}, }; const [showEditScreenData, setShowEditScreen] = useState(null); @@ -127,6 +128,13 @@ if (showEditScreenData) { ); } + +const communityObject = getCommunity({ handle: "developer-dao" }); +const blogv2 = (communityObject.addons || []).find( + (addon) => addon.id === "blogv2" +); +const config = JSON.parse(blogv2.parameters || {}) || {}; + return (
@@ -140,6 +148,7 @@ return ( handle: "developer-dao", hideTitle: true, communityAddonId: "blogv2", + data: config, }} /> diff --git a/instances/devhub.near/widget/devhub/page/community/index.jsx b/instances/devhub.near/widget/devhub/page/community/index.jsx index 3d7fd93e9..b4d3a0764 100644 --- a/instances/devhub.near/widget/devhub/page/community/index.jsx +++ b/instances/devhub.near/widget/devhub/page/community/index.jsx @@ -56,31 +56,6 @@ const [isLinkCopied, setLinkCopied] = useState(false); const [addonView, setAddonView] = useState("viewer"); -// CommunityAddOn -const blogv2 = { - addon_id: "blogv2", - display_name: "BlogV2", - enabled: true, - id: "blogv2", - parameters: - '{"title":"My blog page title",\ - "subtitle":"Classic subtitle",\ - "auhtorEnabled": "enabled",\ - "searchEnabled": "enabled",\ - "orderBy": "timedesc",\ - "categoriesEnabled": "enabled",\ - "categories": ["news", "guide", "reference"],\ - "categoryRequired": false}', -}; - -const blogv2instance2 = { - addon_id: "blogv2", - display_name: "BlogV2", - enabled: true, - id: "blogv2instance2", - parameters: "{}", -}; - const tabs = []; (community.addons || []).map((addon) => { diff --git a/playwright-tests/tests/blog/blogv2.spec.js b/playwright-tests/tests/blog/blogv2.spec.js index 4441151a8..063a313ef 100644 --- a/playwright-tests/tests/blog/blogv2.spec.js +++ b/playwright-tests/tests/blog/blogv2.spec.js @@ -112,6 +112,15 @@ test.describe("Wallet is not connected", () => { expect(span1.isVisible()).toBeTruthy(); expect(span2.isVisible()).toBeTruthy(); }); + + test("should show developer-dao blog via navigation", async ({ page }) => { + await page.goto("/devhub.near/widget/app?page=blogv2"); + await page.waitForSelector(`[data-testid^="card-category"]`); + + const categories = await page.$$(`[data-testid^="card-category"]`); + + expect(categories.length).toBeGreaterThan(0); + }); }); test.describe("Don't ask again enabled", () => { From 78bdeeb044af3b3956bc6133553fded5356436d2 Mon Sep 17 00:00:00 2001 From: Megha <100185149+Megha-Dev-19@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:04:37 +0530 Subject: [PATCH 2/4] fix timeline change to "Payment Processing" (#865) --- .../devhub.near/widget/devhub/entity/proposal/Proposal.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instances/devhub.near/widget/devhub/entity/proposal/Proposal.jsx b/instances/devhub.near/widget/devhub/entity/proposal/Proposal.jsx index fb6fdfb11..ab9656275 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/Proposal.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/Proposal.jsx @@ -391,6 +391,7 @@ const proposalStatusOptions = [ value: { status: TIMELINE_STATUS.PAYMENT_PROCESSING, kyc_verified_review: true, + kyc_verified: true, test_transaction_sent: false, request_for_trustees_created: false, sponsor_requested_review: true, @@ -403,6 +404,7 @@ const proposalStatusOptions = [ status: TIMELINE_STATUS.FUNDED, trustees_released_payment: true, kyc_verified_review: true, + kyc_verified: true, test_transaction_sent: true, request_for_trustees_created: true, sponsor_requested_review: true, From 12b2b06944dfc4c8d91d80892916b0a69d2c9431 Mon Sep 17 00:00:00 2001 From: Megha <100185149+Megha-Dev-19@users.noreply.github.com> Date: Wed, 10 Jul 2024 00:48:20 +0530 Subject: [PATCH 3/4] fix log vertical alignment (#866) --- .../widget/devhub/entity/proposal/CommentsAndLogs.jsx | 5 ++++- .../devhub.near/widget/devhub/entity/proposal/Profile.jsx | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx index 8f980ebf4..4ce7a83a6 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx @@ -446,7 +446,10 @@ const Log = ({ timestamp }) => { : "inline-flex") } > - + {parseProposalKeyAndValue(i.key, i.modifiedValue, i.originalValue)} diff --git a/instances/devhub.near/widget/devhub/entity/proposal/Profile.jsx b/instances/devhub.near/widget/devhub/entity/proposal/Profile.jsx index 64ede1f5e..ee5069de8 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/Profile.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/Profile.jsx @@ -3,10 +3,10 @@ const size = props.size ?? "md"; const showAccountId = props.showAccountId; const Avatar = styled.div` &.sm { - min-width: 30px; - max-width: 30px; - min-height: 30px; - max-height: 30px; + min-width: 26px; + max-width: 26px; + min-height: 26px; + max-height: 26px; } &.md { min-width: 40px; From bd8dbd067ffe9c425acd816256e8c3aa5d5f5e7b Mon Sep 17 00:00:00 2001 From: Megha <100185149+Megha-Dev-19@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:05:00 +0530 Subject: [PATCH 4/4] Add infrastructure-committee (#857) * fix solution action * fix preview for proposals * added infrastructure-committee code * added tests * update permissions * added infrastructure-committee code * added tests * update permissions * fix tests * refactor code * add instance env * update scripts * add access keys for RFP don't ask again test with infrastructure-committee.near * fix infra tests * fix date --------- Co-authored-by: Peter Salomonsen --- .../continuous-integration-workflow-infra.yml | 40 + .../continuous-integration-workflow.yml | 10 +- ...net.yml => deploy-prod-mainnet-devhub.yml} | 2 +- .../workflows/deploy-prod-mainnet-infra.yml | 49 + .../infrastructure-committee.near/.gitignore | 2 + .../aliases.mainnet.json | 12 + .../bos.config.json | 6 + .../infrastructure-committee.near/data.json | 3 + instances/infrastructure-committee.near/src | 1 + .../widget/app.jsx | 129 ++ .../components/admin/AboutConfigurator.jsx | 185 +++ .../components/admin/AccountsEditor.jsx | 79 + .../admin/ModeratorsConfigurator.jsx | 116 ++ .../widget/components/core/lib/contract.jsx | 24 + .../components/molecule/AccountInput.jsx | 75 + .../widget/components/molecule/Compose.jsx | 118 ++ .../components/molecule/ComposeComment.jsx | 265 +++ .../widget/components/molecule/DropDown.jsx | 66 + .../molecule/DropDownWithSearch.jsx | 193 +++ .../components/molecule/FilterByLabel.jsx | 22 + .../widget/components/molecule/LikeButton.jsx | 148 ++ .../components/molecule/LinkedProposals.jsx | 78 + .../molecule/LinkedProposalsDropdown.jsx | 155 ++ .../components/molecule/LinkedRfpDropdown.jsx | 185 +++ .../widget/components/molecule/LinkedRfps.jsx | 61 + .../widget/components/molecule/Markdown.jsx | 38 + .../molecule/MultiSelectCategoryDropdown.jsx | 193 +++ .../components/molecule/NavbarDropdown.jsx | 129 ++ .../components/molecule/RadioButton.jsx | 29 + .../widget/components/molecule/SimpleMDE.jsx | 629 ++++++++ .../widget/components/organism/Navbar.jsx | 259 +++ .../widget/components/pages/about.jsx | 27 + .../widget/components/pages/admin.jsx | 173 ++ .../components/proposals/CommentsAndLogs.jsx | 509 ++++++ .../widget/components/proposals/Editor.jsx | 1318 +++++++++++++++ .../widget/components/proposals/Feed.jsx | 609 +++++++ .../widget/components/proposals/Proposal.jsx | 1424 +++++++++++++++++ .../proposals/ViewProposalModal.jsx | 161 ++ .../components/rfps/CommentsAndLogs.jsx | 473 ++++++ .../components/rfps/ConfirmCancelModal.jsx | 250 +++ .../widget/components/rfps/Editor.jsx | 881 ++++++++++ .../widget/components/rfps/Feed.jsx | 589 +++++++ .../widget/components/rfps/Rfp.jsx | 864 ++++++++++ .../widget/components/rfps/StageDropdown.jsx | 30 + .../widget/components/rfps/StatusTag.jsx | 66 + .../components/rfps/TimelineConfigurator.jsx | 245 +++ .../widget/components/rfps/ViewRfpModal.jsx | 160 ++ .../widget/components/rfps/WarningModal.jsx | 144 ++ .../widget/components/template/AppLayout.jsx | 67 + .../widget/core/common.jsx | 79 + package.json | 3 + ...llet-connected-with-devhub-access-key.json | 8 + .../infrastructure.near.spec.js | 54 + .../tests/infrastructure/proposal.spec.js | 84 + .../tests/infrastructure/rfp.spec.js | 326 ++++ playwright-tests/util/transaction.js | 13 + playwright.config.js | 4 +- 57 files changed, 11854 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/continuous-integration-workflow-infra.yml rename .github/workflows/{deploy-prod-mainnet.yml => deploy-prod-mainnet-devhub.yml} (98%) create mode 100644 .github/workflows/deploy-prod-mainnet-infra.yml create mode 100644 instances/infrastructure-committee.near/.gitignore create mode 100644 instances/infrastructure-committee.near/aliases.mainnet.json create mode 100644 instances/infrastructure-committee.near/bos.config.json create mode 100644 instances/infrastructure-committee.near/data.json create mode 120000 instances/infrastructure-committee.near/src create mode 100644 instances/infrastructure-committee.near/widget/app.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/organism/Navbar.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/pages/about.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/pages/admin.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/proposals/CommentsAndLogs.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/proposals/Editor.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/proposals/Proposal.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/proposals/ViewProposalModal.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/CommentsAndLogs.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/ConfirmCancelModal.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/Editor.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/Feed.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/Rfp.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/StageDropdown.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/StatusTag.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/TimelineConfigurator.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/ViewRfpModal.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/rfps/WarningModal.jsx create mode 100644 instances/infrastructure-committee.near/widget/components/template/AppLayout.jsx create mode 100644 instances/infrastructure-committee.near/widget/core/common.jsx create mode 100644 playwright-tests/tests/infrastructure/infrastructure.near.spec.js create mode 100644 playwright-tests/tests/infrastructure/proposal.spec.js create mode 100644 playwright-tests/tests/infrastructure/rfp.spec.js diff --git a/.github/workflows/continuous-integration-workflow-infra.yml b/.github/workflows/continuous-integration-workflow-infra.yml new file mode 100644 index 000000000..652d2812a --- /dev/null +++ b/.github/workflows/continuous-integration-workflow-infra.yml @@ -0,0 +1,40 @@ +name: CI - Infrastructure Committee +on: + pull_request: + push: + branches: + - main + +jobs: + prettier: + name: Prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "npm" + - name: Install dependencies + run: npm ci + - name: Run code formatting check + run: npm run fmt:check + + playwright-tests-infra: + name: Infrastructure Committee - Playwright tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "npm" + - name: Install dependencies + run: | + npm ci + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mpeterdev/bos-loader/releases/download/v0.7.1/bos-loader-v0.7.1-installer.sh | sh + npx playwright install-deps + npx playwright install + - name: Run tests + run: | + INSTANCE=infrastructure npx playwright test --project=infrastructure playwright-tests/tests/infrastructure diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index c6c455609..ae977c082 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -36,7 +36,7 @@ jobs: npx playwright install - name: Run tests run: | - npx playwright test playwright-tests/tests/blog + INSTANCE=devhub npx playwright test playwright-tests/tests/blog playwright-tests-community: name: Community - Playwright tests runs-on: ubuntu-latest @@ -54,7 +54,7 @@ jobs: npx playwright install - name: Run tests run: | - npx playwright test playwright-tests/tests/community + INSTANCE=devhub npx playwright test playwright-tests/tests/community playwright-tests-other: name: Other - Playwright tests runs-on: ubuntu-latest @@ -72,7 +72,7 @@ jobs: npx playwright install - name: Run tests run: | - npx playwright test playwright-tests/tests/other + INSTANCE=devhub npx playwright test playwright-tests/tests/other playwright-tests-proposal: name: Proposal - Playwright tests runs-on: ubuntu-latest @@ -90,7 +90,7 @@ jobs: npx playwright install - name: Run tests run: | - npx playwright test playwright-tests/tests/proposal + INSTANCE=devhub npx playwright test playwright-tests/tests/proposal playwright-tests-sunset: name: Sunset - Playwright tests runs-on: ubuntu-latest @@ -108,4 +108,4 @@ jobs: npx playwright install - name: Run tests run: | - npx playwright test playwright-tests/tests/sunset + INSTANCE=devhub npx playwright test playwright-tests/tests/sunset diff --git a/.github/workflows/deploy-prod-mainnet.yml b/.github/workflows/deploy-prod-mainnet-devhub.yml similarity index 98% rename from .github/workflows/deploy-prod-mainnet.yml rename to .github/workflows/deploy-prod-mainnet-devhub.yml index 21e996dbe..e437fb9d8 100644 --- a/.github/workflows/deploy-prod-mainnet.yml +++ b/.github/workflows/deploy-prod-mainnet-devhub.yml @@ -1,4 +1,4 @@ -name: Deploy Widgets to Mainnet +name: Deploy Widgets to Mainnet - Devhub on: pull_request: push: diff --git a/.github/workflows/deploy-prod-mainnet-infra.yml b/.github/workflows/deploy-prod-mainnet-infra.yml new file mode 100644 index 000000000..3651a6a42 --- /dev/null +++ b/.github/workflows/deploy-prod-mainnet-infra.yml @@ -0,0 +1,49 @@ +name: Deploy Widgets to Mainnet - Infrastructure +on: + pull_request: + push: + branches: [main] +jobs: + deploy-widgets: + runs-on: ubuntu-latest + name: Deploy ( or diff from PR ) + env: + NEAR_SOCIAL_DEPLOY_ID: ${{ vars.NEAR_INFRA_SOCIAL_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_ID: ${{ vars.NEAR_INFRA_SOCIAL_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY: ${{ vars.NEAR_INFRA_SOCIAL_ACCOUNT_PUBLIC_KEY }} + NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY: ${{ secrets.NEAR_INFRA_SOCIAL_ACCOUNT_PRIVATE_KEY }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + defaults: + run: + working-directory: ./instances/infrastructure-committee.near + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set replacements + id: set_replacements + run: | + echo "replacements=$(jq -r '[to_entries[] | .["find"] = "${" + .key + "}" | .["replace"] = .value | del(.key, .value), {"find": "${REPL_POSTHOG_API_KEY}", "replace": "'$POSTHOG_API_KEY'"}]' aliases.mainnet.json | tr -d "\n\r")" >> $GITHUB_OUTPUT + + - name: Replace placeholders + uses: flcdrg/replace-multiple-action@v1 + with: + files: "**/*.jsx" + find: "${{ steps.set_replacements.outputs.replacements }}" + prefix: "(^|.*)" + suffix: "($|.*)" + + - name: Install bos CLI + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/FroVolod/bos-cli-rs/releases/download/v0.3.6/bos-cli-installer.sh | sh + + - name: Deploy widgets + run: | + BRANCH="$(git rev-parse --abbrev-ref HEAD)" + echo "on branch $BRANCH" + if [[ "$BRANCH" != "main" ]]; then + echo "Not on main branch, dry run by diff with infrastructure-committee.near" + bos components diff infrastructure-committee.near network-config mainnet + else + bos components deploy "$NEAR_INFRA_SOCIAL_DEPLOY_ID" sign-as "$NEAR_INFRA_SOCIAL_ACCOUNT_ID" network-config mainnet sign-with-plaintext-private-key --signer-public-key "$NEAR_INFRA_SOCIAL_ACCOUNT_PUBLIC_KEY" --signer-private-key "$NEAR_INFRA_SOCIAL_ACCOUNT_PRIVATE_KEY" send + fi diff --git a/instances/infrastructure-committee.near/.gitignore b/instances/infrastructure-committee.near/.gitignore new file mode 100644 index 000000000..9d0b71a3c --- /dev/null +++ b/instances/infrastructure-committee.near/.gitignore @@ -0,0 +1,2 @@ +build +dist diff --git a/instances/infrastructure-committee.near/aliases.mainnet.json b/instances/infrastructure-committee.near/aliases.mainnet.json new file mode 100644 index 000000000..a26f38ef0 --- /dev/null +++ b/instances/infrastructure-committee.near/aliases.mainnet.json @@ -0,0 +1,12 @@ +{ + "REPL_DEVHUB": "devhub.near", + "REPL_INFRASTRUCTURE_COMMITTEE": "infrastructure-committee.near", + "REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT": "infrastructure-committee.near", + "REPL_NEAR": "near", + "REPL_RPC_URL": "https://rpc.mainnet.near.org", + "REPL_RFP_IMAGE": "https://ipfs.near.social/ipfs/bafkreicbygt4kajytlxij24jj6tkg2ppc2dw3dlqhkermkjjfgdfnlizzy", + "REPL_RFP_FEED_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_rfps_with_latest_snapshot", + "REPL_RFP_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_rfp_snapshots", + "REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot", + "REPL_PROPOSAL_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_proposal_snapshots" +} diff --git a/instances/infrastructure-committee.near/bos.config.json b/instances/infrastructure-committee.near/bos.config.json new file mode 100644 index 000000000..cdab1132b --- /dev/null +++ b/instances/infrastructure-committee.near/bos.config.json @@ -0,0 +1,6 @@ +{ + "account": "infrastructure-committee.near", + "aliasPrefix": "REPL", + "aliasesContainsPrefix": true, + "aliases": ["./aliases.mainnet.json"] +} diff --git a/instances/infrastructure-committee.near/data.json b/instances/infrastructure-committee.near/data.json new file mode 100644 index 000000000..b3569fc6c --- /dev/null +++ b/instances/infrastructure-committee.near/data.json @@ -0,0 +1,3 @@ +{ + "infrastructure-committee.near": {} +} diff --git a/instances/infrastructure-committee.near/src b/instances/infrastructure-committee.near/src new file mode 120000 index 000000000..0301008ca --- /dev/null +++ b/instances/infrastructure-committee.near/src @@ -0,0 +1 @@ +widget \ No newline at end of file diff --git a/instances/infrastructure-committee.near/widget/app.jsx b/instances/infrastructure-committee.near/widget/app.jsx new file mode 100644 index 000000000..a07dce898 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/app.jsx @@ -0,0 +1,129 @@ +/** + * This is the main entry point for the RFP application. + * Page route gets passed in through params, along with all other page props. + */ + +const { page, ...passProps } = props; + +// Import our modules +const { AppLayout } = VM.require( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/components.template.AppLayout` +); + +if (!AppLayout) { + return

Loading modules...

; +} + +// CSS styles to be used across the app. +// Define fonts here, as well as any other global styles. +const Theme = styled.div` + a { + color: inherit; + } + + .attractable { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + + &:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + } + } +`; + +if (!page) { + // If no page is specified, we default to the feed page TEMP + page = "about"; +} + +// This is our navigation, rendering the page based on the page parameter +function Page() { + const routes = page.split("."); + switch (routes[0]) { + case "about": { + return ( + + ); + } + case "rfps": { + return ( + + ); + } + case "rfp": { + return ( + + ); + } + case "create-rfp": { + return ( + + ); + } + case "create-proposal": { + return ( + + ); + } + + case "proposals": { + return ( + + ); + } + case "proposal": { + return ( + + ); + } + case "about": { + return ( + + ); + } + case "admin": { + return ( + + ); + } + default: { + // TODO: 404 page + return

404

; + } + } +} + +return ( + + + + + +); diff --git a/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx b/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx new file mode 100644 index 000000000..5d8fbe526 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx @@ -0,0 +1,185 @@ +const { Tile } = VM.require( + `${REPL_DEVHUB}/widget/devhub.components.molecule.Tile` +) || { Tile: () => <> }; + +const item = { + path: `${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}/profile/**`, +}; + +const profile = Social.get(item.path); + +if (!profile.description) { +
+ +
; +} + +const initialData = profile.description; +const [content, setContent] = useState(null); +const [showCommentToast, setCommentToast] = useState(false); +const [handler, setHandler] = useState(null); +const [isTxnCreated, setTxnCreated] = useState(false); + +const Container = styled.div` + width: 100%; + margin: 0 auto; + padding: 20px; + text-align: left; +`; + +const hasDataChanged = () => { + return content !== initialData; +}; + +const handlePublish = () => { + setTxnCreated(true); + Near.call([ + { + contractName: "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}", + methodName: "set_social_db_profile_description", + args: { description: content }, + gas: 270000000000000, + }, + ]); +}; + +useEffect(() => { + if (isTxnCreated) { + const checkForAboutInSocialDB = () => { + Near.asyncView(REPL_SOCIAL_CONTRACT, "get", { + keys: [item.path], + }).then((result) => { + try { + const submittedAboutText = content; + const lastAboutTextFromSocialDB = + result["${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}"].profile + .description; + if (submittedAboutText === lastAboutTextFromSocialDB) { + setTxnCreated(false); + setCommentToast(true); + return; + } + } catch (e) {} + setTimeout(() => checkForAboutInSocialDB(), 2000); + }); + }; + checkForAboutInSocialDB(); + } +}, [isTxnCreated]); + +useEffect(() => { + if (!content && initialData) { + setContent(initialData); + setHandler("update"); + } +}, [initialData]); + +function Preview() { + return ( + + + + ); +} + +return ( + + setCommentToast(v), + trigger: <>, + providerProps: { duration: 3000 }, + }} + /> +
    +
  • + +
  • +
  • + +
  • +
+
+
+ { + setContent(v); + }, + showAutoComplete: true, + }} + /> + +
+ +
+
+
+
+ +
+
+
+
+); diff --git a/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx b/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx new file mode 100644 index 000000000..ebe769ca1 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx @@ -0,0 +1,79 @@ +const { data, setList, validate, invalidate } = props; + +const [newItem, setNewItem] = useState(""); + +const handleAddItem = () => { + if (validate(newItem)) { + setList([...data.list, newItem]); + setNewItem(""); + } else { + return invalidate(); + } +}; + +const handleDeleteItem = (index) => { + const updatedData = [...data.list]; + updatedData.splice(index, 1); + setList(updatedData); +}; + +const Item = styled.div` + padding: 10px; + margin: 5px; + display: flex; + align-items: center; + flex-direction: row; + gap: 10px; +`; + +return ( + <> + {data.list.map((item, index) => ( + +
+ +
+ +
+ ))} + {data.list.length < data.maxLength && ( + +
+ setNewItem(value), + value: newItem, + placeholder: data.placeholder, + }} + /> +
+ +
+ )} + +); diff --git a/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx b/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx new file mode 100644 index 000000000..22aad5644 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx @@ -0,0 +1,116 @@ +const { Tile } = VM.require( + `${REPL_DEVHUB}/widget/devhub.components.molecule.Tile` +) || { Tile: () => <> }; + +const { accessControlInfo, createEditTeam } = props; + +const [editModerators, setEditModerators] = useState(false); +const [moderators, setModerators] = useState( + accessControlInfo.members_list["team:moderators"].children || [] +); + +const handleEditModerators = () => { + createEditTeam({ + teamName: "moderators", + description: + "The moderator group has permissions to create and edit RFPs, edit and manage proposals, and manage admins.", + members: moderators, + contractCall: "edit_member", + }); +}; + +const handleCancelModerators = () => { + setEditModerators(false); + setModerators(accessControlInfo.members_list["team:moderators"].children); +}; + +return ( + <> +

Moderators

+
+
+ The moderator group has permissions to create and edit RFPs, edit and + manage proposals, and manage admins. +
+ setEditModerators(!editModerators), + testId: "edit-members", + }} + /> +
+ + {editModerators ? ( + <> + true, + invalidate: () => null, + }} + /> +
+ + +
+ + ) : ( + <> +
Members
+ {moderators && ( +
+ {moderators.length ? ( + moderators.map((child) => ( + + + + )) + ) : ( +
No moderators
+ )} +
+ )} + + )} +
+ +); diff --git a/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx b/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx new file mode 100644 index 000000000..35a1510d5 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx @@ -0,0 +1,24 @@ +function ensureOtherIsLast(arr) { + const otherIndex = (arr ?? []).findIndex((item) => item.value === "Other"); + + if (otherIndex !== -1) { + const [otherItem] = arr.splice(otherIndex, 1); + arr.push(otherItem); + } + return arr; +} + +function getGlobalLabels() { + let labels = Near.view( + "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}", + "get_global_labels" + ); + if (labels !== null) { + labels = ensureOtherIsLast(labels); + } + return labels ?? null; +} + +return { + getGlobalLabels, +}; diff --git a/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx b/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx new file mode 100644 index 000000000..1e821eb56 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx @@ -0,0 +1,75 @@ +const value = props.value; +const placeholder = props.placeholder; +const onUpdate = props.onUpdate; + +const [account, setAccount] = useState(value); +const [showAccountAutocomplete, setAutoComplete] = useState(false); +const [isValidAccount, setValidAccount] = useState(true); +const AutoComplete = styled.div` + margin-top: 1rem; +`; + +useEffect(() => { + if (value !== account) { + setAccount(value); + } +}, [value]); + +useEffect(() => { + if (value !== account) { + onUpdate(account); + } +}, [account]); + +useEffect(() => { + const handler = setTimeout(() => { + const valid = account.length === 64 || (account ?? "").includes(".near"); + setValidAccount(valid); + setAutoComplete(!valid); + }, 100); + + return () => { + clearTimeout(handler); + }; +}, [account]); + +return ( +
+ { + setAccount(e.target.value); + }, + skipPaddingGap: true, + placeholder: placeholder, + inputProps: { + max: 64, + prefix: "@", + }, + }} + /> + {account && !isValidAccount && ( +
+ Please enter valid account ID +
+ )} + {showAccountAutocomplete && ( + + { + setAccount(id); + setAutoComplete(false); + }, + onClose: () => setAutoComplete(false), + }} + /> + + )} +
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx b/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx new file mode 100644 index 000000000..b6afed8a8 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx @@ -0,0 +1,118 @@ +const EmbeddCSS = ` + .CodeMirror { + margin-inline:10px; + border-radius:5px; + } + + .editor-toolbar { + border: none !important; + } +`; + +const Wrapper = styled.div` + .nav-link { + color: inherit !important; + } + + .card-header { + padding-bottom: 0px !important; + } +`; + +const Compose = ({ + data, + onChange, + autocompleteEnabled, + placeholder, + height, + embeddCSS, + showProposalIdAutoComplete, + onChangeKeyup, + handler, +}) => { + State.init({ + data: data, + selectedTab: "editor", + autoFocus: false, + }); + + useEffect(() => { + if (typeof onChange === "function") { + onChange(state.data); + } + }, [state.data]); + + useEffect(() => { + // for clearing editor after txn approval/ showing draft state + if (data !== state.data || handler !== state.handler) { + State.update({ data: data, handler: handler }); + } + }, [data, handler]); + + return ( + +
+
+
+ +
+
+ + {state.selectedTab === "editor" ? ( + <> + { + State.update({ data: content, handler: "update" }); + }, + placeholder: placeholder, + height, + embeddCSS: embeddCSS || EmbeddCSS, + showAutoComplete: autocompleteEnabled, + showProposalIdAutoComplete: showProposalIdAutoComplete, + autoFocus: state.autoFocus, + onChangeKeyup: onChangeKeyup, + }} + /> + + ) : ( +
+ +
+ )} +
+
+ ); +}; + +return Compose(props); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx b/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx new file mode 100644 index 000000000..b69786402 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx @@ -0,0 +1,265 @@ +const proposalId = props.proposalId; +const rfpId = props.rfpId; +const draftKey = "INFRA_COMMENT_DRAFT" + proposalId; +let draftComment = ""; + +const ComposeEmbeddCSS = ` + .CodeMirror { + border: none !important; + min-height: 50px !important; + } + + .editor-toolbar { + border: none !important; + } + + .CodeMirror-scroll{ + min-height: 50px !important; + max-height: 300px !important; + } +`; + +const notifyAccountIds = props.notifyAccountIds ?? []; +const accountId = context.accountId; +const item = props.item; +const [allowGetDraft, setAllowGetDraft] = useState(true); +const [comment, setComment] = useState(null); +const [isTxnCreated, setTxnCreated] = useState(false); +const [handler, setHandler] = useState("update"); // to update editor state on draft and txn approval +const [showCommentToast, setCommentToast] = useState(false); + +if (allowGetDraft) { + draftComment = Storage.privateGet(draftKey); +} + +useEffect(() => { + if (draftComment) { + setComment(draftComment); + setAllowGetDraft(false); + setHandler("refreshEditor"); + } +}, [draftComment]); + +useEffect(() => { + if (draftComment === comment) { + return; + } + const handler = setTimeout(() => { + Storage.privateSet(draftKey, comment); + }, 1000); + + return () => { + clearTimeout(handler); + }; +}, [comment]); + +useEffect(() => { + if (handler === "update") { + return; + } + const handler = setTimeout(() => { + setHandler("update"); + }, 3000); + + return () => { + clearTimeout(handler); + }; +}, [handler]); + +if (!accountId) { + return ( +
+ + + +
to join this conversation.
+
Already have an account?
+ + Log in to comment + +
+ ); +} + +function extractMentions(text) { + const mentionRegex = + /@((?:(?:[a-z\d]+[-_])*[a-z\d]+\.)*(?:[a-z\d]+[-_])*[a-z\d]+)/gi; + mentionRegex.lastIndex = 0; + const accountIds = new Set(); + for (const match of text.matchAll(mentionRegex)) { + if ( + !/[\w`]/.test(match.input.charAt(match.index - 1)) && + !/[/\w`]/.test(match.input.charAt(match.index + match[0].length)) && + match[1].length >= 2 && + match[1].length <= 64 + ) { + accountIds.add(match[1].toLowerCase()); + } + } + return [...accountIds]; +} + +function extractTagNotifications(text, item) { + return extractMentions(text || "") + .filter((accountId) => accountId !== context.accountId) + .map((accountId) => ({ + key: accountId, + value: { + type: "mention", + item, + }, + })); +} + +function composeData() { + setTxnCreated(true); + const data = { + post: { + comment: JSON.stringify({ + type: "md", + text: comment, + item, + }), + }, + index: { + comment: JSON.stringify({ + key: item, + value: { + type: "md", + }, + }), + }, + }; + + const notifications = extractTagNotifications(comment, { + type: "social", + path: `${accountId}/post/comment`, + }); + + if (notifyAccountIds.length > 0) { + notifyAccountIds.map((account) => { + if (account !== context.accountId) { + notifications.push({ + key: account, + value: proposalId + ? { + type: "proposal/reply", + item, + proposal: proposalId, + widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}", + } + : { + type: "rfp/reply", + item, + rfp: rfpId, + widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}", + }, + }); + } + }); + } + + if (notifications.length) { + data.index.notify = JSON.stringify( + notifications.length > 1 ? notifications : notifications[0] + ); + } + + Social.set(data, { + force: true, + onCommit: () => { + setCommentToast(true); + setComment(""); + setHandler("refreshEditor"); + setTxnCreated(false); + }, + onCancel: () => { + setTxnCreated(false); + }, + }); +} + +useEffect(() => { + if (props.transactionHashes && comment) { + setComment(""); + } +}, [props.transactionHashes]); + +const LoadingButtonSpinner = ( + +); + +const Compose = useMemo(() => { + return ( + + ); +}, [draftComment, handler]); + +return ( +
+ setCommentToast(v), + trigger: <>, + providerProps: { duration: 3000 }, + }} + /> + +
+ Add a comment + {Compose} +
+ { + composeData(); + }, + }} + /> +
+
+
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx new file mode 100644 index 000000000..23a1bb819 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx @@ -0,0 +1,66 @@ +const options = props.options; // [{label:"",value:""}] +const label = props.label; +const onUpdate = props.onUpdate ?? (() => {}); +const selectedValue = props.selectedValue; +const [selected, setSelected] = useState(selectedValue); + +useEffect(() => { + if (JSON.stringify(selectedValue) !== JSON.stringify(selected)) { + setSelected(selectedValue); + } +}, [selectedValue]); + +const StyledDropdown = styled.div` + .drop-btn { + width: 100%; + max-width: 200px; + text-align: left; + padding-inline: 10px; + } + + .dropdown-item.active, + .dropdown-item:active { + background-color: #f0f0f0 !important; + color: black; + } + + .cursor-pointer { + cursor: pointer; + } +`; + +useEffect(() => { + onUpdate(selected); +}, [selected]); + +return ( +
+
+ + +
    + {options.map((item) => ( +
  • { + if (selected.label !== item.label) { + setSelected(item); + } + }} + > + {item.label} +
  • + ))} +
+
+
+
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx b/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx new file mode 100644 index 000000000..c4365a553 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx @@ -0,0 +1,193 @@ +const { + selectedValue, + onChange, + options, + defaultLabel, + showSearch, + searchInputPlaceholder, + searchByLabel, + searchByValue, + onSearch, + disabled, +} = props; + +const [searchTerm, setSearchTerm] = useState(""); +const [filteredOptions, setFilteredOptions] = useState(options); +const [isOpen, setIsOpen] = useState(false); +const [selectedOption, setSelectedOption] = useState({ + label: + options?.find((item) => item.value === selectedValue)?.label ?? + defaultLabel, + value: defaultLabel, +}); + +useEffect(() => { + if (selectedOption.value !== selectedValue) { + setSelectedOption({ + label: + options?.find((item) => item.value === selectedValue)?.label ?? + defaultLabel, + value: defaultLabel, + }); + } +}, [selectedValue]); + +useEffect(() => { + setFilteredOptions(options); +}, [options]); + +const handleSearch = (event) => { + const term = event.target.value.toLowerCase(); + setSearchTerm(term); + if (typeof onSearch === "function") { + onSearch(term); + return; + } + + const filteredOptions = options.filter((option) => { + if (searchByLabel) { + return option.label.toLowerCase().includes(term); + } + if (searchByValue) { + return option.value.toString().toLowerCase().includes(term); + } + }); + + setFilteredOptions(filteredOptions); +}; + +const toggleDropdown = () => { + setIsOpen(!isOpen); +}; + +const handleOptionClick = (option) => { + setSelectedOption(option); + setIsOpen(false); + onChange(option); +}; + +const Container = styled.div` + .drop-btn { + width: 100%; + text-align: left; + padding-inline: 10px; + } + + .dropdown-toggle:after { + position: absolute; + top: 46%; + right: 5%; + } + + .dropdown-menu { + width: 100%; + } + + .dropdown-item.active, + .dropdown-item:active { + background-color: #f0f0f0 !important; + color: black; + } + + .custom-select { + position: relative; + } + + .scroll-box { + max-height: 200px; + overflow-y: scroll; + } + + .selected { + background-color: #f0f0f0; + } + + input { + background-color: #f8f9fa; + } + + .cursor-pointer { + cursor: pointer; + } + + .text-wrap { + overflow: hidden; + white-space: normal; + } + + .disabled { + background-color: #f8f8f8 !important; + cursor: not-allowed !important; + border-radius: 5px; + opacity: inherit !important; + } + + .disabled.dropdown-toggle::after { + display: none !important; + } +`; +let searchFocused = false; +return ( + +
{ + setTimeout(() => { + setIsOpen(searchFocused || false); + }, 0); + }} + > +
+
+ {selectedOption.label} +
+
+ + {isOpen && ( +
+ {showSearch && ( + { + searchFocused = true; + }} + onBlur={() => { + setTimeout(() => { + searchFocused = false; + }, 0); + }} + /> + )} +
+ {filteredOptions.map((option) => ( +
handleOptionClick(option)} + > + {option.label} +
+ ))} +
+
+ )} +
+
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx b/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx new file mode 100644 index 000000000..35eb464b6 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx @@ -0,0 +1,22 @@ +const availableOptions = props.availableOptions; +const options = + (availableOptions ?? []).map((i) => { + return { label: i.title, value: i.value }; + }) ?? []; +options.unshift({ label: "All", value: null }); +const setSelected = props.onStateChange ?? (() => {}); + +return ( +
+ { + setSelected(v); + }, + }} + /> +
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx new file mode 100644 index 000000000..02949f0e1 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx @@ -0,0 +1,148 @@ +const item = props.item; +const proposalId = props.proposalId; +const rfpId = props.rfpId; +const notifyAccountIds = props.notifyAccountIds ?? []; +if (!item) { + return ""; +} + +const likes = Social.index("like", item); + +const dataLoading = likes === null; + +const likesByUsers = {}; + +(likes || []).forEach((like) => { + if (like.value.type === "like") { + likesByUsers[like.accountId] = like; + } else if (like.value.type === "unlike") { + delete likesByUsers[like.accountId]; + } +}); +if (state.hasLike === true) { + likesByUsers[context.accountId] = { + accountId: context.accountId, + }; +} else if (state.hasLike === false) { + delete likesByUsers[context.accountId]; +} + +const accountsWithLikes = Object.keys(likesByUsers); +const hasLike = context.accountId && !!likesByUsers[context.accountId]; +const hasLikeOptimistic = + state.hasLikeOptimistic === undefined ? hasLike : state.hasLikeOptimistic; +const totalLikes = + accountsWithLikes.length + + (hasLike === false && state.hasLikeOptimistic === true ? 1 : 0) - + (hasLike === true && state.hasLikeOptimistic === false ? 1 : 0); + +const LikeButton = styled.button` + border: 0; + display: inline-flex; + align-items: center; + gap: 6px; + color: #687076; + font-weight: 400; + font-size: 14px; + line-height: 17px; + cursor: pointer; + background: none; + padding: 6px; + transition: color 200ms; + + i { + font-size: 16px; + transition: color 200ms; + + &.bi-heart-fill { + color: #e5484d !important; + } + } + + &:hover, + &:focus { + outline: none; + color: #11181c; + } +`; + +const likeClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (state.loading) { + return; + } + + State.update({ + loading: true, + hasLikeOptimistic: !hasLike, + }); + + const data = { + index: { + like: JSON.stringify({ + key: item, + value: { + type: hasLike ? "unlike" : "like", + }, + }), + }, + }; + + if (!hasLike && notifyAccountIds.length > 0) { + const notifyData = notifyAccountIds.map((account) => { + if (account !== context.accountId) { + return { + key: account, + value: proposalId + ? { + type: "proposal/like", + item, + proposal: proposalId, + widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}", + } + : { + type: "rfp/like", + item, + rfp: rfpId, + widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}", + }, + }; + } + }); + if (notifyData.length > 0) { + data.index.notify = notifyData; + } + } + Social.set(data, { + onCommit: () => State.update({ loading: false, hasLike: !hasLike }), + onCancel: () => + State.update({ + loading: false, + hasLikeOptimistic: !state.hasLikeOptimistic, + }), + }); +}; + +const title = hasLike ? "Unlike" : "Like"; + +return ( + + + {Object.values(likesByUsers ?? {}).length > 0 ? ( + + + + ) : ( + "0" + )} + +); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx new file mode 100644 index 000000000..c08ca6b65 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx @@ -0,0 +1,78 @@ +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`) || { + href: () => {}, +}; + +const { readableDate } = VM.require( + `${REPL_DEVHUB}/widget/core.lib.common` +) || { readableDate: () => {} }; + +const linkedProposalIds = props.linkedProposalIds ?? []; +const linkedProposalsData = []; +const showStatus = props.showStatus ?? false; + +// using contract instead of indexer, since indexer doesn't return timestamp +linkedProposalIds.map((item) => { + const data = Near.view( + "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}", + "get_proposal", + { + proposal_id: item, + } + ); + if (data !== null) { + linkedProposalsData.push(data); + } +}); + +const Container = styled.div` + a { + &:hover { + text-decoration: none !important; + } + } +`; + +return ( + + {linkedProposalsData.map((item) => { + return ( + +
+ +
+ {item.snapshot.name} +
+ created on {readableDate(item.snapshot.timestamp / 1000000)} +
+ {showStatus && ( +
+ +
+ )} +
+
+
+ ); + })} +
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx new file mode 100644 index 000000000..b7252f090 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx @@ -0,0 +1,155 @@ +const { fetchGraphQL } = VM.require( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common` +) || { fetchGraphQL: () => {} }; + +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); +href || (href = () => {}); + +const linkedProposals = props.linkedProposals; +const onChange = props.onChange; +const [selectedProposals, setSelectedProposals] = useState(linkedProposals); +const [proposalsOptions, setProposalsOptions] = useState([]); +const [searchProposalId, setSearchProposalId] = useState(""); + +const queryName = "${REPL_PROPOSAL_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: {proposal_id: desc} + where: $where +) { + name + proposal_id +} +}`; + +useEffect(() => { + if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) { + setSelectedProposals(linkedProposals); + } +}, [linkedProposals]); + +useEffect(() => { + if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) { + onChange(selectedProposals); + } +}, [selectedProposals]); + +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 = {}; + const { number, text } = separateNumberAndText(searchProposalId); + + 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 fetchProposals = () => { + const FETCH_LIMIT = 30; + const variables = { + limit: FETCH_LIMIT, + offset: 0, + where: buildWhereClause(), + }; + fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => { + if (result.status === 200) { + if (result.body.data) { + const proposalsData = result.body.data?.[queryName]; + const data = []; + for (const prop of proposalsData) { + data.push({ + label: "# " + prop.proposal_id + " : " + prop.name, + value: prop.proposal_id, + }); + } + setProposalsOptions(data); + } + } + }); +}; + +useEffect(() => { + fetchProposals(); +}, [searchProposalId]); + +return ( + <> + {selectedProposals.map((proposal) => { + return ( +
+ + {proposal.label} + +
{ + const updatedLinkedProposals = selectedProposals.filter( + (item) => item.value !== proposal.value + ); + setSelectedProposals(updatedLinkedProposals); + }} + > + +
+
+ ); + })} + + { + if (!selectedProposals.some((item) => item.value === v.value)) { + setSelectedProposals([...selectedProposals, v]); + } + }, + options: proposalsOptions, + showSearch: true, + searchInputPlaceholder: "Search by Id", + defaultLabel: "Search proposals", + searchByValue: true, + onSearch: (value) => { + setSearchProposalId(value); + }, + }} + /> + +); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx new file mode 100644 index 000000000..eca60224d --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx @@ -0,0 +1,185 @@ +const { RFP_TIMELINE_STATUS, fetchGraphQL, parseJSON } = VM.require( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common` +) || { RFP_TIMELINE_STATUS: {}, fetchGraphQL: () => {}, parseJSON: () => {} }; +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); +href || (href = () => {}); + +const { linkedRfp, onChange, disabled, onDeleteRfp } = props; + +const isModerator = Near.view( + "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}", + "is_allowed_to_write_rfps", + { + editor: context.accountId, + } +); + +const [selectedRFP, setSelectedRFP] = useState(null); +const [acceptingRfpsOptions, setAcceptingRfpsOption] = useState([]); +const [allRfpOptions, setAllRfpOptions] = useState([]); +const [searchRFPId, setSearchRfpId] = useState(""); +const [initialStateApplied, setInitialState] = useState(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 + ) { + name + rfp_id + timeline + } + }`; + +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 = () => { + // show only accepting submissions stage rfps + let where = {}; + const { number, text } = separateNumberAndText(searchRFPId); + + if (number) { + where = { rfp_id: { _eq: number }, ...where }; + } + + if (text) { + where = { + _or: [ + { name: { _iregex: `${text}` } }, + { summary: { _iregex: `${text}` } }, + { description: { _iregex: `${text}` } }, + ], + ...where, + }; + } + + return where; +}; + +const fetchRfps = () => { + const FETCH_LIMIT = 30; + const variables = { + limit: FETCH_LIMIT, + offset: 0, + where: buildWhereClause(), + }; + fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => { + if (result.status === 200) { + if (result.body.data) { + const rfpsData = result.body.data?.[queryName]; + const data = []; + const acceptingData = []; + for (const prop of rfpsData) { + const timeline = parseJSON(prop.timeline); + const label = "# " + prop.rfp_id + " : " + prop.name; + const value = prop.rfp_id; + if (timeline.status === RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS) { + acceptingData.push({ + label, + value, + }); + } + data.push({ + label, + value, + }); + } + setAcceptingRfpsOption(acceptingData); + setAllRfpOptions(data); + } + } + }); +}; + +useEffect(() => { + fetchRfps(); +}, [searchRFPId]); + +useEffect(() => { + if (JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP)) { + if (allRfpOptions.length > 0) { + if (typeof linkedRfp !== "object") { + setSelectedRFP(allRfpOptions.find((i) => linkedRfp === i.value)); + } else { + setSelectedRFP(linkedRfp); + } + setInitialState(true); + } + } else { + setInitialState(true); + } +}, [linkedRfp, allRfpOptions]); + +useEffect(() => { + if ( + JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP) && + initialStateApplied + ) { + onChange(selectedRFP); + } +}, [selectedRFP, initialStateApplied]); + +return ( + <> + {selectedRFP && ( +
+ + {selectedRFP.label} + + {!disabled && ( +
{ + onDeleteRfp(); + setSelectedRFP(null); + }} + > + +
+ )} +
+ )} + { + setSelectedRFP(v); + }, + options: isModerator ? allRfpOptions : acceptingRfpsOptions, + showSearch: true, + searchInputPlaceholder: "Search by Id", + defaultLabel: "Search RFP", + searchByValue: true, + onSearch: (value) => { + setSearchRfpId(value); + }, + }} + /> + +); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx new file mode 100644 index 000000000..777162356 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx @@ -0,0 +1,61 @@ +const { readableDate } = VM.require( + `${REPL_DEVHUB}/widget/core.lib.common` +) || { readableDate: () => {} }; + +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`) || { + href: () => {}, +}; + +const linkedRfpIds = props.linkedRfpIds ?? []; +const linkedRfpsData = []; + +linkedRfpIds.map((item) => { + const data = Near.view( + "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}", + "get_rfp", + { + rfp_id: item, + } + ); + if (data !== null) { + linkedRfpsData.push(data); + } +}); + +const Container = styled.div` + a { + &:hover { + text-decoration: none !important; + } + } +`; + +return ( + + {linkedRfpsData.map((item) => { + return ( + +
+ +
+ {item.snapshot.name} +
+ created on {readableDate(item.snapshot.timestamp / 1000000)} +
+
+
+
+ ); + })} +
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx new file mode 100644 index 000000000..26d9f4c6c --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx @@ -0,0 +1,38 @@ +const Container = styled.div` + p { + white-space: pre-line; // This ensures text breaks to new line + + span { + white-space: normal; // and this ensures profile links look normal + } + } + + blockquote { + margin: 1em 0; + padding-left: 1.5em; + border-left: 4px solid #ccc; + color: #666; + font-style: italic; + font-size: inherit; + } + + pre { + background-color: #f4f4f4; + border: 1px solid #ddd; + border-radius: 4px; + padding: 1em; + overflow-x: auto; + font-family: "Courier New", Courier, monospace; + } + + a { + color: #3c697d; + font-weight: 500 !important; + } +`; + +return ( + + + +); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx new file mode 100644 index 000000000..358984ee7 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx @@ -0,0 +1,193 @@ +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); +href || (href = () => {}); + +const { + selected, + onChange, + disabled, + availableOptions, + hideDropdown, + linkedRfp, +} = props; + +const [selectedOptions, setSelectedOptions] = useState([]); +const [isOpen, setIsOpen] = useState(false); +const [initialStateApplied, setInitialState] = useState(false); + +const toggleDropdown = () => { + setIsOpen(!isOpen); +}; + +useEffect(() => { + if (JSON.stringify(selectedOptions) !== JSON.stringify(selected)) { + if (availableOptions.length > 0) { + if ((selected ?? []).some((i) => !i.value)) { + setSelectedOptions( + selected.map((i) => availableOptions.find((t) => t.value === i)) + ); + } else { + setSelectedOptions(selected); + } + setInitialState(true); + } + } else { + setInitialState(true); + } +}, [selected, availableOptions]); + +useEffect(() => { + if ( + JSON.stringify(selectedOptions) !== JSON.stringify(selected) && + initialStateApplied + ) { + onChange(selectedOptions); + } +}, [selectedOptions, initialStateApplied]); + +const Container = styled.div` + .drop-btn { + width: 100%; + text-align: left; + padding-inline: 10px; + } + + .dropdown-toggle:after { + position: absolute; + top: 46%; + right: 2%; + } + + .dropdown-menu { + width: 100%; + } + + .dropdown-item.active, + .dropdown-item:active { + background-color: #f0f0f0 !important; + color: black; + } + + .disabled { + background-color: #f8f8f8 !important; + cursor: not-allowed !important; + border-radius: 5px; + opacity: inherit !important; + } + + .disabled.dropdown-toggle::after { + display: none !important; + } + + .custom-select { + position: relative; + } + + .selected { + background-color: #f0f0f0; + } + + .cursor-pointer { + cursor: pointer; + } + + .text-wrap { + overflow: hidden; + white-space: normal; + } +`; + +const handleOptionClick = (option) => { + if (!selectedOptions.some((item) => item.value === option.value)) { + setSelectedOptions([...selectedOptions, option]); + } + setIsOpen(false); +}; + +const Item = ({ option }) => { + return
{option.title}
; +}; + +return ( + <> +
+ {(selectedOptions ?? []).map((option) => { + return ( +
+ {option.title} + {!disabled && ( +
{ + const updatedOptions = selectedOptions.filter( + (item) => item.value !== option.value + ); + setSelectedOptions(updatedOptions); + }} + > + +
+ )} +
+ ); + })} +
+ {!hideDropdown && ( + +
setIsOpen(false)} + > +
+
+ {linkedRfp ? ( + + + These categories match the chosen RFP and cannot be changed. + To use different categories, unlink the RFP. + + ) : ( + Select Category + )} +
+
+ + {isOpen && ( +
+
+ {(availableOptions ?? []).map((option) => ( +
item.value === option.value + ) + ? "selected" + : "" + }`} + onClick={() => handleOptionClick(option)} + > + +
+ ))} +
+
+ )} +
+
+ )} + +); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx new file mode 100644 index 000000000..156284ccb --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx @@ -0,0 +1,129 @@ +const title = props.title; +const links = props.links; +const href = props.href; + +const [showMenu, setShowMenu] = useState(false); + +const { href: linkHref } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); + +linkHref || (linkHref = () => {}); + +const Dropdown = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: center; + + p { + &.active { + color: #fff; + + &:hover { + text-decoration: none; + color: #096d50 !important; + } + } + } +`; + +const DropdownMenu = styled.div` + z-index: 50; + position: absolute; + top: 2.25rem; + + &.active { + padding: 0.5rem 1rem; + padding-top: 1rem; + border-radius: 1rem; + background: rgba(217, 217, 217, 0.7); + backdrop-filter: blur(5px); + width: max-content; + animation: slide-down 300ms ease; + transform-origin: top center; + } + + @keyframes slide-down { + 0% { + transform: scaleY(0); + } + 100% { + transform: scaleY(1); + } + } +`; + +const DropdownLink = styled.div` + color: inherit; + text-decoration: none; + + &.active { + color: #555555; + } + + &:hover { + text-decoration: none; + color: #096d50 !important; + } +`; + +return ( + setShowMenu(true)} + onMouseLeave={() => setShowMenu(false)} + > + {href ? ( + + + {title} + + + ) : ( +

+ {title} ↓ +

+ )} + {showMenu && links.length !== 0 && ( + +
+ {links.map((link) => ( + // Check if the link is external + + {link.href.startsWith("http://") || + link.href.startsWith("https://") ? ( + // External link: Render an tag + + {link.title} + + ) : ( + // Internal link: Render the component + + {link.title} + + )} + + ))} +
+
+ )} +
+); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx b/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx new file mode 100644 index 000000000..b08dcd10d --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx @@ -0,0 +1,29 @@ +const RadioButton = ({ value, isChecked, label, onClick, disabled }) => { + const [checked, setChecked] = useState(isChecked); + + useEffect(() => { + if (isChecked !== checked) { + setChecked(isChecked); + } + }, [isChecked]); + + useEffect(() => { + onClick(checked); + }, [checked]); + + return ( +
+ setChecked(e.target.checked)} + /> + +
+ ); +}; + +return RadioButton(props); diff --git a/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx b/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx new file mode 100644 index 000000000..a2157e055 --- /dev/null +++ b/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx @@ -0,0 +1,629 @@ +/** + * iframe embedding a SimpleMDE component + * https://github.com/sparksuite/simplemde-markdown-editor + */ +const { getLinkUsingCurrentGateway } = VM.require( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common` +) || { getLinkUsingCurrentGateway: () => {} }; +const data = props.data; +const onChange = props.onChange ?? (() => {}); +const onChangeKeyup = props.onChangeKeyup ?? (() => {}); // in case where we want immediate action +const height = props.height ?? "390"; +const className = props.className ?? "w-100"; +const embeddCSS = props.embeddCSS; + +State.init({ + iframeHeight: height, + message: props.data, +}); + +const profilesData = Social.get("*/profile/name", "final"); +const followingData = Social.get( + `${context.accountId}/graph/follow/**`, + "final" +); + +// SIMPLEMDE CONFIG // +const fontFamily = props.fontFamily ?? "sans-serif"; +const alignToolItems = props.alignToolItems ?? "right"; +const placeholder = props.placeholder ?? ""; +const showAccountAutoComplete = props.showAutoComplete ?? false; +const showProposalIdAutoComplete = props.showProposalIdAutoComplete ?? false; +const showRfpIdAutoComplete = props.showRfpIdAutoComplete ?? false; +const autoFocus = props.autoFocus ?? false; + +const proposalQueryName = "${REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME}"; +const proposalLink = getLinkUsingCurrentGateway( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/app?page=proposal&id=` +); +const proposalQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${proposalQueryName}_bool_exp = {}) { +${proposalQueryName}( + offset: $offset + limit: $limit + order_by: {proposal_id: desc} + where: $where +) { + name + proposal_id +} +}`; + +const rfpQueryName = "${REPL_RFP_FEED_INDEXER_QUERY_NAME}"; +const rfpLink = getLinkUsingCurrentGateway( + `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/app?page=rfp&id=` +); +const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) { +${rfpQueryName}( + offset: $offset + limit: $limit + order_by: {rfp_id: desc} + where: $where +) { + rfp_id + name +} +}`; + +const code = ` + + + + + + + + + + + + + + + + + + + + + + + +`; + +return ( +