From 064f44b733f5c0b8e84f321ed7ba9221069b7915 Mon Sep 17 00:00:00 2001 From: Tom Brennan Date: Sat, 9 Apr 2022 11:57:39 -0400 Subject: [PATCH 1/4] refactor "users" to imbuer --- .../db/migrations/20220114134044_initial.ts | 26 ++++----- api/src/middleware/authentication/index.ts | 18 +++--- .../authentication/strategies/google-oidc.ts | 4 +- .../strategies/web3/polkadot-js.ts | 30 +++++----- api/src/models.ts | 57 ++++++++++--------- .../routes/api/v1/{users.ts => imbuers.ts} | 4 +- api/src/routes/api/v1/index.ts | 6 +- api/src/routes/api/v1/projects.ts | 11 ++-- web/src/authentication/index.ts | 16 +++--- web/src/dapp/index.ts | 25 ++++---- web/src/model.ts | 10 ++-- web/src/my-projects/listing/index.ts | 22 +++---- web/src/proposals/detail/index.ts | 38 ++++++------- .../proposals/draft/editor/form/index.html | 6 +- web/src/proposals/draft/editor/form/index.ts | 48 +++++++++------- web/src/proposals/draft/editor/index.ts | 2 +- web/src/proposals/draft/index.html | 2 +- web/src/proposals/draft/preview/index.ts | 38 +++++++------ web/src/styles/common.css | 4 ++ 19 files changed, 194 insertions(+), 173 deletions(-) rename api/src/routes/api/v1/{users.ts => imbuers.ts} (87%) diff --git a/api/src/db/migrations/20220114134044_initial.ts b/api/src/db/migrations/20220114134044_initial.ts index e3c675cd..a89c1709 100644 --- a/api/src/db/migrations/20220114134044_initial.ts +++ b/api/src/db/migrations/20220114134044_initial.ts @@ -6,8 +6,8 @@ export async function up(knex: Knex): Promise { await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION); - const usersTableName = "users"; - await knex.schema.createTable(usersTableName, (builder) => { + const imbuerTableName = "imbuer"; + await knex.schema.createTable(imbuerTableName, (builder) => { /** * We need to be able to capture users who are just casually creating * a Project without any web3 functionality yet. So we lazily require @@ -17,22 +17,22 @@ export async function up(knex: Knex): Promise { builder.text("display_name"); auditFields(knex, builder); - }).then(onUpdateTrigger(knex, usersTableName)); + }).then(onUpdateTrigger(knex, imbuerTableName)); /** * Without at least one of these, a usr can't really do much beyond saving * a draft proposal. */ - const web3AccountsTableName = "web3_accounts"; + const web3AccountsTableName = "web3_account"; await knex.schema.createTable(web3AccountsTableName, (builder) => { builder.text("address"); - builder.integer("user_id").notNullable(); + builder.integer("imbuer_id").notNullable(); builder.text("type"); builder.text("challenge"); builder.primary(["address"]); - builder.foreign("user_id").references("users.id"); + builder.foreign("imbuer_id").references("imbuer.id"); auditFields(knex, builder); }).then(onUpdateTrigger(knex, web3AccountsTableName)); @@ -45,7 +45,7 @@ export async function up(knex: Knex): Promise { builder.primary(["issuer", "subject"]); builder.foreign("id") - .references("users.id") + .references("imbuer.id") .onDelete("CASCADE") .onUpdate("CASCADE"); @@ -123,12 +123,12 @@ export async function up(knex: Knex): Promise { * create an account and associate it with a web3 address, we can update * all of the projects whose "owner" is a `web3_account` associated with * the `usr` account. Likewise, when a user decides to delete their - * account, we don't CASCADE in that case -- only nullify the `user_id` + * account, we don't CASCADE in that case -- only nullify the `imbuer_id` * here, as it wouldn't point to anything useful. */ - builder.integer("user_id"); - builder.foreign("user_id") - .references("users.id") + builder.integer("imbuer_id"); + builder.foreign("imbuer_id") + .references("imbuer.id") .onUpdate("CASCADE") .onDelete("SET NULL"); @@ -234,7 +234,7 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists("projects"); await knex.schema.dropTableIfExists("project_status"); await knex.schema.dropTableIfExists("federated_credentials"); - await knex.schema.dropTableIfExists("web3_accounts"); - await knex.schema.dropTableIfExists("users"); + await knex.schema.dropTableIfExists("web3_account"); + await knex.schema.dropTableIfExists("imbuer"); await knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION); } diff --git a/api/src/middleware/authentication/index.ts b/api/src/middleware/authentication/index.ts index 6c0511c9..511ddeb5 100644 --- a/api/src/middleware/authentication/index.ts +++ b/api/src/middleware/authentication/index.ts @@ -2,32 +2,36 @@ import express from "express"; import passport from "passport"; import { googleOIDCStrategy, googleOIDCRouter } from "./strategies/google-oidc"; import { polkadotJsAuthRouter, polkadotJsStrategy } from "./strategies/web3/polkadot-js"; -import type { User, Web3Account } from "../../models"; +import type { Imbuer, Web3Account } from "../../models"; import db from "../../db"; passport.use(googleOIDCStrategy); passport.use(polkadotJsStrategy); +/** + * The `user` term here is specific to the passport workflow, but in this + * system we refer to "users" as `imbuer` instead. + */ passport.serializeUser((user, done) => { if (!(user as any).id) { return done( new Error("Failed to serialize User: no `id` found.") ); } - return done(null, (user as User).id); + return done(null, (user as Imbuer).id); }); passport.deserializeUser(async (id: string, done) => { try { - const user = await db.select().from("users").where({"id": Number(id)}).first(); - if (!user) { + const imbuer = await db.select().from("imbuer").where({"id": Number(id)}).first(); + if (!imbuer) { done(new Error(`No user found with id: ${id}`)); } else { - user.web3Accounts = await db("web3_accounts").select().where({ - user_id: user.id + imbuer.web3Accounts = await db("web3_account").select().where({ + imbuer_id: imbuer.id }); - return done(null, user); + return done(null, imbuer); } } catch (e) { return done( diff --git a/api/src/middleware/authentication/strategies/google-oidc.ts b/api/src/middleware/authentication/strategies/google-oidc.ts index 2e5291ba..5ca26059 100644 --- a/api/src/middleware/authentication/strategies/google-oidc.ts +++ b/api/src/middleware/authentication/strategies/google-oidc.ts @@ -1,7 +1,7 @@ import express from "express"; import type { Session } from "express-session"; import passport from "passport"; -import { getOrCreateFederatedUser } from "../../../models"; +import { getOrCreateFederatedImbuer } from "../../../models"; import config from "../../../config"; // No @types yet :( @@ -48,7 +48,7 @@ export const googleOIDCStrategy = new GoogleOIDCStrategy( // state: true, }, (issuer: string, profile: Record, done: CallableFunction) => { - return getOrCreateFederatedUser(issuer, profile.id, profile.displayName, done); + return getOrCreateFederatedImbuer(issuer, profile.id, profile.displayName, done); } ); diff --git a/api/src/middleware/authentication/strategies/web3/polkadot-js.ts b/api/src/middleware/authentication/strategies/web3/polkadot-js.ts index 5e6f831b..e12c4d7b 100644 --- a/api/src/middleware/authentication/strategies/web3/polkadot-js.ts +++ b/api/src/middleware/authentication/strategies/web3/polkadot-js.ts @@ -8,11 +8,11 @@ import { decodeAddress, encodeAddress } from "@polkadot/keyring"; import { hexToU8a, isHex } from '@polkadot/util'; import { - fetchUser, + fetchImbuer, fetchWeb3Account, upsertWeb3Challenge, - User, - getOrCreateFederatedUser + Imbuer, + getOrCreateFederatedImbuer } from "../../../../models"; import db from "../../../../db"; @@ -44,8 +44,8 @@ export class Web3Strategy extends passport.Strategy { if (!web3Account) { this.fail(); } else { - const user = await fetchUser(web3Account.user_id)(tx); - if (user?.id) { + const imbuer = await fetchImbuer(web3Account.imbuer_id)(tx); + if (imbuer?.id) { if ( signatureVerify( web3Account.challenge, @@ -53,11 +53,11 @@ export class Web3Strategy extends passport.Strategy { solution.address ).isValid ) { - this.success(user); + this.success(imbuer); } else { const challenge = uuid(); const [web3Account, _] = await upsertWeb3Challenge( - user, + imbuer, req.body.address, req.body.type, challenge @@ -128,34 +128,34 @@ polkadotJsAuthRouter.post("/", (req, res, next) => { next(err); } - // If no address can be found, create a `users` and then a + // If no address can be found, create an `imbuer` and then a // `federated_credential` - getOrCreateFederatedUser( + getOrCreateFederatedImbuer( req.body.meta.source, address, req.body.meta.name, - async (err: Error, user: User) => { + async (err: Error, imbuer: Imbuer) => { if (err) { next(err); } - if (!user) { - next(new Error("No user provided.")); + if (!imbuer) { + next(new Error("No imbuer provided.")); } try { - // create a `challenge` uuid and insert it into the users + // create a `challenge` uuid and insert it into the `imbuer` // table respond with the challenge db.transaction(async tx => { const challenge = uuid(); const [web3Account, isInsert] = await upsertWeb3Challenge( - user, address, req.body.type, challenge + imbuer, address, req.body.type, challenge )(tx); if (isInsert) { res.status(201); } - res.send({user, web3Account}); + res.send({imbuer, web3Account}); }); } catch (e) { next(new Error( diff --git a/api/src/models.ts b/api/src/models.ts index ce34426e..02c89f20 100644 --- a/api/src/models.ts +++ b/api/src/models.ts @@ -10,12 +10,12 @@ export type FederatedCredential = { export type Web3Account = { address: string, - user_id: number; + imbuer_id: number; type: string; challenge: string; }; -export type User = { +export type Imbuer = { id: number; display_name: string; web3Accounts: Web3Account[]; @@ -34,7 +34,7 @@ export type GrantProposal = { milestones: ProposedMilestone[]; required_funds: number; owner?: string; - user_id?: number; + imbuer_id?: number; category?: string | number; chain_project_id?: number; }; @@ -55,43 +55,43 @@ export type Project = { chain_project_id?: number; required_funds: number; owner?: string; - user_id?: string | number; + imbuer_id?: string | number; }; export const fetchWeb3Account = (address: string) => (tx: Knex.Transaction) => - tx("web3_accounts") + tx("web3_account") .select() .where({ address, }) .first(); -export const fetchUser = (id: number) => +export const fetchImbuer = (id: number) => (tx: Knex.Transaction) => - tx("users").where({ id }).first(); + tx("imbuer").where({ id }).first(); export const upsertWeb3Challenge = ( - user: User, + imbuer: Imbuer, address: string, type: string, challenge: string, ) => async (tx: Knex.Transaction): Promise<[web3Account: Web3Account, isInsert: boolean]> => { - let web3Account = await tx("web3_accounts") + let web3Account = await tx("web3_account") .select() .where({ - user_id: user?.id + imbuer_id: imbuer?.id }) .first(); if (!web3Account) { return [ ( - await tx("web3_accounts").insert({ + await tx("web3_account").insert({ address, - user_id: user.id, + imbuer_id: imbuer.id, type, challenge, }).returning("*") @@ -102,17 +102,17 @@ export const upsertWeb3Challenge = ( return [ ( - await tx("web3_accounts").update({ challenge }).where( - { user_id: user.id } + await tx("web3_account").update({ challenge }).where( + { imbuer_id: imbuer.id } ).returning("*") )[0], false ]; }; -export const insertUserByDisplayName = (displayName: string) => +export const insertImbuerByDisplayName = (displayName: string) => async (tx: Knex.Transaction) => ( - await tx("users").insert({ + await tx("imbuer").insert({ display_name: displayName }).returning("*") )[0]; @@ -138,10 +138,10 @@ export const fetchAllProjects = () => (tx: Knex.Transaction) => tx("projects").select(); -export const fetchUserProjects = (id: string | number) => +export const fetchImbuerProjects = (id: string | number) => (tx: Knex.Transaction) => tx("projects").select().where({ - user_id: id + imbuer_id: id }).select(); @@ -177,14 +177,14 @@ export const insertFederatedCredential = ( }).returning("*") )[0]; -export const getOrCreateFederatedUser = ( +export const getOrCreateFederatedImbuer = ( issuer: string, subject: string, displayName: string, done: CallableFunction ) => { db.transaction(async tx => { - let user: User; + let imbuer: Imbuer; try { /** @@ -196,26 +196,27 @@ export const getOrCreateFederatedUser = ( }).first(); /** - * If not, create the `usr`, then the `federated_credential` + * If not, create the `imbuer`, then the `federated_credential` */ if (!federated) { - user = await insertUserByDisplayName(displayName)(tx); - await insertFederatedCredential(user.id, issuer, subject)(tx); + imbuer = await insertImbuerByDisplayName(displayName)(tx); + await insertFederatedCredential(imbuer.id, issuer, subject)(tx); } else { - const user_ = await db.select().from("users").where({ + const imbuer_ = await db.select().from("imbuer").where({ id: federated.id }).first(); - if (!user_) { + if (!imbuer_) { throw new Error( - `Unable to find matching user by \`federated_credential.id\`: ${federated.id + `Unable to find matching imbuer by \`federated_credential.id\`: ${ + federated.id }` ); } - user = user_; + imbuer = imbuer_; } - done(null, user); + done(null, imbuer); } catch (err) { done(new Error( "Failed to upsert federated authentication.", diff --git a/api/src/routes/api/v1/users.ts b/api/src/routes/api/v1/imbuers.ts similarity index 87% rename from api/src/routes/api/v1/users.ts rename to api/src/routes/api/v1/imbuers.ts index 7f93d99e..91587e92 100644 --- a/api/src/routes/api/v1/users.ts +++ b/api/src/routes/api/v1/imbuers.ts @@ -13,7 +13,7 @@ router.get("/:id/projects", (req, res, next) => { db.transaction(async tx => { try { - const projects: models.Project[] = await models.fetchUserProjects(id)(tx); + const projects: models.Project[] = await models.fetchImbuerProjects(id)(tx); if (!projects) { return res.status(404).end(); } @@ -21,7 +21,7 @@ router.get("/:id/projects", (req, res, next) => { } catch (e) { next(new Error( - `Failed to fetch projects for user id: ${id}`, + `Failed to fetch projects for imbuer id: ${id}`, { cause: e as Error } )); } diff --git a/api/src/routes/api/v1/index.ts b/api/src/routes/api/v1/index.ts index e5b38bc1..a62681d9 100644 --- a/api/src/routes/api/v1/index.ts +++ b/api/src/routes/api/v1/index.ts @@ -2,12 +2,12 @@ import express from "express"; import db from "../../../db"; import * as models from "../../../models"; import projectsRouter from "./projects"; -import usersRouter from "./users"; +import imbuersRouter from "./imbuers"; import config from "../../../config"; const router = express.Router(); -router.get("/user", (req, res) => { +router.get("/me", (req, res) => { if (req.isAuthenticated()) { res.send(req.user); } else { @@ -22,6 +22,6 @@ router.get("/info", (req, res) => { }); router.use("/projects", projectsRouter); -router.use("/users", usersRouter); +router.use("/imbuers", imbuersRouter); export default router; diff --git a/api/src/routes/api/v1/projects.ts b/api/src/routes/api/v1/projects.ts index 2568aef9..d00fe7fd 100644 --- a/api/src/routes/api/v1/projects.ts +++ b/api/src/routes/api/v1/projects.ts @@ -8,7 +8,8 @@ type ProjectPkg = models.Project & { } /** - * FIXME: all of this is terriblme + * FIXME: all of this is terrible + * XXX: later... why is this terrible? Write better comments... */ const router = express.Router(); @@ -111,7 +112,7 @@ router.post("/", (req, res, next) => { category, required_funds, owner, - user_id: (req.user as any).id, + imbuer_id: (req.user as any).id, })(tx); if (!project.id) { @@ -166,7 +167,7 @@ router.put("/:id", (req, res, next) => { milestones, } = req.body.proposal as models.GrantProposal; - const user_id = (req.user as any).id; + const imbuer_id = (req.user as any).id; db.transaction(async tx => { try { @@ -177,7 +178,7 @@ router.put("/:id", (req, res, next) => { return res.status(404).end(); } - if (exists.user_id !== user_id) { + if (exists.imbuer_id !== imbuer_id) { return res.status(403).end(); } @@ -190,7 +191,7 @@ router.put("/:id", (req, res, next) => { chain_project_id, required_funds, owner, - // user_id, + // imbuer_id, })(tx); if (!project.id) { diff --git a/web/src/authentication/index.ts b/web/src/authentication/index.ts index 77ee37fc..388ea3bf 100644 --- a/web/src/authentication/index.ts +++ b/web/src/authentication/index.ts @@ -5,7 +5,7 @@ import { SignerResult } from "@polkadot/api/types"; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import * as config from "../config"; -import { User } from "../model"; +import { Imbuer } from "../model"; import { signWeb3Challenge } from "../utils/polkadot"; import authDialogContent from "./auth-dialog-content.html"; @@ -22,7 +22,7 @@ type AuthenticationDialogOptions = { export default class Authentication extends HTMLElement { - user?: User; + imbuer?: Imbuer; launchAuthDialog(opts?: AuthenticationDialogOptions) { const callback = opts?.callback || (() => {}); @@ -79,8 +79,8 @@ export default class Authentication extends HTMLElement { SignerResult; const account = state?.account as InjectedAccountWithMeta; - const user = state?.user as - User; + const imbuer = state?.imbuer as + Imbuer; const resp = await fetch( `/auth/web3/${account.meta.source}/callback`, @@ -94,8 +94,8 @@ export default class Authentication extends HTMLElement { } ); if (resp.ok) { - // authenticated. Set user. - this.user = user; + // authenticated. Set imbuer. + this.imbuer = imbuer; return this.web3AuthWorkflow("done", state); } else { // TODO: UX for 401 @@ -117,14 +117,14 @@ export default class Authentication extends HTMLElement { if (resp.ok) { // could be 200 or 201 - const { user, web3Account } = await resp.json(); + const { imbuer, web3Account } = await resp.json(); const signature = await signWeb3Challenge( account, web3Account.challenge ); if (signature) { return this.web3AuthWorkflow( "signed", - {...state, signature, user} + {...state, signature, imbuer} ); } else { // TODO: UX for no way to sign challenge? diff --git a/web/src/dapp/index.ts b/web/src/dapp/index.ts index 33e5fb57..addf4919 100644 --- a/web/src/dapp/index.ts +++ b/web/src/dapp/index.ts @@ -33,7 +33,7 @@ import Authentication from "../authentication"; import "../account-choice"; import AccountChoice from "../account-choice"; -import { User } from "../model"; +import { Imbuer } from "../model"; import { getWeb3Accounts } from "../utils/polkadot"; import html from "./index.html"; @@ -42,12 +42,12 @@ import { ApiPromise, WsProvider } from "@polkadot/api"; export type ImbueRequest = { - user: Promise; + imbuer: Promise; accounts: Promise; - apiInfo: Promise; + apiInfo: Promise; }; -export type polkadotJsApiInfo = { +export type PolkadotJsApiInfo = { api: ApiPromise; provider: WsProvider; webSockAddr: string; @@ -112,9 +112,9 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { $accountChoice: AccountChoice; $auth: Authentication; - user: Promise; + imbuer: Promise; accounts: Promise; - apiInfo: Promise; + apiInfo: Promise; constructor() { @@ -145,7 +145,7 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { this[CONTENT].getElementById("account-choice") as AccountChoice; - this.user = fetch(`${config.apiBase}/user`).then( + this.imbuer = fetch(`${config.apiBase}/me`).then( resp => { if (resp.ok) { return resp.json(); @@ -198,7 +198,7 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { initRouting() { window.addEventListener("popstate", e => { - console.log("popstate", window.location.href); + console.log("Dapp/top-level: popstate", window.location.href); this.route(window.location.pathname); this.$layout.closeDrawer("right"); }); @@ -257,14 +257,14 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { )); } - async initPolkadotJSAPI(): Promise { + async initPolkadotJSAPI(): Promise { const webSockAddr = (await fetch(`${config.apiBase}/info`).then( resp => resp.json() )).imbueNetworkWebsockAddr as string; const provider = new WsProvider(webSockAddr); provider.on("error", e => { - this.errorNotification(e); + // this.errorNotification(e); console.log(e); }); provider.on("disconnected", e => { @@ -312,7 +312,7 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { const route = new Route(`${config.context}/:app`, path); const request: ImbueRequest = { - user: this.user, + imbuer: this.imbuer, accounts: this.accounts, apiInfo: this.apiInfo, } @@ -348,6 +348,9 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { * such that ``s within `shadowRoot` can render the font. * * We do this here because this should be considered the entrypoint of the app. + * + * XXX: However, couldn't this also just be hard-coded in the index.html file + * instead? */ document.head.appendChild( document.createRange().createContextualFragment(` diff --git a/web/src/model.ts b/web/src/model.ts index d7ce808e..2f5338c4 100644 --- a/web/src/model.ts +++ b/web/src/model.ts @@ -19,7 +19,7 @@ export type DraftProposal = { milestones: DraftMilestone[]; required_funds: number; owner: string; - user_id?: number; + imbuer_id?: number; chain_project_id?: number; category?: string | number; }; @@ -30,7 +30,7 @@ export type DraftProposal = { export type Proposal = DraftProposal & { id: number; status: string; - user_id: number; + imbuer_id: number; create_block_number?: number; created: string; modified: string; @@ -52,7 +52,7 @@ export type Web3Account = { address: string; }; -export type User = { +export type Imbuer = { id: number; web3Accounts: Web3Account[]; }; @@ -93,7 +93,7 @@ export const fetchProjects = () => fetch( ); - export const fetchUserProjects = (userId: number) => fetch( - `${config.apiBase}/users/${userId}/projects/`, + export const fetchImbuerProjects = (imbuerId: number) => fetch( + `${config.apiBase}/imbuers/${imbuerId}/projects/`, {headers: config.getAPIHeaders} ); \ No newline at end of file diff --git a/web/src/my-projects/listing/index.ts b/web/src/my-projects/listing/index.ts index 9fa1872e..329aa815 100644 --- a/web/src/my-projects/listing/index.ts +++ b/web/src/my-projects/listing/index.ts @@ -3,11 +3,11 @@ import css from "./index.css"; import "../../proposals/proposal-item"; import ProposalItem from "../../proposals/proposal-item"; -import { Proposal, User } from "../../model"; +import { Proposal, Imbuer } from "../../model"; import * as model from "../../model"; import * as utils from "../../utils"; import * as config from "../../config"; -import type { ImbueRequest, polkadotJsApiInfo } from "../../dapp"; +import type { ImbueRequest, PolkadotJsApiInfo } from "../../dapp"; import authDialogContent from "../../dapp/auth-dialog-content.html"; const CONTENT = Symbol(); @@ -20,8 +20,8 @@ template.innerHTML = ` export default class List extends HTMLElement { - user?: User | null; - apiInfo?: polkadotJsApiInfo; + imbuer?: Imbuer | null; + apiInfo?: PolkadotJsApiInfo; private [CONTENT]: DocumentFragment; @@ -45,18 +45,18 @@ export default class List extends HTMLElement { async init(request: ImbueRequest) { this.apiInfo = await request.apiInfo; - this.user = await request.user; + this.imbuer = await request.imbuer; this.$list.innerHTML = ""; - await this.fetchUserProjects().then(projects => { + await this.fetchImbuerProjects().then(projects => { if (projects) { this.renderProjects(projects); } }); // Are we logged in? - if (!this.user) { + if (!this.imbuer) { this.wrapAuthentication(() => { location.reload() }); @@ -66,9 +66,9 @@ export default class List extends HTMLElement { wrapAuthentication(action: CallableFunction) { const callback = (state: any) => { - this.user = state.user; + this.imbuer = state.imbuer; console.log(state); - console.log(state.user); + console.log(state.imbuer); action(); } @@ -93,8 +93,8 @@ export default class List extends HTMLElement { } - async fetchUserProjects() { - const resp = await model.fetchUserProjects(this.user?.id!); + async fetchImbuerProjects() { + const resp = await model.fetchImbuerProjects(this.imbuer?.id!); if (resp.ok) { return await resp.json(); } else { diff --git a/web/src/proposals/detail/index.ts b/web/src/proposals/detail/index.ts index b3fd3a29..223272c4 100644 --- a/web/src/proposals/detail/index.ts +++ b/web/src/proposals/detail/index.ts @@ -11,14 +11,14 @@ import materialComponentsLink from "/material-components-link.html"; import materialIconsLink from "/material-icons-link.html"; import templateSrc from "./index.html"; import styles from "./index.css"; -import { DraftProposal, Proposal, User } from "../../model"; +import { DraftProposal, Proposal, Imbuer } from "../../model"; import * as model from "../../model"; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import { ApiPromise, WsProvider } from "@polkadot/api"; import { getWeb3Accounts } from "../../utils/polkadot"; import * as config from "../../config"; import * as utils from "../../utils"; -import type { ImbueRequest, polkadotJsApiInfo } from "../../dapp"; +import type { ImbueRequest, PolkadotJsApiInfo } from "../../dapp"; const CONTENT = Symbol(); @@ -31,20 +31,20 @@ template.innerHTML = ` `; export default class Detail extends HTMLElement { - user?: User | null; + imbuer?: Imbuer | null; project?: Proposal; $tabContentContainer: HTMLElement; $tabBar: HTMLElement; tabBar: MDCTabBar; openForVoting: boolean; - userIsInitiator: boolean; + imbuerIsInitiator: boolean; $projectName: HTMLElement; $projectWebsite: HTMLElement; $projectDescription: HTMLElement; $projectLogo: HTMLImageElement; $milestones: HTMLOListElement; - apiInfo?: polkadotJsApiInfo; + apiInfo?: PolkadotJsApiInfo; $fundsRequired: HTMLElement; contributionSubmissionForm: HTMLFormElement; @@ -70,7 +70,7 @@ export default class Detail extends HTMLElement { this.attachShadow({ mode: "open" }); this.openForVoting = false; - this.userIsInitiator = false; + this.imbuerIsInitiator = false; this[CONTENT] = template.content.cloneNode(true) as @@ -181,7 +181,7 @@ export default class Detail extends HTMLElement { } this.apiInfo = await request.apiInfo; - this.user = await request.user; + this.imbuer = await request.imbuer; /** * We await this here because if there's no draft, we don't want to @@ -203,10 +203,10 @@ export default class Detail extends HTMLElement { const projectOnChain: any = await (await this.apiInfo?.api.query.imbueProposals.projects(this.project?.chain_project_id)).toHuman(); if (projectOnChain) { - if (this.user) { - this.user?.web3Accounts.forEach(web3Account => { + if (this.imbuer) { + this.imbuer?.web3Accounts.forEach(web3Account => { if (web3Account.address == projectOnChain.initiator) { - this.userIsInitiator = true; + this.imbuerIsInitiator = true; } }); } @@ -216,13 +216,13 @@ export default class Detail extends HTMLElement { this.$submitMilestoneSelect.appendChild(this.milestoneFragment(milestone)); }); - if (this.userIsInitiator) { - this.contributionSubmissionForm.hidden = this.userIsInitiator; - this.$contribute.hidden = this.userIsInitiator; + if (this.imbuerIsInitiator) { + this.contributionSubmissionForm.hidden = this.imbuerIsInitiator; + this.$contribute.hidden = this.imbuerIsInitiator; if (projectOnChain.approvedForFunding) { - this.$submitMilestoneForm.hidden = !this.userIsInitiator - this.$submitMilestone.hidden = !this.userIsInitiator - this.$withdraw.hidden = !this.userIsInitiator + this.$submitMilestoneForm.hidden = !this.imbuerIsInitiator + this.$submitMilestone.hidden = !this.imbuerIsInitiator + this.$withdraw.hidden = !this.imbuerIsInitiator } } else if (projectOnChain.approvedForFunding) { this.openForVoting = projectOnChain.approvedForFunding; @@ -295,7 +295,7 @@ export default class Detail extends HTMLElement { wrapAuthentication(action: CallableFunction) { const callback = (state: any) => { - this.user = state.user; + this.imbuer = state.imbuer; action(); } @@ -453,7 +453,7 @@ export default class Detail extends HTMLElement { state?: Record ): Promise { const formData = new FormData(this.$voteSubmissionForm); - const userVote = (formData.get("vote-select") as string).toLowerCase() == "true"; + const imbuerVote = (formData.get("vote-select") as string).toLowerCase() == "true"; const milestoneKey = parseInt(formData.get("vote-milestone-select") as string); const api = this.apiInfo?.api; @@ -467,7 +467,7 @@ export default class Detail extends HTMLElement { const extrinsic = this.apiInfo?.api.tx.imbueProposals.voteOnMilestone( this.project?.chain_project_id, milestoneKey, - userVote + imbuerVote ); if (!extrinsic) { diff --git a/web/src/proposals/draft/editor/form/index.html b/web/src/proposals/draft/editor/form/index.html index 57de9118..72022105 100644 --- a/web/src/proposals/draft/editor/form/index.html +++ b/web/src/proposals/draft/editor/form/index.html @@ -15,7 +15,7 @@ name="imbu-name" type="text" helper="The name of the project." - maxlength="256" + maxlength="255" required> Project name @@ -52,7 +52,7 @@ ; $submitProposal: HTMLInputElement; $categorySelect: HTMLSelectElement; @@ -144,9 +144,10 @@ export default class Form extends HTMLElement { } async init(request: ImbueRequest) { - this.disabled = false; + this.disabled = true; this.reset(); - this.user = await request.user; + this.imbuer = await request.imbuer; + const projectId = this.projectId; /** @@ -155,9 +156,14 @@ export default class Form extends HTMLElement { * it. We say instead "preview" because they will be redirected to the * detail page, where they have other options. */ - this.$submitProposal.value = this.user - ? "Save Draft Proposal" - : "Preview Draft Proposal"; + this.$submitProposal.value = this.imbuer + /** + * TODO: "Save" should only be for when you're creating a new proposal. + * "Update" should be for when you're `PUT`ing changes to the draft. + * When in an `Update` state, we should also provide a "Cancel" option. + */ + ? `${projectId ? "Update" : "Save"} Draft` + : "Preview Draft"; this.accounts = request.accounts.then(accounts => { const $select = this.$web3AccountSelect; @@ -205,8 +211,6 @@ export default class Form extends HTMLElement { ); }); - const projectId = this.projectId; - try { if (projectId) { await this.setupExistingDraft(projectId); @@ -221,10 +225,12 @@ export default class Form extends HTMLElement { // maybe 500 page. this.addMilestoneItem(); setTimeout(() => this.$fields[0].focus(), 0); + } finally { + this.disabled = false; } // Are we logged in? - if (!this.user) { + if (!this.imbuer) { this.wrapAuthentication(() => { location.reload() }); @@ -250,9 +256,9 @@ export default class Form extends HTMLElement { draft = JSON.parse(String(local)); } else { - if (!this.user) { + if (!this.imbuer) { /** - * No user means not logged in, so don't bother trying to + * No imbuer means not logged in, so don't bother trying to * fetch, etc., because the server will only respond 401. */ this.redirectToNewDraft(); @@ -266,7 +272,7 @@ export default class Form extends HTMLElement { ).then(async resp => { if (resp.ok) { const project = await resp.json(); - if (this.user?.id === project.user_id) { + if (this.imbuer?.id === project.imbuer_id) { return project; } else { this.redirectToNewDraft(); @@ -341,7 +347,7 @@ export default class Form extends HTMLElement { * there won't be any undefined values. * * Note that `requiredFunds` is multiplied by 1e12, so that whatever the - * user submits here, we are ultimately submitting + * imbuer submits here, we are ultimately submitting * $number * 1_000_000_000_000 to the blockchain. */ proposalFromForm(formData: FormData): DraftProposal { @@ -538,16 +544,16 @@ export default class Form extends HTMLElement { account?: InjectedAccountWithMeta, ): Promise { - // if yes, go ahead and post the draft with the `user_id` - draft.user_id = this.user?.id; - let proposal; + // if yes, go ahead and post the draft with the `imbuer_id` + draft.imbuer_id = this.imbuer?.id; + let _proposal; + if (this.projectId) { - proposal = this.updateGrantProposal(draft,this.projectId); + await this.updateGrantProposal(draft, this.projectId); utils.redirect(`${ config.grantProposalsURL }/draft/preview?id=${this.projectId}`); - } else { const resp = await model.postDraftProposal(draft); if (resp.ok) { @@ -566,9 +572,9 @@ export default class Form extends HTMLElement { wrapAuthentication(action: CallableFunction) { const callback = (state: any) => { - this.user = state.user; + this.imbuer = state.imbuer; console.log(state); - console.log(state.user); + console.log(state.imbuer); action(); } diff --git a/web/src/proposals/draft/editor/index.ts b/web/src/proposals/draft/editor/index.ts index 6c5b93fa..3c9e4124 100644 --- a/web/src/proposals/draft/editor/index.ts +++ b/web/src/proposals/draft/editor/index.ts @@ -3,7 +3,7 @@ import css from "./index.css"; import "./form"; import ProposalsDraftEditorForm from "./form"; -import { User } from "../../../model"; +import { Imbuer } from "../../../model"; import { ImbueRequest } from "../../../dapp"; diff --git a/web/src/proposals/draft/index.html b/web/src/proposals/draft/index.html index 5e020f05..f19a2d0c 100644 --- a/web/src/proposals/draft/index.html +++ b/web/src/proposals/draft/index.html @@ -3,7 +3,7 @@ diff --git a/web/src/proposals/draft/preview/index.ts b/web/src/proposals/draft/preview/index.ts index 884dacfc..04de5e40 100644 --- a/web/src/proposals/draft/preview/index.ts +++ b/web/src/proposals/draft/preview/index.ts @@ -14,12 +14,12 @@ import html from "./index.html"; import localDraftDialogContent from "./local-draft-dialog-content.html"; import authDialogContent from "../../../dapp/auth-dialog-content.html"; import css from "./index.css"; -import type { DraftProposal, Proposal, User } from "../../../model"; +import type { DraftProposal, Proposal, Imbuer } from "../../../model"; import * as config from "../../../config"; import * as model from "../../../model"; import * as utils from "../../../utils"; -import { ImbueRequest, polkadotJsApiInfo } from "../../../dapp"; +import { ImbueRequest, PolkadotJsApiInfo } from "../../../dapp"; const CONTENT = Symbol(); @@ -36,7 +36,7 @@ template.innerHTML = ` export default class Preview extends HTMLElement { project?: Proposal; address?: string; - user?: User | null; + imbuer?: Imbuer | null; private [CONTENT]: DocumentFragment; $tabBar: HTMLElement; @@ -57,7 +57,7 @@ export default class Preview extends HTMLElement { $milestones: HTMLOListElement; accounts?: InjectedAccountWithMeta[]; - apiInfo?: polkadotJsApiInfo; + apiInfo?: PolkadotJsApiInfo; constructor() { @@ -153,6 +153,8 @@ export default class Preview extends HTMLElement { /** * This is just for a11y. Do not use this value unless you know what * you're doing. + * + * To be clear, this `href` is not used. We are using the history API. */ this.$edit.href = `${config.context}${config.grantProposalsURL }/draft?id=${this.projectId}`; @@ -161,7 +163,7 @@ export default class Preview extends HTMLElement { async init(request: ImbueRequest) { const projectId = this.projectId; - this.user = await request.user; + this.imbuer = await request.imbuer; this.accounts = await request.accounts; this.apiInfo = await request.apiInfo; @@ -192,26 +194,26 @@ export default class Preview extends HTMLElement { - if (this.user) { + if (this.imbuer) { /** - * User is logged in with a session. + * Imbuer is logged in with a session. * - * XXX: We have to assume that since the user is logged in at + * XXX: We have to assume that since the imbuer is logged in at * this point, there's no reason to "save" -- only edit or * finalize. */ /** - * Toggling save false because if user is logged in, we + * Toggling save false because if imbuer is logged in, we * automatically save and refresh for them. */ this.toggleSave = false; /** - * Should only be able to edit or finalize if user - * is the user_id on the project. + * Should only be able to edit or finalize if imbuer + * is the imbuer_id on the project. */ - if (this.user?.id !== this.project?.user_id) { + if (this.imbuer?.id !== this.project?.imbuer_id) { this.toggleFinalize = false; this.toggleEdit = false; } @@ -277,7 +279,7 @@ export default class Preview extends HTMLElement { }/draft?id=${this.projectId}`); }; - if (this.projectId === "local-draft" || this.user) { + if (this.projectId === "local-draft" || this.imbuer) { edit(); } else { this.wrapAuthentication(edit); @@ -301,7 +303,7 @@ export default class Preview extends HTMLElement { } }; - if (!this.user) { + if (!this.imbuer) { this.wrapAuthentication(save); } else { /** @@ -310,7 +312,7 @@ export default class Preview extends HTMLElement { * to save? * * I think we should just handle it in the background -- - * user logs in, and gets redirected back here to `local-draft` + * imbuer logs in, and gets redirected back here to `local-draft` * but we detect that from the URL and then go through a save * workflow (the same one we would do in this else block), * which would redirect them to this "preview" page, but with a @@ -321,8 +323,8 @@ export default class Preview extends HTMLElement { }); this.$finalize.addEventListener("click", e => { - const userOwnsDraft = (this.user && (this.user.id === this.project?.user_id)); - if (!this.user && !userOwnsDraft) { + const imbuerOwnsDraft = (this.imbuer && (this.imbuer.id === this.project?.imbuer_id)); + if (!this.imbuer && !imbuerOwnsDraft) { this.wrapAuthentication(() => { // call this handler "recursively" this.$finalize.click(); @@ -335,7 +337,7 @@ export default class Preview extends HTMLElement { wrapAuthentication(action: CallableFunction) { const callback = (state: any) => { - this.user = state.user; + this.imbuer = state.imbuer; action(); } diff --git a/web/src/styles/common.css b/web/src/styles/common.css index 455f6c52..05ef81ab 100644 --- a/web/src/styles/common.css +++ b/web/src/styles/common.css @@ -54,6 +54,10 @@ --mdc-theme-text-secondary-on-background: var(--mdc-theme-secondary); --mdc-theme-text-icon-on-background: var(--mdc-text-field-label-ink-color); + /** This seems to fix the bug where letter "tails" are being cut off + inexplicably */ + --mdc-typography-subtitle1-font-size: 1.05rem; + /* --mdc-dialog-content-ink-color: var(--mdc-text-field-ink-color); --mdc-dialog-scrim-color: rgba(200, 200, 255, 0.5); */ From 42b6824682048ae8499cf1e4e06f8cdc0b2a9c8b Mon Sep 17 00:00:00 2001 From: Tom Brennan Date: Sat, 9 Apr 2022 12:24:45 -0400 Subject: [PATCH 2/4] bugfix for missing preview update --- web/src/proposals/draft/editor/form/index.ts | 14 +++++++--- web/src/proposals/draft/preview/index.ts | 28 +++++++++++++------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/web/src/proposals/draft/editor/form/index.ts b/web/src/proposals/draft/editor/form/index.ts index 9bbf63dd..1dc2e1fc 100644 --- a/web/src/proposals/draft/editor/form/index.ts +++ b/web/src/proposals/draft/editor/form/index.ts @@ -527,7 +527,10 @@ export default class Form extends HTMLElement { } else { // TODO: UX for submission failed // maybe `this.dialog(...)` - this.disabled = false; + // this.disabled = false; + throw new Error("Failed to create draft proposal", { + cause: {...resp, name: "fetch", message: resp.statusText} + }); } } @@ -537,6 +540,9 @@ export default class Form extends HTMLElement { const proposal: Proposal = await resp.json(); return proposal; } + throw new Error("Failed to update draft proposal", { + cause: {...resp, name: "fetch", message: resp.statusText} + }); } async submitGrantProposal( @@ -546,14 +552,14 @@ export default class Form extends HTMLElement { // if yes, go ahead and post the draft with the `imbuer_id` draft.imbuer_id = this.imbuer?.id; - let _proposal; - if (this.projectId) { + // TODO: UX/error handling -- we probably don't want to redirect + // here if the update went wrong. await this.updateGrantProposal(draft, this.projectId); utils.redirect(`${ config.grantProposalsURL - }/draft/preview?id=${this.projectId}`); + }/draft/preview?id=${this.projectId}&update`); } else { const resp = await model.postDraftProposal(draft); if (resp.ok) { diff --git a/web/src/proposals/draft/preview/index.ts b/web/src/proposals/draft/preview/index.ts index 04de5e40..eae468bb 100644 --- a/web/src/proposals/draft/preview/index.ts +++ b/web/src/proposals/draft/preview/index.ts @@ -179,7 +179,7 @@ export default class Preview extends HTMLElement { * We await this here because if there's no draft, we don't want to * bother with any other confusing and/or expensive tasks. */ - await this.fetchProject(projectId).then(project => { + await this.fetchProject(projectId, this.updateDeclared).then(project => { if (project) { this.renderProject(project); } else { @@ -237,8 +237,8 @@ export default class Preview extends HTMLElement { )); } - async fetchProject(projectId: string) { - if (this.project) { + async fetchProject(projectId: string, force = false) { + if (!force && this.project) { return this.project; } const resp = await model.fetchProject(projectId); @@ -261,6 +261,14 @@ export default class Preview extends HTMLElement { return null; } + get updateDeclared() { + return window.location.search + .split("?")[1] + ?.split("&") + .map(str => str.split("=")) + .find(([k, _]) => k === "update")?.[0] === "update"; + } + bind() { this.shadowRoot?.addEventListener("MDCTabBar:activated", e => { const detail = (e as CustomEvent).detail; @@ -473,7 +481,8 @@ export default class Preview extends HTMLElement { // this.$["about-project"].innerText = proposal.name; this.$projectName.innerText = project.name; this.$projectWebsite.innerHTML = ` - ${project.website + ${ + project.website } `; this.$projectDescription.innerHTML = @@ -491,11 +500,12 @@ export default class Preview extends HTMLElement { pending_actions - ${milestone.name - } - ${milestone.percentage_to_unlock}% - + ${ + milestone.name + } + ${ + milestone.percentage_to_unlock + }% `) From 958db00a7782da97a407f0f33a86ce0c749502ea Mon Sep 17 00:00:00 2001 From: Tom Brennan Date: Sat, 9 Apr 2022 15:29:14 -0400 Subject: [PATCH 3/4] `ImbueRequest` => `DappRequest` --- web/nginx/templates/imbue.conf.template | 4 ++ web/src/dapp/index.ts | 10 +++- web/src/model.ts | 22 +++++++-- web/src/my-projects/index.ts | 4 +- web/src/my-projects/listing/index.ts | 12 ++--- web/src/proposals/detail/index.ts | 24 ++++++---- web/src/proposals/draft/editor/form/index.ts | 4 +- web/src/proposals/draft/editor/index.ts | 4 +- web/src/proposals/draft/index.ts | 4 +- web/src/proposals/draft/preview/index.ts | 48 ++----------------- .../preview/local-draft-dialog-content.html | 21 -------- web/src/proposals/index.ts | 4 +- 12 files changed, 67 insertions(+), 94 deletions(-) delete mode 100644 web/src/proposals/draft/preview/local-draft-dialog-content.html diff --git a/web/nginx/templates/imbue.conf.template b/web/nginx/templates/imbue.conf.template index 5d3cace0..b78f465d 100644 --- a/web/nginx/templates/imbue.conf.template +++ b/web/nginx/templates/imbue.conf.template @@ -2,6 +2,10 @@ server { listen ${HTTP_PORT}; listen [::]:${HTTP_PORT}; +# We're not doing HTTPS here because we're assuming +# that we'll be deployed behind a load balancer. But +# this is how you might do it: +# ### # Change 302 to 301 to make the redirect permanent. ### return 302 https://$host:${HTTPS_PORT}$request_uri; ###} diff --git a/web/src/dapp/index.ts b/web/src/dapp/index.ts index addf4919..e72b6631 100644 --- a/web/src/dapp/index.ts +++ b/web/src/dapp/index.ts @@ -41,7 +41,7 @@ import styles from "./index.css"; import { ApiPromise, WsProvider } from "@polkadot/api"; -export type ImbueRequest = { +export type DappRequest = { imbuer: Promise; accounts: Promise; apiInfo: Promise; @@ -311,7 +311,7 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { } const route = new Route(`${config.context}/:app`, path); - const request: ImbueRequest = { + const request: DappRequest = { imbuer: this.imbuer, accounts: this.accounts, apiInfo: this.apiInfo, @@ -329,6 +329,12 @@ window.customElements.define("imbu-dapp", class extends HTMLElement { } switch (route.data?.app) { + /** + * This is the app intended to be used for creating/editing project + * proposal drafts. Once the draft has been finalized, it's been + * promoted to a "project" and should be dealt with in the "projects" + * app (below). + */ case "proposals": this.$pages.select("proposals"); (this.$pages.selected as Proposals).route(route.tail, request); diff --git a/web/src/model.ts b/web/src/model.ts index 2f5338c4..37b144e3 100644 --- a/web/src/model.ts +++ b/web/src/model.ts @@ -37,12 +37,28 @@ export type Proposal = DraftProposal & { milestones: Milestone[]; } +/** Models the Project type from the chain */ +export type ImbueProject = { + name: string; + logo: string; + description: string; + website: string; + milestones: Milestone[]; + contributions: any[]; // in the future, something like `Contribution[]` + required_funds: number; + withdrawn_funds: number; + /// The account that will receive the funds if the campaign is successful + initiator: string, // public address + create_block_number: number; + approved_for_funding: boolean; +} + /** * Models a "milestone" saved to the db (and also as it will appear on chain). */ export type Milestone = DraftMilestone & { - milestone_index?: number; - project_id: number; + milestone_key?: number; + project_key: number; is_approved: boolean; created: string; modified: string; @@ -93,7 +109,7 @@ export const fetchProjects = () => fetch( ); - export const fetchImbuerProjects = (imbuerId: number) => fetch( + export const fetchProjectsByImbuerId = (imbuerId: number) => fetch( `${config.apiBase}/imbuers/${imbuerId}/projects/`, {headers: config.getAPIHeaders} ); \ No newline at end of file diff --git a/web/src/my-projects/index.ts b/web/src/my-projects/index.ts index 0df4b8f4..7df0c39b 100644 --- a/web/src/my-projects/index.ts +++ b/web/src/my-projects/index.ts @@ -10,7 +10,7 @@ import "../my-projects/listing"; import List from "../my-projects/listing"; import * as utils from "../utils"; -import { ImbueRequest } from "../dapp"; +import { DappRequest } from "../dapp"; const template = document.createElement("template"); @@ -44,7 +44,7 @@ export default class MyProjects extends HTMLElement { this.shadowRoot?.appendChild(this[CONTENT]); } - route(path: string | null, request: ImbueRequest) { + route(path: string | null, request: DappRequest) { if (!path) { this.$pages.select("listing"); (this.$pages.selected as List).init(request); diff --git a/web/src/my-projects/listing/index.ts b/web/src/my-projects/listing/index.ts index 329aa815..ad6d994f 100644 --- a/web/src/my-projects/listing/index.ts +++ b/web/src/my-projects/listing/index.ts @@ -7,7 +7,7 @@ import { Proposal, Imbuer } from "../../model"; import * as model from "../../model"; import * as utils from "../../utils"; import * as config from "../../config"; -import type { ImbueRequest, PolkadotJsApiInfo } from "../../dapp"; +import type { DappRequest, PolkadotJsApiInfo } from "../../dapp"; import authDialogContent from "../../dapp/auth-dialog-content.html"; const CONTENT = Symbol(); @@ -43,7 +43,7 @@ export default class List extends HTMLElement { this.shadowRoot?.appendChild(this[CONTENT]); } - async init(request: ImbueRequest) { + async init(request: DappRequest) { this.apiInfo = await request.apiInfo; this.imbuer = await request.imbuer; @@ -67,8 +67,8 @@ export default class List extends HTMLElement { wrapAuthentication(action: CallableFunction) { const callback = (state: any) => { this.imbuer = state.imbuer; - console.log(state); - console.log(state.imbuer); + // console.log(state); + // console.log(state.imbuer); action(); } @@ -86,7 +86,7 @@ export default class List extends HTMLElement { handler: () => {}, label: "Continue using local storage" } - } + }, }, } )); @@ -94,7 +94,7 @@ export default class List extends HTMLElement { async fetchImbuerProjects() { - const resp = await model.fetchImbuerProjects(this.imbuer?.id!); + const resp = await model.fetchProjectsByImbuerId(this.imbuer?.id!); if (resp.ok) { return await resp.json(); } else { diff --git a/web/src/proposals/detail/index.ts b/web/src/proposals/detail/index.ts index 223272c4..3765b6ca 100644 --- a/web/src/proposals/detail/index.ts +++ b/web/src/proposals/detail/index.ts @@ -18,7 +18,7 @@ import { ApiPromise, WsProvider } from "@polkadot/api"; import { getWeb3Accounts } from "../../utils/polkadot"; import * as config from "../../config"; import * as utils from "../../utils"; -import type { ImbueRequest, PolkadotJsApiInfo } from "../../dapp"; +import type { DappRequest, PolkadotJsApiInfo } from "../../dapp"; const CONTENT = Symbol(); @@ -169,7 +169,7 @@ export default class Detail extends HTMLElement { this.bind(); } - async init(request: ImbueRequest) { + async init(request: DappRequest) { const projectId = this.projectId; if (!projectId) { /** @@ -200,7 +200,13 @@ export default class Detail extends HTMLElement { } }); - const projectOnChain: any = await (await this.apiInfo?.api.query.imbueProposals.projects(this.project?.chain_project_id)).toHuman(); + const projectOnChain = await ( + await this.apiInfo + ?.api + .query + .imbueProposals + .projects(this.project?.chain_project_id) + ).toHuman() as unknown as model.ImbueProject; if (projectOnChain) { if (this.imbuer) { @@ -219,13 +225,13 @@ export default class Detail extends HTMLElement { if (this.imbuerIsInitiator) { this.contributionSubmissionForm.hidden = this.imbuerIsInitiator; this.$contribute.hidden = this.imbuerIsInitiator; - if (projectOnChain.approvedForFunding) { + if (projectOnChain.approved_for_funding) { this.$submitMilestoneForm.hidden = !this.imbuerIsInitiator this.$submitMilestone.hidden = !this.imbuerIsInitiator this.$withdraw.hidden = !this.imbuerIsInitiator } - } else if (projectOnChain.approvedForFunding) { - this.openForVoting = projectOnChain.approvedForFunding; + } else if (projectOnChain.approved_for_funding) { + this.openForVoting = projectOnChain.approved_for_funding; this.$voteSubmissionForm.hidden = !this.openForVoting this.$vote.hidden = !this.openForVoting; } else { @@ -235,13 +241,13 @@ export default class Detail extends HTMLElement { } } - milestoneFragment(milestone: any) { + milestoneFragment(milestone: model.Milestone) { return document.createRange().createContextualFragment(` + value="${milestone.milestone_key}"> ${milestone.name} - ${milestone.percentageToUnlock + ${milestone.percentage_to_unlock }% `); diff --git a/web/src/proposals/draft/editor/form/index.ts b/web/src/proposals/draft/editor/form/index.ts index 1dc2e1fc..55c2ca8f 100644 --- a/web/src/proposals/draft/editor/form/index.ts +++ b/web/src/proposals/draft/editor/form/index.ts @@ -14,7 +14,7 @@ import { getWeb3Accounts } from "../../../../utils/polkadot"; import * as model from "../../../../model"; import * as config from "../../../../config"; import * as utils from '../../../../utils'; -import { ImbueRequest } from '../../../../dapp'; +import { DappRequest } from '../../../../dapp'; import authDialogContent from "../../../../dapp/auth-dialog-content.html"; declare global { @@ -143,7 +143,7 @@ export default class Form extends HTMLElement { `); } - async init(request: ImbueRequest) { + async init(request: DappRequest) { this.disabled = true; this.reset(); this.imbuer = await request.imbuer; diff --git a/web/src/proposals/draft/editor/index.ts b/web/src/proposals/draft/editor/index.ts index 3c9e4124..91c3737a 100644 --- a/web/src/proposals/draft/editor/index.ts +++ b/web/src/proposals/draft/editor/index.ts @@ -4,7 +4,7 @@ import css from "./index.css"; import "./form"; import ProposalsDraftEditorForm from "./form"; import { Imbuer } from "../../../model"; -import { ImbueRequest } from "../../../dapp"; +import { DappRequest } from "../../../dapp"; const template = document.createElement("template"); @@ -36,7 +36,7 @@ export default class Editor extends HTMLElement { this.shadowRoot?.appendChild(this[CONTENT]); } - init(request: ImbueRequest) { + init(request: DappRequest) { return this.$form?.init(request); } } diff --git a/web/src/proposals/draft/index.ts b/web/src/proposals/draft/index.ts index 53022236..0f445ad8 100644 --- a/web/src/proposals/draft/index.ts +++ b/web/src/proposals/draft/index.ts @@ -12,7 +12,7 @@ import ProposalsDraftPreview from "./preview"; import * as config from "../../config"; import * as utils from "../../utils"; -import { ImbueRequest } from "../../dapp"; +import { DappRequest } from "../../dapp"; const template = document.createElement("template"); @@ -46,7 +46,7 @@ export default class ProposalsDraft extends HTMLElement { this.shadowRoot?.appendChild(this[CONTENT]); } - route(path: string | null, request: ImbueRequest) { + route(path: string | null, request: DappRequest) { if (!path) { this.$pages.select("editor"); (this.$pages.selected as ProposalsDraftEditor).init(request); diff --git a/web/src/proposals/draft/preview/index.ts b/web/src/proposals/draft/preview/index.ts index eae468bb..617c410d 100644 --- a/web/src/proposals/draft/preview/index.ts +++ b/web/src/proposals/draft/preview/index.ts @@ -1,17 +1,15 @@ import { marked } from "marked"; import "@pojagi/hoquet/lib/dialog/dialog"; -import Dialog, { ActionConfig } from "@pojagi/hoquet/lib/dialog/dialog"; +import Dialog from "@pojagi/hoquet/lib/dialog/dialog"; import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import { MDCTabBar } from "@material/tab-bar"; -import type { SignerResult, SubmittableExtrinsic } from "@polkadot/api/types"; -import { ApiPromise, WsProvider } from "@polkadot/api"; +import type { SubmittableExtrinsic } from "@polkadot/api/types"; import { web3FromSource } from "@polkadot/extension-dapp"; import type { ISubmittableResult } from "@polkadot/types/types"; import materialComponentsLink from "/material-components-link.html"; import materialIconsLink from "/material-icons-link.html"; import html from "./index.html"; -import localDraftDialogContent from "./local-draft-dialog-content.html"; import authDialogContent from "../../../dapp/auth-dialog-content.html"; import css from "./index.css"; import type { DraftProposal, Proposal, Imbuer } from "../../../model"; @@ -19,7 +17,7 @@ import type { DraftProposal, Proposal, Imbuer } from "../../../model"; import * as config from "../../../config"; import * as model from "../../../model"; import * as utils from "../../../utils"; -import { ImbueRequest, PolkadotJsApiInfo } from "../../../dapp"; +import { DappRequest, PolkadotJsApiInfo } from "../../../dapp"; const CONTENT = Symbol(); @@ -160,7 +158,7 @@ export default class Preview extends HTMLElement { }/draft?id=${this.projectId}`; } - async init(request: ImbueRequest) { + async init(request: DappRequest) { const projectId = this.projectId; this.imbuer = await request.imbuer; @@ -287,49 +285,13 @@ export default class Preview extends HTMLElement { }/draft?id=${this.projectId}`); }; - if (this.projectId === "local-draft" || this.imbuer) { + if (this.imbuer) { edit(); } else { this.wrapAuthentication(edit); } }); - this.$save.addEventListener("click", e => { - const save = async () => { - if (this.project) { - const resp = await model.postDraftProposal(this.project); - if (resp.ok) { - const project = await resp.json(); - utils.redirect(`${config.grantProposalsURL - }/draft/preview?id=${project.id}`); - } else { - // TODO: UX for bad request posting draft - console.warn("Bad request posting draft", this.project); - } - } else { - // shouldn't happen? - } - }; - - if (!this.imbuer) { - this.wrapAuthentication(save); - } else { - /** - * Save and redirect back to legit projectId? Or do we want to - * bring them back here so that they can decide whether or not - * to save? - * - * I think we should just handle it in the background -- - * imbuer logs in, and gets redirected back here to `local-draft` - * but we detect that from the URL and then go through a save - * workflow (the same one we would do in this else block), - * which would redirect them to this "preview" page, but with a - * legit `projectId` instead of `local-draft`. - */ - save(); - } - }); - this.$finalize.addEventListener("click", e => { const imbuerOwnsDraft = (this.imbuer && (this.imbuer.id === this.project?.imbuer_id)); if (!this.imbuer && !imbuerOwnsDraft) { diff --git a/web/src/proposals/draft/preview/local-draft-dialog-content.html b/web/src/proposals/draft/preview/local-draft-dialog-content.html deleted file mode 100644 index 51f65f7f..00000000 --- a/web/src/proposals/draft/preview/local-draft-dialog-content.html +++ /dev/null @@ -1,21 +0,0 @@ -

- You are viewing a temporary draft that has been saved in your browser's - local storage. Feel free to click the edit button on the - bottom of this page to continue editing, or save to - save the draft to be able to access it from another browser or computer. -

-

- Note that saving is optional, and that in order to save - you will be asked to sign in using either a traditional service (i.e., web - 2.0, like Google, Github, etc.) or using the - polkadot{.js} web3 extension. Either way, this will create an - account on our server with a traditional, centralised database, not the - Imbue Network blockchain. Saving would allow you to, for example, - share your draft before finalizing. -

-

- Only finalised proposals are eligible for funding. If you wish to go ahead - and finalise your proposal without creating an account, - your polkadot{.js} extension will ask you to cryptographically sign the - transaction to commit your proposal to the Imbue Network blockchain. -

diff --git a/web/src/proposals/index.ts b/web/src/proposals/index.ts index 1db70a46..b455dd0d 100644 --- a/web/src/proposals/index.ts +++ b/web/src/proposals/index.ts @@ -14,7 +14,7 @@ import "../proposals/detail"; import Detail from "../proposals/detail"; import * as utils from "../utils"; -import { ImbueRequest } from "../dapp"; +import { DappRequest } from "../dapp"; const template = document.createElement("template"); @@ -48,7 +48,7 @@ export default class Proposals extends HTMLElement { this.shadowRoot?.appendChild(this[CONTENT]); } - route(path: string | null, request: ImbueRequest) { + route(path: string | null, request: DappRequest) { if (!path) { this.$pages.select("listing"); (this.$pages.selected as List).init(); From 6e47fe8f58912197d8a75cb68895a37746e00303 Mon Sep 17 00:00:00 2001 From: Tom Brennan Date: Sat, 9 Apr 2022 15:43:46 -0400 Subject: [PATCH 4/4] re-singularize table names --- .../db/migrations/20220114134044_initial.ts | 22 +++++++++---------- api/src/models.ts | 20 ++++++++--------- api/src/routes/api/v1/projects.ts | 3 ++- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/api/src/db/migrations/20220114134044_initial.ts b/api/src/db/migrations/20220114134044_initial.ts index a89c1709..bc8ce504 100644 --- a/api/src/db/migrations/20220114134044_initial.ts +++ b/api/src/db/migrations/20220114134044_initial.ts @@ -37,7 +37,7 @@ export async function up(knex: Knex): Promise { auditFields(knex, builder); }).then(onUpdateTrigger(knex, web3AccountsTableName)); - const federatedCredsTableName = "federated_credentials"; + const federatedCredsTableName = "federated_credential"; await knex.schema.createTable(federatedCredsTableName, (builder) => { builder.integer("id"); builder.text("issuer"); @@ -75,8 +75,8 @@ export async function up(knex: Knex): Promise { * create_block_number: BlockNumber, * } */ - const projectsTableName = "projects"; - await knex.schema.createTable(projectsTableName, (builder: Knex.CreateTableBuilder) => { + const projectTableName = "project"; + await knex.schema.createTable(projectTableName, (builder: Knex.CreateTableBuilder) => { builder.increments("id", { primaryKey: true }); builder.text("name"); // project name builder.text("logo"); // URL or dataURL (i.e., base64 encoded) @@ -150,7 +150,7 @@ export async function up(knex: Knex): Promise { builder.bigInteger("create_block_number").nullable(); //.unsigned(); auditFields(knex, builder); - }).then(onUpdateTrigger(knex, projectsTableName)); + }).then(onUpdateTrigger(knex, projectTableName)); /** * pub struct Milestone { @@ -161,13 +161,13 @@ export async function up(knex: Knex): Promise { * is_approved: bool * } */ - const milestonesTableName = "milestones"; - await knex.schema.createTable(milestonesTableName, (builder) => { + const milestoneTableName = "milestone"; + await knex.schema.createTable(milestoneTableName, (builder) => { builder.integer("milestone_index"); builder.integer("project_id").notNullable(); builder.primary(["project_id","milestone_index"]); builder.foreign("project_id") - .references("projects.id") + .references("project.id") .onDelete("CASCADE") .onUpdate("CASCADE"); @@ -176,7 +176,7 @@ export async function up(knex: Knex): Promise { builder.boolean("is_approved").defaultTo(false); auditFields(knex, builder); - }).then(onUpdateTrigger(knex, milestonesTableName)); + }).then(onUpdateTrigger(knex, milestoneTableName)); /** * TODO: ? votes and contributions @@ -230,10 +230,10 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists("milestones"); - await knex.schema.dropTableIfExists("projects"); + await knex.schema.dropTableIfExists("milestone"); + await knex.schema.dropTableIfExists("project"); await knex.schema.dropTableIfExists("project_status"); - await knex.schema.dropTableIfExists("federated_credentials"); + await knex.schema.dropTableIfExists("federated_credential"); await knex.schema.dropTableIfExists("web3_account"); await knex.schema.dropTableIfExists("imbuer"); await knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION); diff --git a/api/src/models.ts b/api/src/models.ts index 02c89f20..b31db14d 100644 --- a/api/src/models.ts +++ b/api/src/models.ts @@ -119,12 +119,12 @@ export const insertImbuerByDisplayName = (displayName: string) => export const insertProject = (project: Project) => async (tx: Knex.Transaction) => ( - await tx("projects").insert(project).returning("*") + await tx("project").insert(project).returning("*") )[0]; export const updateProject = (id: string | number, project: Project) => async (tx: Knex.Transaction) => ( - await tx("projects") + await tx("project") .update(project) .where({ id }) .returning("*") @@ -132,15 +132,15 @@ export const updateProject = (id: string | number, project: Project) => export const fetchProject = (id: string | number) => (tx: Knex.Transaction) => - tx("projects").select().where({ id }).first(); + tx("project").select().where({ id }).first(); export const fetchAllProjects = () => (tx: Knex.Transaction) => - tx("projects").select(); + tx("project").select(); export const fetchImbuerProjects = (id: string | number) => (tx: Knex.Transaction) => - tx("projects").select().where({ + tx("project").select().where({ imbuer_id: id }).select(); @@ -156,23 +156,23 @@ export const insertMilestones = ( })); return (tx: Knex.Transaction) => - tx("milestones").insert(values).returning("*"); + tx("milestone").insert(values).returning("*"); }; export const deleteMilestones = (project_id: string | number) => (tx: Knex.Transaction) => - tx("milestones").delete().where({ project_id }); + tx("milestone").delete().where({ project_id }); export const fetchProjectMilestones = (id: string | number) => (tx: Knex.Transaction) => - tx("milestones").select().where({ project_id: id }); + tx("milestone").select().where({ project_id: id }); export const insertFederatedCredential = ( id: number, issuer: string, subject: string, ) => async (tx: Knex.Transaction) => ( - await tx("federated_credentials").insert({ + await tx("federated_credential").insert({ id, issuer, subject }).returning("*") )[0]; @@ -190,7 +190,7 @@ export const getOrCreateFederatedImbuer = ( /** * Do we already have a federated_credential ? */ - const federated = await tx("federated_credentials").select().where({ + const federated = await tx("federated_credential").select().where({ issuer, subject, }).first(); diff --git a/api/src/routes/api/v1/projects.ts b/api/src/routes/api/v1/projects.ts index d00fe7fd..dd750252 100644 --- a/api/src/routes/api/v1/projects.ts +++ b/api/src/routes/api/v1/projects.ts @@ -66,7 +66,8 @@ const validateProposal = (proposal: models.GrantProposal) => { } const entries = Object.entries(proposal); - if (entries.filter(([_,v]) => { + if ( + entries.filter(([_,v]) => { // undefined not allowed return v === void 0; }).length