diff --git a/playwright-tests/tests/create.spec.js b/playwright-tests/tests/create.spec.js
index 22dafad3f..804676b01 100644
--- a/playwright-tests/tests/create.spec.js
+++ b/playwright-tests/tests/create.spec.js
@@ -1,24 +1,179 @@
-import { test } from "@playwright/test";
-import { setInputAndAssert, selectAndAssert } from "../testUtils";
+import { expect, test } from "@playwright/test";
+import { selectAndAssert, setInputAndAssert } from "../testUtils";
-test("should be able to submit a solution with USDC as currency", async ({
- page,
-}) => {
- await page.goto("/devgovgigs.near/widget/app?page=create");
+test.describe("Wallet is not connected", () => {
+ // sign in to wallet
+ test.use({
+ storageState: "playwright-tests/storage-states/wallet-not-connected.json",
+ });
- await page.click('button:has-text("Solution")');
+ test("should not be able to create if not logged in", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
- await setInputAndAssert(
+ const createPostButton = 'button[data-testid="submit-create-post"]';
+
+ await page.waitForSelector(createPostButton, {
+ state: "detached",
+ });
+
+ const isCreatePostButtonVisible = await page.isVisible(createPostButton);
+
+ expect(isCreatePostButtonVisible).toBeFalsy();
+ });
+});
+
+test.describe("Wallet is connected", () => {
+ // sign in to wallet
+ test.use({
+ storageState: "playwright-tests/storage-states/wallet-connected.json",
+ });
+
+ test("should be able to submit a solution with USDC as currency", async ({
page,
- 'p:has-text("Title") + input',
- "The test title"
- );
+ }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+
+ await page.click('button:has-text("Solution")');
+
+ await setInputAndAssert(
+ page,
+ 'input[data-testid="name-editor"]',
+ "The test title"
+ );
+
+ await page.click('label:has-text("Yes") button');
+ await selectAndAssert(page, 'div:has-text("Currency") select', "USDT");
+ await setInputAndAssert(
+ page,
+ 'input[data-testid="requested-amount-editor"]',
+ "300"
+ );
+ });
- await page.click('label:has-text("Yes") button');
- await selectAndAssert(page, 'div:has-text("Currency") select', "USDT");
- await setInputAndAssert(
+ test("should init create post with single label from params", async ({
page,
- 'div:has-text("Requested amount") input',
- "300"
- );
+ }) => {
+ await page.goto("/devhub.near/widget/app?page=create&labels=devhub-test");
+
+ const selector = ".rbt-input-multi";
+ const typeAheadElement = await page.waitForSelector(selector);
+
+ const tokenWithValue = await typeAheadElement.$(
+ '.rbt-token:has-text("devhub-test")'
+ );
+ expect(tokenWithValue).toBeTruthy();
+ });
+
+ test("should init create post with multi label from params", async ({
+ page,
+ }) => {
+ await page.goto(
+ "/devhub.near/widget/app?page=create&labels=devhub-test,security"
+ );
+
+ const selector = ".rbt-input-multi";
+ const typeAheadElement = await page.waitForSelector(selector);
+
+ const devhubTestToken = await typeAheadElement.$(
+ '.rbt-token:has-text("devhub-test")'
+ );
+ expect(devhubTestToken).toBeTruthy();
+ const securityToken = await typeAheadElement.$(
+ '.rbt-token:has-text("security")'
+ );
+ expect(securityToken).toBeTruthy();
+ });
+
+ test("should allow user to select multiple labels", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+ const selector = ".rbt-input-main";
+ await page.waitForSelector(selector);
+ await page.fill(selector, "devhub-test");
+
+ await page.evaluate(() => {
+ window.scrollTo(0, document.body.scrollHeight); // Scroll to the bottom of the page
+ }); // we do this because the input dropdown is not visible and cannot be clicked
+ await page.getByLabel("devhub-test").click();
+
+ await page.fill(selector, "security");
+ await page.getByLabel("security").click();
+
+ const typeAheadElement = await page.waitForSelector(".rbt-input-multi");
+ const devhubToken = await typeAheadElement.$(
+ `.rbt-token:has-text("devhub-test")`
+ );
+ expect(devhubToken).toBeTruthy();
+
+ const securityToken = await typeAheadElement.$(
+ `.rbt-token:has-text("security")`
+ );
+ expect(securityToken).toBeTruthy();
+ });
+
+ test("should not allow user to use the blog label", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+
+ const selector = ".rbt-input-main";
+ await page.waitForSelector(selector);
+ await page.fill(selector, "blog");
+ const labelSelector = `:is(label:has-text("blog"))`;
+ await page.waitForSelector(labelSelector, { state: "detached" });
+ });
+
+ test("should not allow user to use a protected label", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+
+ const selector = ".rbt-input-main";
+ await page.waitForSelector(selector);
+ await page.fill(selector, "funding-requested");
+ const labelSelector = `:is(label:has-text("funding-requested"))`;
+ await page.waitForSelector(labelSelector, { state: "detached" });
+
+ await page.waitForSelector(".alert", { state: "visible" });
+ });
+
+ test("should allow the user to create new labels", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+
+ const selector = ".rbt-input-main";
+ await page.waitForSelector(selector);
+ await page.fill(selector, "random-crazy-label-lol");
+ const labelSelector = `:is(mark:has-text("random-crazy-label-lol"))`;
+ const element = await page.waitForSelector(labelSelector);
+ await page.evaluate(() => {
+ window.scrollTo(0, document.body.scrollHeight); // Scroll to the bottom of the page
+ });
+ await element.click();
+
+ const typeAheadElement = await page.waitForSelector(".rbt-input-multi");
+ const newToken = await typeAheadElement.$(
+ `.rbt-token:has-text("random-crazy-label-lol")`
+ );
+ expect(newToken).toBeTruthy();
+ });
+});
+
+test.describe("Admin is connected", () => {
+ // sign in to wallet
+ test.use({
+ storageState: "playwright-tests/storage-states/wallet-connected-admin.json",
+ });
+
+ test("should allow admin to use a protected label", async ({ page }) => {
+ await page.goto("/devhub.near/widget/app?page=create");
+
+ const selector = ".rbt-input-main";
+ await page.waitForSelector(selector);
+ await page.fill(selector, "funding-requested");
+ await page.evaluate(() => {
+ window.scrollTo(0, document.body.scrollHeight); // Scroll to the bottom of the page
+ }); // we do this because the input dropdown is not visible and cannot be clicked
+ await page.getByLabel("funding-requested").click();
+
+ const typeAheadElement = await page.waitForSelector(".rbt-input-multi");
+ const protectedToken = await typeAheadElement.$(
+ `.rbt-token:has-text("funding-requested")`
+ );
+ expect(protectedToken).toBeTruthy();
+ });
});
diff --git a/src/devhub/page/create.jsx b/src/devhub/page/create.jsx
index 54ecdb65e..127740edb 100644
--- a/src/devhub/page/create.jsx
+++ b/src/devhub/page/create.jsx
@@ -1,26 +1,69 @@
+const CenteredMessage = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 384px;
+`;
+
+if (!context.accountId) {
+ return (
+ Please sign in to create a post.
+
Loading modules...
; } -const parentId = props.parentId ?? null; -const postId = props.postId ?? null; -const mode = props.mode ?? "Create"; - -const referralLabels = props.referral ? [`referral:${props.referral}`] : []; -const labelStrings = (props.labels ? props.labels.split(",") : []).concat( - referralLabels -); -const labels = labelStrings.map((s) => { - return { name: s }; -}); - -State.update({ - labels, -}); - -if (state.waitForDraftStateRestore) { - const draftstatestring = Storage.privateGet(DRAFT_STATE_STORAGE_KEY); - if (draftstatestring != null) { - if (props.transactionHashes) { - State.update({ waitForDraftStateRestore: false }); - Storage.privateSet(DRAFT_STATE_STORAGE_KEY, undefined); - } else { - try { - const draftstate = JSON.parse(draftstatestring); - State.update(draftstate); - } catch (e) { - console.error("error restoring draft", draftstatestring); - } - } - State.update({ waitForDraftStateRestore: false }); - } -} - // This must be outside onClick, because Near.view returns null at first, and when the view call finished, it returns true/false. // If checking this inside onClick, it will give `null` and we cannot tell the result is true or false. let grantNotify = Near.view("social.near", "is_write_permission_granted", { @@ -129,10 +131,6 @@ if (grantNotify === null) { } const onSubmit = () => { - Storage.privateSet(DRAFT_STATE_STORAGE_KEY, JSON.stringify(state)); - - let labels = state.labelStrings; - let body = { name: state.name, description: generateDescription( @@ -159,47 +157,33 @@ const onSubmit = () => { }; } - if (!context.accountId) return; - let txn = []; - if (mode == "Create") { - txn.push({ - contractName: "${REPL_DEVHUB_CONTRACT}", - methodName: "add_post", - args: { - parent_id: parentId, - labels, - body: body, - }, - gas: Big(10).pow(14), - }); - } else if (mode == "Edit") { - txn.push({ - contractName: "${REPL_DEVHUB_CONTRACT}", - methodName: "edit_post", + + txn.push({ + contractName: "${REPL_DEVHUB_CONTRACT}", + methodName: "add_post", + args: { + parent_id: null, + labels: state.labels, + body: body, + }, + gas: Big(10).pow(14), + }); + + if (grantNotify === false) { + txn.unshift({ + contractName: "social.near", + methodName: "grant_write_permission", args: { - id: postId, - labels, - body: body, + predecessor_id: "${REPL_DEVHUB_CONTRACT}", + keys: [context.accountId + "/index/notify"], }, gas: Big(10).pow(14), + deposit: Big(10).pow(22), }); } - if (mode == "Create" || mode == "Edit") { - if (grantNotify === false) { - txn.unshift({ - contractName: "social.near", - methodName: "grant_write_permission", - args: { - predecessor_id: "${REPL_DEVHUB_CONTRACT}", - keys: [context.accountId + "/index/notify"], - }, - gas: Big(10).pow(14), - deposit: Big(10).pow(22), - }); - } - Near.call(txn); - } + + Near.call(txn); }; const onIdeaClick = () => { @@ -240,218 +224,223 @@ const checkLabel = (label) => { }; const setLabels = (labels) => { - labels = labels.map((o) => { - o.name = normalizeLabel(o.name); - return o; - }); - if (labels.length < state.labels.length) { - let oldLabels = new Set(state.labels.map((label) => label.name)); - for (let label of labels) { - oldLabels.delete(label.name); - } - let removed = oldLabels.values().next().value; - Near.asyncView("${REPL_DEVHUB_CONTRACT}", "is_allowed_to_use_labels", { - editor: context.accountId, - labels: [removed], - }).then((allowed) => { - if (allowed) { - let labelStrings = labels.map(({ name }) => name); - State.update({ labels, labelStrings }); - } else { - State.update({ - warning: - 'The label "' + - removed + - '" is protected and can only be updated by moderators', - }); - return; + const normalizedLabels = labels.map((o) => + o.customOption ? normalizeLabel(o.label) : normalizeLabel(o) + ); + const uniqueLabels = [...new Set(normalizedLabels)]; + + if (uniqueLabels.length < state.labels.length) { + const removedLabel = state.labels.find( + (label) => !uniqueLabels.includes(label) + ); + + const allowed = Near.asyncView( + "${REPL_DEVHUB_CONTRACT}", + "is_allowed_to_use_labels", + { + editor: context.accountId, + labels: [removedLabel], } - }); + ); + + if (allowed) { + State.update({ labels: uniqueLabels }); + } else { + State.update({ + warning: `The label "${removedLabel}" is protected and can only be updated by moderators`, + }); + } } else { - let labelStrings = labels.map((o) => { - return o.name; - }); - State.update({ labels, labelStrings }); + State.update({ labels: uniqueLabels }); } }; -const existingLabelStrings = + +const existingLabels = Near.view("${REPL_DEVHUB_CONTRACT}", "get_all_allowed_labels", { editor: context.accountId, }) ?? []; -const existingLabelSet = new Set(existingLabelStrings); -const existingLabels = existingLabelStrings - .filter((it) => it !== "blog") // remove blog label so users cannot publish blogs from feed - .map((s) => { - return { name: s }; - }); - -const labelEditor = ( -Labels
-Title
- State.update({ name: event.target.value })} - /> -Description
-Title
+ State.update({ name: event.target.value })} + /> +- Are you seeking funding for your solution? - (Optional) -
-Labels
+- Requested sponsor (Optional) -
-- If you are requesting funding from a specific sponsor, please enter - their username. -
-+ Are you seeking funding for your solution? + (Optional) +
++ Requested sponsor (Optional) +
++ If you are requesting funding from a specific sponsor, please enter + their username. +
+{state.seekingFunding}
- {state.postType === "Idea" - ? "Get feedback from the community about a problem, opportunity, or need." - : "Provide a specific proposal or implementation to an idea, optionally requesting funding. If your solution relates to an existing idea, please reply to the original post with a solution."} + {postTypeOptions[state.postType].description}
{state.warning && (