diff --git a/package.json b/package.json index 6a05398d80..e47f75a4f1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,12 @@ "pioneer/packages/apps*", "pioneer/packages/page*", "pioneer/packages/react*", + "pioneer/packages/joy-utils", + "pioneer/packages/joy-members", + "pioneer/packages/joy-pages", + "pioneer/packages/joy-election", + "pioneer/packages/joy-proposals", + "pioneer/packages/joy-roles", "utils/api-examples" ], "resolutions": { diff --git a/pioneer/.eslintignore b/pioneer/.eslintignore index e24e58eaba..293b8583b2 100644 --- a/pioneer/.eslintignore +++ b/pioneer/.eslintignore @@ -2,16 +2,10 @@ **/coverage/* **/node_modules/* packages/old-apps/* -packages/joy-members/* -packages/joy-election/* packages/joy-forum/* packages/joy-help/* packages/joy-media/* -packages/joy-pages/* -packages/joy-proposals/* -packages/joy-roles/* packages/joy-settings/* -packages/joy-utils/* packages/joy-utils-old/* .eslintrc.js i18next-scanner.config.js diff --git a/pioneer/.eslintrc.js b/pioneer/.eslintrc.js index ada8259c1e..4b3de541bd 100644 --- a/pioneer/.eslintrc.js +++ b/pioneer/.eslintrc.js @@ -13,14 +13,22 @@ module.exports = { rules: { ...base.rules, '@typescript-eslint/no-explicit-any': 'off', - 'react/prop-types': 'off', 'new-cap': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/ban-ts-comment': 'error', // why only required in VSCode!?!? is eslint plugin not working like eslint commandline? // Or are we having to add this because of new versions of eslint-config-* ? 'no-console': 'off', - 'header/header': 'off' // Temporary disable polkadot's rule + // Override some extended config rules: + 'camelcase': 'off', + 'header/header': 'off', + 'sort-keys': 'off', + 'react/jsx-sort-props': 'off', + 'react/jsx-max-props-per-line': 'off', + 'sort-destructure-keys/sort-destructure-keys': 'off', + '@typescript-eslint/unbound-method': 'warn', // Doesn't work well with our version of Formik, see: https://github.com/formium/formik/issues/2589 + 'react-hooks/exhaustive-deps': 'warn', // Causes more issues than it solves currently + 'no-void': 'off' // Otherwise we cannot mark unhandles promises }, // isolate pioneer from monorepo eslint rules root: true diff --git a/pioneer/.storybook/webpack.config.js b/pioneer/.storybook/webpack.config.js index 6c02952773..f32af8608d 100644 --- a/pioneer/.storybook/webpack.config.js +++ b/pioneer/.storybook/webpack.config.js @@ -37,7 +37,7 @@ config.module.rules.push({ use: [ { loader: require.resolve('babel-loader'), - options: require('@polkadot/dev-react/config/babel') + options: require('@polkadot/dev/config/babel') }, ], }); diff --git a/pioneer/package.json b/pioneer/package.json index b050147ce3..91a97d68d7 100644 --- a/pioneer/package.json +++ b/pioneer/package.json @@ -25,7 +25,6 @@ "test": "echo \"skipping tests\"", "vanitygen": "node packages/app-accounts/scripts/vanitygen.js", "start": "yarn clean && cd packages/apps && webpack --config webpack.config.js", - "generate-schemas": "json2ts -i packages/joy-types/src/schemas/role.schema.json -o packages/joy-types/src/schemas/role.schema.ts", "build-storybook": "build-storybook -c .storybook", "storybook": "start-storybook -s ./packages/apps/public -p 3001" }, @@ -40,7 +39,7 @@ "@types/chart.js": "^2.9.23", "@types/file-saver": "^2.0.1", "@types/i18next": "^13.0.0", - "@types/jest": "^26.0.7", + "@types/jest": "^26.0.10", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-dom": "^16.9.8", @@ -74,11 +73,13 @@ "@storybook/addon-actions": "^5.2.5", "@storybook/addon-console": "^1.2.1", "@storybook/react": "^5.2.5", - "json-schema-to-typescript": "^7.1.0", "storybook-react-router": "^1.0.8", "typescript": "^3.9.7", "eslint-plugin-header": "^3.0.0", - "eslint-plugin-sort-destructure-keys": "^1.3.5" + "eslint-plugin-sort-destructure-keys": "^1.3.5", + "jest": "^26.4.1", + "ts-jest": "^26.2.0", + "tsconfig-paths-webpack-plugin": "^3.2.0" }, "dependencies": { "@types/lodash": "^4.14.138", diff --git a/pioneer/packages/apps-config/src/api/spec/index.ts b/pioneer/packages/apps-config/src/api/spec/index.ts index d65760b541..22e2122aeb 100644 --- a/pioneer/packages/apps-config/src/api/spec/index.ts +++ b/pioneer/packages/apps-config/src/api/spec/index.ts @@ -10,6 +10,7 @@ import encointerNodeTeeproxy from './encointer-node-teeproxy'; import kulupu from './kulupu'; import nodeTemplate from './node-template'; import stablePoc from './stable-poc'; +import joystreamNode from './joystream-node'; export default { acala, @@ -21,5 +22,6 @@ export default { kulupu, 'node-template': nodeTemplate, 'stable-poc': stablePoc, - stable_poc: stablePoc + stable_poc: stablePoc, + 'joystream-node': joystreamNode }; diff --git a/pioneer/packages/apps-config/src/api/spec/joystream-node.ts b/pioneer/packages/apps-config/src/api/spec/joystream-node.ts new file mode 100644 index 0000000000..7e436a96d4 --- /dev/null +++ b/pioneer/packages/apps-config/src/api/spec/joystream-node.ts @@ -0,0 +1,3 @@ +import { types } from '@joystream/types'; + +export default types; diff --git a/pioneer/packages/apps-config/src/settings/endpoints.ts b/pioneer/packages/apps-config/src/settings/endpoints.ts index 0b1b5dd42c..c7d662f519 100644 --- a/pioneer/packages/apps-config/src/settings/endpoints.ts +++ b/pioneer/packages/apps-config/src/settings/endpoints.ts @@ -29,6 +29,11 @@ function createDev (t: TFunction): LinkOption[] { function createLive (t: TFunction): LinkOption[] { return [ + { + info: 'joystream', + text: t('rpc.joystream', 'Joystream (Current Testnet, hosted by Jsgenesis)', { ns: 'apps-config' }), + value: 'wss://rome-rpc-endpoint.joystream.org:9944' + }, { dnslink: 'polkadot', info: 'polkadot', diff --git a/pioneer/packages/apps-config/src/ui/logos/index.ts b/pioneer/packages/apps-config/src/ui/logos/index.ts index 93f37ff13f..b4d0036715 100644 --- a/pioneer/packages/apps-config/src/ui/logos/index.ts +++ b/pioneer/packages/apps-config/src/ui/logos/index.ts @@ -17,6 +17,7 @@ import nodeNodle from './nodes/nodle.svg'; import nodePolkadot from './nodes/polkadot-circle.svg'; import nodePolkadotJs from './nodes/polkadot-js.svg'; import nodeSubstrate from './nodes/substrate-hexagon.svg'; +import nodeJoystream from './nodes/joystream-node.svg'; // extensions import extensionPolkadotJs from './extensions/polkadot-js.svg'; @@ -48,7 +49,8 @@ const nodeLogos: Record = [ ['Nodle Chain Node', nodeNodle], ['parity-polkadot', nodePolkadot], ['polkadot-js', nodePolkadotJs], - ['substrate-node', nodeSubstrate] + ['substrate-node', nodeSubstrate], + ['joystream-node', nodeJoystream] ].reduce((logos, [node, logo]): Record => ({ ...logos, [(node as string).toLowerCase().replace(/-/g, ' ')]: logo diff --git a/pioneer/packages/apps-config/src/ui/logos/nodes/joystream-node.svg b/pioneer/packages/apps-config/src/ui/logos/nodes/joystream-node.svg new file mode 100755 index 0000000000..67dd1b71f2 --- /dev/null +++ b/pioneer/packages/apps-config/src/ui/logos/nodes/joystream-node.svg @@ -0,0 +1 @@ +Icon-mono-white-1bg-blue \ No newline at end of file diff --git a/pioneer/packages/apps-routing/src/index.ts b/pioneer/packages/apps-routing/src/index.ts index cc5eab5943..55ad59d500 100644 --- a/pioneer/packages/apps-routing/src/index.ts +++ b/pioneer/packages/apps-routing/src/index.ts @@ -9,69 +9,52 @@ import appSettings from '@polkadot/ui-settings'; // When adding here, also ensure to add to Dummy.tsx import accounts from './accounts'; -import claims from './claims'; -import contracts from './contracts'; -import council from './council'; -// import dashboard from './dashboard'; -import democracy from './democracy'; import explorer from './explorer'; import extrinsics from './extrinsics'; -import genericAsset from './generic-asset'; import js from './js'; -import parachains from './parachains'; -import poll from './poll'; import settings from './settings'; -import society from './society'; -import staking from './staking'; import storage from './storage'; import sudo from './sudo'; -import techcomm from './techcomm'; import toolbox from './toolbox'; import transfer from './transfer'; -import treasury from './treasury'; +// Joy packages +import members from './joy-members'; +import { terms, privacyPolicy } from './joy-pages'; +import election from './joy-election'; +import proposals from './joy-proposals'; +import roles from './joy-roles'; export default function create (t: (key: string, text: string, options: { ns: string }) => T): Routes { return appSettings.uiMode === 'light' ? [ - // dashboard, - explorer(t), - accounts(t), - claims(t), - poll(t), - transfer(t), - genericAsset(t), - null, - staking(t), - democracy(t), - council(t), - // TODO Not sure about the inclusion of treasury, parachains & society here + members(t), + roles(t), + election(t), + proposals(t), null, + transfer(t), + accounts(t), settings(t) ] : [ - // dashboard(t), - explorer(t), - accounts(t), - claims(t), - poll(t), - transfer(t), - genericAsset(t), + members(t), + roles(t), + election(t), + proposals(t), null, - staking(t), - democracy(t), - council(t), - treasury(t), - techcomm(t), - parachains(t), - society(t), + transfer(t), + accounts(t), + settings(t), null, - contracts(t), + explorer(t), storage(t), extrinsics(t), + js(t), + toolbox(t), sudo(t), null, - settings(t), - toolbox(t), - js(t) + // Those are hidden + terms(t), + privacyPolicy(t) ]; } diff --git a/pioneer/packages/apps-routing/src/joy-election.ts b/pioneer/packages/apps-routing/src/joy-election.ts new file mode 100644 index 0000000000..cbff554d99 --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-election.ts @@ -0,0 +1,17 @@ +import { Route } from './types'; + +import Election from '@polkadot/joy-election/index'; +import SidebarSubtitle from '@polkadot/joy-election/SidebarSubtitle'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Election, + display: { + needsApi: ['query.council.activeCouncil', 'query.councilElection.stage'] + }, + text: t('nav.election', 'Council', { ns: 'apps-routing' }), + icon: 'university', + name: 'council', + SubtitleComponent: SidebarSubtitle + }; +} diff --git a/pioneer/packages/apps-routing/src/joy-members.ts b/pioneer/packages/apps-routing/src/joy-members.ts new file mode 100644 index 0000000000..e8c3712d4d --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-members.ts @@ -0,0 +1,15 @@ +import { Route } from './types'; + +import Members from '@polkadot/joy-members/index'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Members, + display: { + needsApi: ['query.members.nextMemberId'] + }, + icon: 'users', + name: 'members', + text: t('nav.membership', 'Membership', { ns: 'apps-routing' }) + }; +} diff --git a/pioneer/packages/apps-routing/src/joy-pages.ts b/pioneer/packages/apps-routing/src/joy-pages.ts new file mode 100644 index 0000000000..a39470e322 --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-pages.ts @@ -0,0 +1,27 @@ +import { Route } from './types'; + +import { ToS, Privacy } from '@polkadot/joy-pages/index'; + +export function terms (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: ToS, + display: { + isHidden: true + }, + text: t('nav.terms', 'Terms of Service', { ns: 'apps-routing' }), + icon: 'file', + name: 'pages/tos' + }; +} + +export function privacyPolicy (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Privacy, + display: { + isHidden: true + }, + text: t('nav.privacy', 'Privacy Policy', { ns: 'apps-routing' }), + icon: 'file', + name: 'pages/privacy' + }; +} diff --git a/pioneer/packages/apps-routing/src/joy-proposals.ts b/pioneer/packages/apps-routing/src/joy-proposals.ts new file mode 100644 index 0000000000..4c76e21eb8 --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-proposals.ts @@ -0,0 +1,16 @@ +import { Route } from './types'; + +import Proposals from '@polkadot/joy-proposals/index'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Proposals, + display: { + needsApi: ['query.proposalsEngine.proposalCount'] + }, + text: t('nav.proposals', 'Proposals', { ns: 'apps-routing' }), + icon: 'tasks', + name: 'proposals' + // TODO: useCounter with active proposals count? (could be a nice addition) + }; +} diff --git a/pioneer/packages/old-apps/apps-routing/src/joy-roles.ts b/pioneer/packages/apps-routing/src/joy-roles.ts similarity index 50% rename from pioneer/packages/old-apps/apps-routing/src/joy-roles.ts rename to pioneer/packages/apps-routing/src/joy-roles.ts index 7131a2fce8..8664da1e52 100644 --- a/pioneer/packages/old-apps/apps-routing/src/joy-roles.ts +++ b/pioneer/packages/apps-routing/src/joy-roles.ts @@ -1,9 +1,9 @@ -import { Routes } from './types'; +import { Route } from './types'; import Roles from '@polkadot/joy-roles/index'; -export default ([ - { +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { Component: Roles, display: { needsApi: [ @@ -11,10 +11,8 @@ export default ([ 'query.storageWorkingGroup.mint' ] }, - i18n: { - defaultValue: 'Working groups' - }, + text: t('nav.roles', 'Working groups', { ns: 'apps-routing' }), icon: 'users', name: 'working-groups' - } -] as Routes); + }; +} diff --git a/pioneer/packages/apps-routing/src/types.ts b/pioneer/packages/apps-routing/src/types.ts index 6dce432d8c..4a6695fef2 100644 --- a/pioneer/packages/apps-routing/src/types.ts +++ b/pioneer/packages/apps-routing/src/types.ts @@ -24,6 +24,8 @@ export interface Route { name: string; text: string; useCounter?: () => number | string | null; + // Joystream-specific + SubtitleComponent?: React.ComponentType; } export type Routes = (Route | null)[]; diff --git a/pioneer/packages/apps/public/favicon.ico b/pioneer/packages/apps/public/favicon.ico index ec3c167a5b..a3d783d522 100644 Binary files a/pioneer/packages/apps/public/favicon.ico and b/pioneer/packages/apps/public/favicon.ico differ diff --git a/pioneer/packages/apps/public/index.html b/pioneer/packages/apps/public/index.html index c9efd8fa30..d8318bd0c3 100644 --- a/pioneer/packages/apps/public/index.html +++ b/pioneer/packages/apps/public/index.html @@ -6,6 +6,17 @@ <%= htmlWebpackPlugin.options.PAGE_TITLE %> + <% if (htmlWebpackPlugin.options.IS_PROD) { %> + + + + <% } %> diff --git a/pioneer/packages/apps/src/Apps.tsx b/pioneer/packages/apps/src/Apps.tsx index f0995e28e5..c331f9f976 100644 --- a/pioneer/packages/apps/src/Apps.tsx +++ b/pioneer/packages/apps/src/Apps.tsx @@ -21,6 +21,9 @@ import SideBar from './SideBar'; import WarmUp from './WarmUp'; import { WindowDimensionsCtx } from './WindowDimensions'; +/* Joystream-specific */ +import TopBar from './JoyTopBar/TopBar'; + interface SidebarState { isCollapsed: boolean; isMenu: boolean; @@ -96,7 +99,10 @@ function Apps ({ className = '' }: Props): React.ReactElement { toggleMenu={_toggleMenu} /> - +
+ + +
@@ -222,4 +228,11 @@ export default React.memo(styled(Apps)` opacity: 1; } } + + .apps--Main { + flex-grow: 1; + min-height: 100vh; + overflow-x: hidden; + overflow-y: auto; + } `); diff --git a/pioneer/packages/apps/src/Content/index.tsx b/pioneer/packages/apps/src/Content/index.tsx index 66ea117e5d..cf2dc00224 100644 --- a/pioneer/packages/apps/src/Content/index.tsx +++ b/pioneer/packages/apps/src/Content/index.tsx @@ -16,6 +16,11 @@ import { useTranslation } from '../translate'; import NotFound from './NotFound'; import Status from './Status'; +// Joystream-specific +// We must use transport provider here instead of /apps/src/index +// to avoid "Cannot create Transport: The Substrate API is not ready yet." error +import { TransportProvider } from '@polkadot/joy-utils/react/context'; + interface Props { className?: string; } @@ -60,11 +65,25 @@ function Content ({ className }: Props): React.ReactElement { ? : ( - + { needsApi + // Add transport provider for routes that need the api + // (the above condition makes sure it's aleady initialized at this point) + ? ( + + + + ) + : ( + + ) } ) } @@ -78,15 +97,11 @@ function Content ({ className }: Props): React.ReactElement { } export default React.memo(styled(Content)` - background: #f5f4f3; - flex-grow: 1; - height: 100%; - min-height: 100vh; - overflow-x: hidden; - overflow-y: auto; + background: rgba(250, 250, 250); padding: 0 1.5rem; position: relative; width: 100%; + height: 100%; @media(max-width: 768px) { padding: 0 0.5rem; diff --git a/pioneer/packages/apps/src/JoyTopBar/TopBar.tsx b/pioneer/packages/apps/src/JoyTopBar/TopBar.tsx new file mode 100644 index 0000000000..807141b73c --- /dev/null +++ b/pioneer/packages/apps/src/JoyTopBar/TopBar.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; +import { InputAddress } from '@polkadot/react-components'; +import { Available } from '@polkadot/react-query'; +import styled from 'styled-components'; +import { useApi } from '@polkadot/react-hooks'; + +const StyledTopBar = styled.div` + padding: 0.75rem; + background-color: #3f3f3f; + border-bottom: 1px solid #d4d4d5; + text-align: right; + margin: 0; + + &.NoMyAddress { + background-color: #ffeb83; + color: #000; + text-align: center; + } + + .ui--InputAddress { + display: inline-block; + } +`; + +function JoyTopBar () { + const { + allAccounts, + myAddress + } = useMyMembership(); + + const { isApiReady } = useApi(); + + if (!isApiReady) { + return null; + } + + const balance = Balance: ; + const labelExtra = myAddress + ? + : 'No key selected'; + + return Object.keys(allAccounts || {}).length ? ( + + + + ) : null; +} + +export default JoyTopBar; diff --git a/pioneer/packages/apps/src/SideBar/ChainInfo.tsx b/pioneer/packages/apps/src/SideBar/ChainInfo.tsx index 0e417ee81e..737bed301b 100644 --- a/pioneer/packages/apps/src/SideBar/ChainInfo.tsx +++ b/pioneer/packages/apps/src/SideBar/ChainInfo.tsx @@ -24,7 +24,7 @@ function ChainInfo ({ className = '', onClick }: Props): React.ReactElement
diff --git a/pioneer/packages/apps/src/SideBar/Item.tsx b/pioneer/packages/apps/src/SideBar/Item.tsx index abcd801144..629f9c9c19 100644 --- a/pioneer/packages/apps/src/SideBar/Item.tsx +++ b/pioneer/packages/apps/src/SideBar/Item.tsx @@ -79,12 +79,15 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement - {text} + + {text} + { SubtitleComponent && } + {!!count && (
@@ -159,7 +138,7 @@ export default React.memo(styled(SideBar)` .apps--SideBar { align-items: center; - background: #4f5255; + background: #3f3f3f; box-sizing: border-box; display: flex; flex-flow: column; @@ -221,7 +200,7 @@ export default React.memo(styled(SideBar)` } .apps--SideBar-collapse { - background: #4f5255; + background: #3f3f3f; bottom: 0; left: 0; padding: 0.75rem 0 .75rem 0.65rem; diff --git a/pioneer/packages/apps/src/index.tsx b/pioneer/packages/apps/src/index.tsx index 1654bc4541..df91ca7b15 100644 --- a/pioneer/packages/apps/src/index.tsx +++ b/pioneer/packages/apps/src/index.tsx @@ -20,6 +20,9 @@ import settings from '@polkadot/ui-settings'; import Apps from './Apps'; import WindowDimensions from './WindowDimensions'; +/* Joystream-specific */ +import { MyMembershipProvider, MyAccountProvider } from '@polkadot/joy-utils/react/context'; + const rootId = 'root'; const rootElement = document.getElementById(rootId); const theme = { theme: settings.uiTheme }; @@ -38,19 +41,23 @@ store.each((_, key): void => { ReactDOM.render( - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + , rootElement diff --git a/pioneer/packages/apps/webpack.base.config.js b/pioneer/packages/apps/webpack.base.config.js index 4fb1d34154..14d96e3999 100644 --- a/pioneer/packages/apps/webpack.base.config.js +++ b/pioneer/packages/apps/webpack.base.config.js @@ -69,6 +69,17 @@ function createWebpack (ENV, context) { } ] }, + { + test: /\.s[ac]ss$/i, + use: [ + // Creates `style` nodes from JS strings + 'style-loader', + // Translates CSS into CommonJS + 'css-loader', + // Compiles Sass to CSS + 'sass-loader' + ] + }, { include: /node_modules/, test: /\.css$/, @@ -98,7 +109,7 @@ function createWebpack (ENV, context) { ] }, { - exclude: [/semantic-ui-css/], + // Original config had "exclude: [/semantic-ui-css/]" test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], use: [ { @@ -112,7 +123,8 @@ function createWebpack (ENV, context) { ] }, { - exclude: [/semantic-ui-css/], + // Original config had "exclude: [/semantic-ui-css/]", because Semantic UI Icons + // are not used in polkadot-js/apps repository, but they are used in ours test: [/\.eot$/, /\.ttf$/, /\.svg$/, /\.woff$/, /\.woff2$/], use: [ { @@ -123,15 +135,6 @@ function createWebpack (ENV, context) { } } ] - }, - { - include: [/semantic-ui-css/], - test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.eot$/, /\.ttf$/, /\.svg$/, /\.woff$/, /\.woff2$/], - use: [ - { - loader: require.resolve('null-loader') - } - ] } ] }, diff --git a/pioneer/packages/apps/webpack.config.js b/pioneer/packages/apps/webpack.config.js index f31f22bbe1..041f8f8356 100644 --- a/pioneer/packages/apps/webpack.config.js +++ b/pioneer/packages/apps/webpack.config.js @@ -20,7 +20,8 @@ module.exports = merge( devtool: process.env.BUILD_ANALYZE ? 'source-map' : false, plugins: [ new HtmlWebpackPlugin({ - PAGE_TITLE: 'Polkadot/Substrate Portal', + IS_PROD: ENV === 'production', + PAGE_TITLE: 'Joystream Network Portal', inject: true, template: path.join(context, `${hasPublic ? 'public/' : ''}index.html`) }) diff --git a/pioneer/packages/joy-election/.skip-build b/pioneer/packages/joy-election/.skip-build deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pioneer/packages/joy-election/package.json b/pioneer/packages/joy-election/package.json index 2bee08d3ff..17c74cd58c 100644 --- a/pioneer/packages/joy-election/package.json +++ b/pioneer/packages/joy-election/package.json @@ -7,9 +7,9 @@ "author": "Joystream contributors", "maintainers": [], "dependencies": { - "@babel/runtime": "^7.7.1", - "@polkadot/react-components": "0.37.0-beta.63", - "@polkadot/react-query": "0.37.0-beta.63", + "@babel/runtime": "^7.10.5", + "@polkadot/react-components": "0.51.1", + "@polkadot/react-query": "0.51.1", "@polkadot/joy-utils": "^0.1.1" } } diff --git a/pioneer/packages/joy-election/src/Applicant.tsx b/pioneer/packages/joy-election/src/Applicant.tsx index b48c3291f3..0626c53e1e 100644 --- a/pioneer/packages/joy-election/src/Applicant.tsx +++ b/pioneer/packages/joy-election/src/Applicant.tsx @@ -4,24 +4,25 @@ import { Table } from 'semantic-ui-react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { AccountId } from '@polkadot/types/interfaces'; import { formatBalance } from '@polkadot/util'; import CandidatePreview from './CandidatePreview'; import translate from './translate'; -import { calcTotalStake } from '@polkadot/joy-utils/index'; +import { calcTotalStake } from '@polkadot/joy-utils/functions/misc'; import { ElectionStake } from '@joystream/types/council'; type Props = ApiProps & I18nProps & { index: number; accountId: AccountId; stake?: ElectionStake; + isVotingStage: boolean; }; class Applicant extends React.PureComponent { render () { - const { index, accountId, stake } = this.props; + const { index, accountId, stake, isVotingStage } = this.props; const voteUrl = `/council/votes?applicantId=${accountId.toString()}`; return ( @@ -33,9 +34,11 @@ class Applicant extends React.PureComponent { {formatBalance(calcTotalStake(stake))} - - Vote - + { isVotingStage && ( + + Vote + + ) } ); } diff --git a/pioneer/packages/joy-election/src/Applicants.tsx b/pioneer/packages/joy-election/src/Applicants.tsx index e84c85f3d5..3ee84db4d7 100644 --- a/pioneer/packages/joy-election/src/Applicants.tsx +++ b/pioneer/packages/joy-election/src/Applicants.tsx @@ -1,49 +1,68 @@ import React from 'react'; -import { Table } from 'semantic-ui-react'; +import { Table, Message } from 'semantic-ui-react'; import BN from 'bn.js'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { AccountId } from '@polkadot/types/interfaces'; +import { Option } from '@polkadot/types'; import { formatNumber } from '@polkadot/util'; import translate from './translate'; import Applicant from './Applicant'; import ApplyForm from './ApplyForm'; -import Section from '@polkadot/joy-utils/Section'; -import { queryToProp } from '@polkadot/joy-utils/index'; -import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; +import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { ElectionStage } from '@joystream/types/src/council'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & I18nProps & MyAccountProps & { +type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & { candidacyLimit?: BN; applicants?: Array; + stage?: Option; }; class Applicants extends React.PureComponent { - private renderTable = (applicants: Array) => ( - - - - # - Applicant - Total stake - Actions - - - {applicants.map((accountId, index) => ( - - ))} -
- ) + private renderTable = (applicants: Array) => { + const isVotingStage = this.props.stage?.unwrapOr(undefined)?.isOfType('Voting') || false; + + return ( + + + + # + Applicant + Total stake + { isVotingStage && ( + Actions + ) } + + + {applicants.map((accountId, index) => ( + + ))} +
+ ); + } render () { - const { myAddress, applicants = [], candidacyLimit = new BN(0) } = this.props; + const { myAddress, applicants = [], candidacyLimit = new BN(0), stage } = this.props; const title = Applicants {applicants.length}/{formatNumber(candidacyLimit)}; return <>
- + { stage?.unwrapOr(undefined)?.isOfType('Announcing') + ? ( + + ) + : ( + + Applying to council is only possible during Announcing stage. + + ) + }
{!applicants.length @@ -59,6 +78,7 @@ class Applicants extends React.PureComponent { export default translate( withCalls( queryToProp('query.councilElection.candidacyLimit'), - queryToProp('query.councilElection.applicants') + queryToProp('query.councilElection.applicants'), + queryToProp('query.councilElection.stage') )(withMyAccount(Applicants)) ); diff --git a/pioneer/packages/joy-election/src/ApplyForm.tsx b/pioneer/packages/joy-election/src/ApplyForm.tsx index f058fc992c..518d95ce51 100644 --- a/pioneer/packages/joy-election/src/ApplyForm.tsx +++ b/pioneer/packages/joy-election/src/ApplyForm.tsx @@ -3,21 +3,21 @@ import React from 'react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { Labelled } from '@polkadot/react-components/index'; import { Balance } from '@polkadot/types/interfaces'; import translate from './translate'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import InputStake from '@polkadot/joy-utils/InputStake'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import InputStake from '@polkadot/joy-utils/react/components/InputStake'; import { ElectionStake } from '@joystream/types/council'; -import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index'; -import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; +import { calcTotalStake, ZERO } from '@polkadot/joy-utils/functions/misc'; +import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; type Props = ApiProps & I18nProps & MyAddressProps & { minStake?: Balance; alreadyStaked?: ElectionStake; - myBalance?: Balance; }; type State = { @@ -48,15 +48,16 @@ class ApplyForm extends React.PureComponent { isValid={isStakeValid} onChange={this.onChangeStake} /> - - - +
+ + + +
); } @@ -71,10 +72,9 @@ class ApplyForm extends React.PureComponent { private onChangeStake = (stake?: BN): void => { stake = stake || ZERO; - const { myBalance = ZERO } = this.props; - const isStakeLteBalance = stake.lte(myBalance); const isStakeGteMinStake = stake.add(this.alreadyStaked()).gte(this.minStake()); - const isStakeValid = !stake.isZero() && isStakeGteMinStake && isStakeLteBalance; + const isStakeValid = !stake.isZero() && isStakeGteMinStake; + this.setState({ stake, isStakeValid }); } } @@ -88,8 +88,6 @@ export default withMulti( ['query.councilElection.minCouncilStake', { propName: 'minStake' }], ['query.councilElection.applicantStakes', - { paramName: 'myAddress', propName: 'alreadyStaked' }], - ['query.balances.freeBalance', - { paramName: 'myAddress', propName: 'myBalance' }] + { paramName: 'myAddress', propName: 'alreadyStaked' }] ) ); diff --git a/pioneer/packages/joy-election/src/CandidatePreview.tsx b/pioneer/packages/joy-election/src/CandidatePreview.tsx index 9e6571efb6..bab07182e7 100644 --- a/pioneer/packages/joy-election/src/CandidatePreview.tsx +++ b/pioneer/packages/joy-election/src/CandidatePreview.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import AddressMini from '@polkadot/react-components/AddressMiniJoy'; -import MemberByAccount from '@polkadot/joy-utils/MemberByAccountPreview'; +import AddressMini from '@polkadot/react-components/AddressMini'; +import MemberByAccount from '@polkadot/joy-utils/react/components/MemberByAccountPreview'; import { AccountId } from '@polkadot/types/interfaces'; import styled from 'styled-components'; diff --git a/pioneer/packages/joy-election/src/Council.tsx b/pioneer/packages/joy-election/src/Council.tsx index b81c144883..f4e9e9597d 100644 --- a/pioneer/packages/joy-election/src/Council.tsx +++ b/pioneer/packages/joy-election/src/Council.tsx @@ -2,22 +2,22 @@ import React from 'react'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Table } from 'semantic-ui-react'; import { formatBalance } from '@polkadot/util'; import CouncilCandidate from './CandidatePreview'; -import { calcBackersStake } from '@polkadot/joy-utils/index'; +import { calcBackersStake } from '@polkadot/joy-utils/functions/misc'; import { Seat } from '@joystream/types/council'; import translate from './translate'; -import Section from '@polkadot/joy-utils/Section'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & -I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { council?: Seat[]; }; -type State = {}; +type State = Record; class Council extends React.PureComponent { state: State = {}; @@ -53,9 +53,10 @@ class Council extends React.PureComponent { render () { const { council = [] } = this.props; + // console.log({ council }); return ( -
+
{!council.length ? Council is not elected yet : this.renderTable(council)}
); diff --git a/pioneer/packages/joy-election/src/Dashboard.tsx b/pioneer/packages/joy-election/src/Dashboard.tsx index c7f64ef811..da6fffbd9c 100644 --- a/pioneer/packages/joy-election/src/Dashboard.tsx +++ b/pioneer/packages/joy-election/src/Dashboard.tsx @@ -3,18 +3,19 @@ import React from 'react'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Option } from '@polkadot/types'; import { BlockNumber, Balance } from '@polkadot/types/interfaces'; -import { Bubble } from '@polkadot/react-components/index'; +import { Label, Icon } from 'semantic-ui-react'; import { formatNumber, formatBalance } from '@polkadot/util'; -import Section from '@polkadot/joy-utils/Section'; -import { queryToProp } from '@polkadot/joy-utils/index'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; import { ElectionStage, Seat } from '@joystream/types/council'; import translate from './translate'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { bestNumber?: BN; activeCouncil?: Seat[]; @@ -34,7 +35,7 @@ type Props = ApiProps & I18nProps & { stage?: Option; }; -type State = {}; +type State = Record; class Dashboard extends React.PureComponent { state: State = {}; @@ -45,12 +46,17 @@ class Dashboard extends React.PureComponent { const title = `Council ${activeCouncil.length > 0 ? '' : '(not elected)'}`; return
- - {activeCouncil.length} - - - {formatNumber(p.termEndsAt)} - + + + +
; } @@ -59,13 +65,16 @@ class Dashboard extends React.PureComponent { let stageName: string | undefined; let stageEndsAt: BlockNumber | undefined; + if (stage && stage.isSome) { const stageValue = stage.value as ElectionStage; + stageEndsAt = stageValue.value as BlockNumber; // contained value stageName = stageValue.type; // name of Enum variant } let leftBlocks: BN | undefined; + if (stageEndsAt && bestNumber) { leftBlocks = stageEndsAt.sub(bestNumber); } @@ -76,20 +85,28 @@ class Dashboard extends React.PureComponent { const title = <>Election ({stateText}); return
- - {formatNumber(round)} - - {isRunning && <> - - {stageName} - - - {formatNumber(leftBlocks)} - - - {formatNumber(stageEndsAt)} - - } + + + {isRunning && <> + + + + } +
; } @@ -98,33 +115,46 @@ class Dashboard extends React.PureComponent { const isAutoStart = (p.autoStart || false).valueOf(); return
- - {isAutoStart ? 'Yes' : 'No'} - - - {formatNumber(p.newTermDuration)} - - - {formatNumber(p.candidacyLimit)} - - - {formatNumber(p.councilSize)} - - - {formatBalance(p.minCouncilStake)} - - - {formatBalance(p.minVotingStake)} - - - {formatNumber(p.announcingPeriod)} blocks - - - {formatNumber(p.votingPeriod)} blocks - - - {formatNumber(p.revealingPeriod)} blocks - + + + + + + + + + + + + +
; } diff --git a/pioneer/packages/joy-election/src/Reveals.tsx b/pioneer/packages/joy-election/src/Reveals.tsx index 6a20dad6f9..7b51d2cb0c 100644 --- a/pioneer/packages/joy-election/src/Reveals.tsx +++ b/pioneer/packages/joy-election/src/Reveals.tsx @@ -1,23 +1,23 @@ import React from 'react'; -import { AppProps, I18nProps } from '@polkadot/react-components/types'; +import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { AccountId } from '@polkadot/types/interfaces'; import { Input, Labelled, InputAddress } from '@polkadot/react-components/index'; import translate from './translate'; -import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/index'; +import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/functions/misc'; import { accountIdsToOptions, hashVote } from './utils'; -import TxButton from '@polkadot/joy-utils/TxButton'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; import { findVoteByHash } from './myVotesStore'; -import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; +import { RouteProps } from 'react-router-dom'; // AppsProps is needed to get a location from the route. -type Props = AppProps & ApiProps & I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { applicantId?: string | null; applicants?: AccountId[]; - location: any; }; type State = { @@ -30,8 +30,9 @@ class RevealVoteForm extends React.PureComponent { constructor (props: Props) { super(props); let { applicantId, location } = this.props; - applicantId = applicantId || getUrlParam(location, 'applicantId'); - const hashedVote = getUrlParam(location, 'hashedVote'); + + applicantId = applicantId || (location && getUrlParam(location, 'applicantId')); + const hashedVote = location && getUrlParam(location, 'hashedVote'); this.state = { applicantId, @@ -45,6 +46,7 @@ class RevealVoteForm extends React.PureComponent { const applicantOpts = accountIdsToOptions(this.props.applicants || []); const myVote = hashedVote ? findVoteByHash(hashedVote) : undefined; + if (myVote) { // Try to substitue applicantId and salt from local sotre: if (!applicantId) applicantId = myVote.applicantId; @@ -81,15 +83,16 @@ class RevealVoteForm extends React.PureComponent { onChange={this.onChangeSalt} />
} - - - +
+ + + +
); } diff --git a/pioneer/packages/joy-election/src/SealedVote.tsx b/pioneer/packages/joy-election/src/SealedVote.tsx index 54650328c1..769a884e4a 100644 --- a/pioneer/packages/joy-election/src/SealedVote.tsx +++ b/pioneer/packages/joy-election/src/SealedVote.tsx @@ -1,38 +1,47 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Table } from 'semantic-ui-react'; +import { Table, Message } from 'semantic-ui-react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Hash } from '@polkadot/types/interfaces'; import { formatBalance } from '@polkadot/util'; import translate from './translate'; -import { calcTotalStake } from '@polkadot/joy-utils/index'; +import { calcTotalStake } from '@polkadot/joy-utils/functions/misc'; import { SealedVote } from '@joystream/types/council'; -import AddressMini from '@polkadot/react-components/AddressMiniJoy'; +import AddressMini from '@polkadot/react-components/AddressMini'; import CandidatePreview from './CandidatePreview'; import { findVoteByHash } from './myVotesStore'; type Props = ApiProps & I18nProps & { hash: Hash; sealedVote?: SealedVote; + isStageRevealing: boolean; + isMyVote: boolean; }; class Comp extends React.PureComponent { renderCandidateOrAction () { - const { hash, sealedVote } = this.props; + const { hash, sealedVote, isStageRevealing, isMyVote } = this.props; + if (!sealedVote) { return Unknown hashed vote: {hash.toHex()}; } if (sealedVote.vote.isSome) { const candidateId = sealedVote.vote.unwrap(); + return ; - } else { + } else if (isStageRevealing && isMyVote) { const revealUrl = `/council/reveals?hashedVote=${hash.toHex()}`; + return Reveal this vote; + } else if (isMyVote) { + return Wait until Revealing stage in order to reveal this vote.; + } else { + return This vote has not been revealed yet.; } } diff --git a/pioneer/packages/joy-election/src/SealedVotes.tsx b/pioneer/packages/joy-election/src/SealedVotes.tsx index 1f03b66ac0..5effd2b117 100644 --- a/pioneer/packages/joy-election/src/SealedVotes.tsx +++ b/pioneer/packages/joy-election/src/SealedVotes.tsx @@ -1,36 +1,43 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Button } from 'semantic-ui-react'; +import { Button, Message } from 'semantic-ui-react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Hash } from '@polkadot/types/interfaces'; import translate from './translate'; import SealedVote from './SealedVote'; -import { queryToProp } from '@polkadot/joy-utils/index'; -import { MyAddressProps } from '@polkadot/joy-utils/MyAccount'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; +import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts'; import { SavedVote } from './myVotesStore'; -import Section from '@polkadot/joy-utils/Section'; +import Section from '@polkadot/joy-utils/react/components/Section'; type Props = ApiProps & I18nProps & MyAddressProps & { myVotes?: SavedVote[]; commitments?: Hash[]; + isStageRevealing: boolean; }; class Comp extends React.PureComponent { private filterVotes = (myVotesOnly: boolean): Hash[] => { const { myVotes = [], commitments = [] } = this.props; + const isMyVote = (hash: string): boolean => { - return myVotes.find(x => x.hash === hash) !== undefined; + return myVotes.find((x) => x.hash === hash) !== undefined; }; - return commitments.filter(x => myVotesOnly === isMyVote(x.toHex())); + + return commitments.filter((x) => myVotesOnly === isMyVote(x.toHex())); } - private renderVotes = (votes: Hash[]) => { + private renderVotes = (votes: Hash[], areVotesMine: boolean) => { return votes.map((hash, index) => - + ); } @@ -39,17 +46,19 @@ class Comp extends React.PureComponent { const otherVotes = this.filterVotes(false); return <> -
{ - !myVotes.length - ? No votes by the current account found on the current browser. - : this.renderVotes(myVotes) - }
+
+ { + !myVotes.length + ? No votes by the current account found on the current browser. + : this.renderVotes(myVotes, true) + } + { this.props.isStageRevealing && } +
- { !otherVotes.length ? No votes submitted by other accounts yet. - : this.renderVotes(otherVotes) + : this.renderVotes(otherVotes, false) }
; diff --git a/pioneer/packages/joy-election/src/SidebarSubtitle.tsx b/pioneer/packages/joy-election/src/SidebarSubtitle.tsx new file mode 100644 index 0000000000..82e63d39bf --- /dev/null +++ b/pioneer/packages/joy-election/src/SidebarSubtitle.tsx @@ -0,0 +1,35 @@ +/** Component providing election stage subtitle for SideBar menu **/ +import React from 'react'; +import { ElectionStage } from '@joystream/types/council'; +import { Option } from '@polkadot/types/codec'; +import { useApi, useCall } from '@polkadot/react-hooks'; +import styled from 'styled-components'; + +const colorByStage = { + Announcing: '#4caf50', + Voting: '#2196f3', + Revealing: '#ff5722' +} as const; + +type StyledSubtitleProps = { + stage?: keyof typeof colorByStage; +} +const StyledSubtitle = styled.div` + display: block; + font-size: 0.85rem; + color: ${(props: StyledSubtitleProps) => props.stage ? colorByStage[props.stage] : 'grey'}; +`; + +export default function SidebarSubtitle () { + const apiProps = useApi(); + const electionStage = useCall>(apiProps.isApiReady && apiProps.api.query.councilElection.stage, []); + + if (electionStage) { + const stageName = electionStage.unwrapOr(undefined)?.type; + const text = stageName ? `${stageName} stage` : 'No active election'; + + return {text}; + } + + return null; +} diff --git a/pioneer/packages/joy-election/src/VoteForm.tsx b/pioneer/packages/joy-election/src/VoteForm.tsx index 63fbb78b17..418c4bd6d0 100644 --- a/pioneer/packages/joy-election/src/VoteForm.tsx +++ b/pioneer/packages/joy-election/src/VoteForm.tsx @@ -4,9 +4,9 @@ import uuid from 'uuid/v4'; import React from 'react'; import { Message, Table } from 'semantic-ui-react'; -import { AppProps, I18nProps } from '@polkadot/react-components/types'; +import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { AccountId, Balance } from '@polkadot/types/interfaces'; import { Button, Input, Labelled } from '@polkadot/react-components/index'; import { SubmittableResult } from '@polkadot/api'; @@ -14,14 +14,16 @@ import { formatBalance } from '@polkadot/util'; import translate from './translate'; import { hashVote } from './utils'; -import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import InputStake from '@polkadot/joy-utils/InputStake'; +import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/functions/misc'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import InputStake from '@polkadot/joy-utils/react/components/InputStake'; import CandidatePreview from './CandidatePreview'; -import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; -import MembersDropdown from '@polkadot/joy-utils/MembersDropdown'; +import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; +import MembersDropdown from '@polkadot/joy-utils/react/components/MembersDropdown'; import { saveVote, NewVote } from './myVotesStore'; import { TxFailedCallback } from '@polkadot/react-components/Status/types'; +import { RouteProps } from 'react-router-dom'; // TODO use a crypto-prooven generator instead of UUID 4. function randomSalt () { @@ -29,11 +31,10 @@ function randomSalt () { } // AppsProps is needed to get a location from the route. -type Props = AppProps & ApiProps & I18nProps & MyAccountProps & { +type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & { applicantId?: string | null; minVotingStake?: Balance; applicants?: AccountId[]; - location?: any; }; type State = { @@ -49,7 +50,8 @@ class Component extends React.PureComponent { super(props); let { applicantId, location } = this.props; - applicantId = applicantId || getUrlParam(location, 'applicantId'); + + applicantId = applicantId || (location && getUrlParam(location, 'applicantId')); this.state = { applicantId, @@ -103,14 +105,11 @@ class Component extends React.PureComponent { - - + - + { proposalsBatch && (<> { proposalsBatch.totalBatches > 1 && ( @@ -81,16 +81,16 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) { /> ) } - { proposalsBatch.proposals.length - ? ( - - {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => ( - - ))} - - ) - : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.` - } + { proposalsBatch.proposals.length + ? ( + + {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => ( + + ))} + + ) + : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.` + } ) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx b/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx index 092848a77b..994faf34bf 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx @@ -6,7 +6,7 @@ import { Item, Icon, Button, Label } from 'semantic-ui-react'; import { ProposalType, Category } from '@polkadot/joy-utils/types/proposals'; import _ from 'lodash'; import styled from 'styled-components'; -import useVoteStyles from './useVoteStyles'; +import getVoteStyles from './getVoteStyles'; import { formatBalance } from '@polkadot/util'; import './ProposalType.css'; @@ -62,9 +62,9 @@ type ProposalTypePreviewProps = { }; const ProposalTypeDetail = (props: { title: string; value: string }) => ( -
-
{ `${props.title}:` }
-
{ props.value }
+
+
{ `${props.title}:` }
+
{ props.value }
); @@ -90,7 +90,7 @@ export default function ProposalTypePreview (props: ProposalTypePreviewProps) { }; return ( - + {/* TODO: We can add it once we have the actual assets @@ -98,51 +98,51 @@ export default function ProposalTypePreview (props: ProposalTypePreviewProps) { {_.startCase(type)} {description} -
+
1 ? 's' : ''}` : 'NONE' } /> 1 ? 's' : ''}` : 'NONE' } />
{ approvalQuorum && ( - - + + Approval Quorum: { approvalQuorum }% ) } { approvalThreshold && ( - - + + Approval Threshold: { approvalThreshold }% ) } { slashingQuorum && ( - - + + Slashing Quorum: { slashingQuorum }% ) } { slashingThreshold && ( - - + + Slashing Threshold: { slashingThreshold }% ) } -
- +
+ Create - +
diff --git a/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx b/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx index 6eec0b42c8..20cea5011d 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Header, Divider, Table, Icon } from 'semantic-ui-react'; -import useVoteStyles from './useVoteStyles'; +import getVoteStyles from './getVoteStyles'; import { VoteKind } from '@joystream/types/proposals'; import { VoteKindStr } from './VotingSection'; -import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview'; +import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; import { ParsedProposal, ProposalVotes } from '@polkadot/joy-utils/types/proposals'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; type VotesProps = { proposal: ParsedProposal; @@ -24,20 +24,21 @@ export default function Votes ({ proposal: { id, votingResults } }: VotesProps) + message='Fetching the votes...'> { (votes && votes.votes.length > 0) ? ( <> -
+
All Votes: ({votes.votes.length}/{votes.councilMembersLength})
- +
{votes.votes.map((proposalVote, idx) => { const { vote, member } = proposalVote; const voteStr = (vote as VoteKind).type.toString() as VoteKindStr; - const { icon, textColor } = useVoteStyles(voteStr); + const { icon, textColor } = getVoteStyles(voteStr); + return ( @@ -59,7 +60,7 @@ export default function Votes ({ proposal: { id, votingResults } }: VotesProps) ) : ( -
No votes have been submitted!
+
No votes have been submitted!
) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx b/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx index 7e10d4b1fb..2908792788 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import { Icon, Button, Message, Divider, Header } from 'semantic-ui-react'; -import useVoteStyles from './useVoteStyles'; -import TxButton from '@polkadot/joy-utils/TxButton'; +import { Icon, Message, Divider, Header } from 'semantic-ui-react'; +import getVoteStyles from './getVoteStyles'; +import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton'; import { MemberId } from '@joystream/types/members'; import { ProposalId, VoteKind, VoteKinds } from '@joystream/types/proposals'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; @@ -32,27 +32,28 @@ type VoteButtonProps = { proposalId: ProposalId; onSuccess: () => void; } + function VoteButton ({ voteKind, proposalId, memberId, onSuccess }: VoteButtonProps) { - const { icon, color } = useVoteStyles(voteKind); + const { icon, color } = getVoteStyles(voteKind); + return ( - // Button.Group "cheat" to force TxButton color - - sendTx() } - txFailedCb={ () => null } - txSuccessCb={ onSuccess } - className={'icon left labeled'}> - - { voteKind } - - + sendTx() } + txFailedCb={ () => null } + txSuccessCb={ onSuccess } + color={color} + style={{ marginRight: '5px' }} + icon + labelPosition={ 'left' }> + + { voteKind } + ); } @@ -82,13 +83,13 @@ export default function VotingSection ({ const voteStr: VoteKindStr | null = voted || (vote && vote.type.toString() as VoteKindStr); if (voteStr) { - const { icon, color } = useVoteStyles(voteStr); + const { icon, color } = getVoteStyles(voteStr); return ( - You voted {`"${voteStr}"`} + You voted {`"${voteStr}"`} ); @@ -98,7 +99,7 @@ export default function VotingSection ({ return ( <> -
Sumbit your vote
+
Sumbit your vote
{ VoteKinds.map((voteKind) => diff --git a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx index 40d3125e22..b1c26fdea5 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Button, Icon } from 'semantic-ui-react'; import { ParsedPost } from '@polkadot/joy-utils/types/proposals'; -import MemberProfilePreview from '@polkadot/joy-utils/MemberProfilePreview'; +import MemberProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview'; import DiscussionPostForm from './DiscussionPostForm'; import { MemberId } from '@joystream/types/members'; import { useTransport } from '@polkadot/joy-utils/react/hooks'; @@ -61,6 +61,7 @@ export default function DiscussionPost ({ authorId.toNumber() === memberId.toNumber() && editsCount < constraints.maxPostEdits ); + const onEditSuccess = () => { setEditing(false); refreshDiscussion(); @@ -98,9 +99,9 @@ export default function DiscussionPost ({ setEditing(true)} primary - size="tiny" + size='tiny' icon> - + ) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx index 5b7ff204fd..b9161990bb 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { Form, Field, withFormik, FormikProps } from 'formik'; import * as Yup from 'yup'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import * as JoyForms from '@polkadot/joy-utils/forms'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import * as JoyForms from '@polkadot/joy-utils/react/components/forms'; import { SubmittableResult } from '@polkadot/api'; import { Button } from 'semantic-ui-react'; import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types'; @@ -76,7 +76,7 @@ const DiscussionPostFormInner = (props: InnerProps) => { }; return ( -
+ { { /> { isEditForm ? ( )} - - + ); }; diff --git a/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx b/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx index 4c228e15dd..98bc9dfaa3 100644 --- a/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx +++ b/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx @@ -1,20 +1,17 @@ import React from 'react'; import { getFormErrorLabelsProps } from './errorHandling'; -import { - GenericProposalForm, +import { GenericProposalForm, GenericFormValues, genericFormDefaultValues, ProposalFormExportProps, ProposalFormContainerProps, - ProposalFormInnerProps -} from './GenericProposalForm'; + ProposalFormInnerProps } from './GenericProposalForm'; import { FormField } from './FormFields'; import { ProposalType } from '@polkadot/joy-utils/types/proposals'; import { WorkingGroupKey, WorkingGroupDef } from '@joystream/types/common'; -import './forms.css'; import { Dropdown, Message } from 'semantic-ui-react'; import { usePromise, useTransport } from '@polkadot/joy-utils/react/hooks'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; import { WorkerData } from '@polkadot/joy-utils/types/workingGroups'; import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo'; @@ -46,7 +43,7 @@ type ExportComponentProps = ProposalFormExportProps; export type FormInnerProps = ProposalFormInnerProps; -export const GenericWorkingGroupProposalForm: React.FunctionComponent = props => { +export const GenericWorkingGroupProposalForm: React.FunctionComponent = (props) => { const { handleChange, errors, @@ -70,20 +67,21 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent(errors, touched); + return ( ({ text: wgKey + ' Working Group', value: wgKey }))} + options={Object.keys(WorkingGroupDef).map((wgKey) => ({ text: wgKey + ' Working Group', value: wgKey }))} value={values.workingGroup} onChange={ handleChange } /> diff --git a/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx b/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx index d8880ba5bc..deb9be8c0d 100644 --- a/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx +++ b/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx @@ -5,6 +5,7 @@ type LabelWithHelpProps = { text: string; help: string }; export default function LabelWithHelp (props: LabelWithHelpProps) { const [open, setOpen] = useState(false); + return (
{[...props.transactionDetails].map((v, k) => ( @@ -919,6 +942,7 @@ export const SubmitApplicationStage = (props: SubmitApplicationStageProps) => {

Please select the account that will be used as the source of funds.

@@ -942,7 +966,7 @@ export type DoneStageProps = { export function DoneStage (props: DoneStageProps) { return ( - +

Application submitted!

Your application is #. @@ -950,8 +974,8 @@ export function DoneStage (props: DoneStageProps) {

You can track the progress of your - application in the My roles and applications section. Note that your application is attached - to your role key (see below). If you have any issues, you can message the group lead in in the Forum or contact them directly. + application in the My roles and applications section. Note that your application is attached + to your role key (see below). If you have any issues, you can message the group lead in in the Forum or contact them directly.

Your new role key

@@ -961,7 +985,7 @@ export function DoneStage (props: DoneStageProps) {

{'We\'ve generated a new role key, '}{props.roleKeyName}, automatically. A copy of the backup file should have been downloaded, or you can - get a backup from the My account section. + get a backup from the My account section.

You can also switch your role key using the Accounts selector in the top right of the screen. It works like @@ -977,7 +1001,7 @@ export function DoneStage (props: DoneStageProps) { This role key has been generated with no password! - We strongly recommend that you set a password for it in the My account section. + We strongly recommend that you set a password for it in the My account section. @@ -1003,8 +1027,8 @@ export type FlowModalProps = Pick & Fu setApplicationStake: (b: Balance) => void; roleStake: Balance; setRoleStake: (b: Balance) => void; - appDetails: any; - setAppDetails: (v: any) => void; + appDetails: ApplicationDetailsData; + setAppDetails: (v: ApplicationDetailsData) => void; txKeyAddress: AccountId; setTxKeyAddress: (v: AccountId) => void; activeStep: ProgressSteps; @@ -1022,7 +1046,7 @@ export const FlowModal = Loadable( 'keypairs', 'slots' ], - props => { + (props) => { const { applicationStake, setApplicationStake, roleStake, setRoleStake, @@ -1033,17 +1057,23 @@ export const FlowModal = Loadable( complete, setComplete } = props; - const accCtx = useMyAccount(); - if (txKeyAddress.isEmpty) { - setTxKeyAddress(new AccountId(accCtx.state.address)); - } + const accContext = useMyAccount(); + + useEffect(() => { + if (txKeyAddress.isEmpty) { + setTxKeyAddress(createMock('AccountId', accContext.state.address)); + } + }, [txKeyAddress]); const history = useHistory(); + const cancel = () => { if (history.length > 1) { history.goBack(); + return; } + history.push('/working-groups/'); }; @@ -1095,63 +1125,59 @@ export const FlowModal = Loadable( setSelectedRoleStake: setRoleStake }; - const stages: { [k in ProgressSteps]: JSX.Element } = { - [ProgressSteps.ConfirmStakes]: (), - - [ProgressSteps.ApplicationDetails]: ( { props.hasConfirmStep ? enterConfirmStakeState() : cancel(); }} - />), - - [ProgressSteps.SubmitApplication]: (), - - [ProgressSteps.Done]: () - }; - const cancelText = complete ? 'Close' : 'Cancel application'; return ( - -

- - - + +
+ + + - + cancel()}> {cancelText} - - + + - {stages[activeStep]} + { activeStep === ProgressSteps.ConfirmStakes && ( + ) } + { activeStep === ProgressSteps.ApplicationDetails && ( { props.hasConfirmStep ? enterConfirmStakeState() : cancel(); }} + /> + ) } + { activeStep === ProgressSteps.SubmitApplication && ( + ) } + { activeStep === ProgressSteps.Done && () } - +
{props.role.headline}
-
{txInProgress && -
-
+
+
} diff --git a/pioneer/packages/joy-roles/src/index.tsx b/pioneer/packages/joy-roles/src/index.tsx index aeabe4a4df..e231d2d5eb 100644 --- a/pioneer/packages/joy-roles/src/index.tsx +++ b/pioneer/packages/joy-roles/src/index.tsx @@ -8,9 +8,7 @@ import { Route, Switch, RouteComponentProps } from 'react-router'; import Tabs from '@polkadot/react-components/Tabs'; import { withMulti } from '@polkadot/react-api/index'; import QueueContext from '@polkadot/react-components/Status/Context'; -import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount'; - -import { ViewComponent } from '@polkadot/joy-utils/index'; +import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; import { Transport } from './transport.substrate'; @@ -25,17 +23,10 @@ import './index.sass'; import translate from './translate'; -const renderViewComponent = (Component: ViewComponent, props?: RouteComponentProps) => { - let params = new Map(); - if (props && props.match.params) { - params = new Map(Object.entries(props.match.params)); - } - - return ; -}; - type Props = AppProps & ApiProps & I18nProps & MyAccountProps +type DefaultRouteProps = RouteComponentProps>; + export const App: React.FC = (props: Props) => { const { t } = props; const tabs: Array = [ @@ -53,20 +44,26 @@ export const App: React.FC = (props: Props) => { const { api } = useContext(ApiContext); const { queueExtrinsic } = useContext(QueueContext); - const transport = new Transport(api, queueExtrinsic); + const [transport] = useState(() => new Transport(api, queueExtrinsic)); - const [wgCtrl] = useState(new WorkingGroupsController(transport)); - const oppCtrl = new OpportunityController(transport, props.myMemberId); - const oppsCtrl = new OpportunitiesController(transport, props.myMemberId); - const [applyCtrl] = useState(new ApplyController(transport)); - const myRolesCtrl = new MyRolesController(transport, props.myAddress); - const [adminCtrl] = useState(new AdminController(transport, api, queueExtrinsic)); + const [wgCtrl] = useState(() => new WorkingGroupsController(transport)); + const [oppCtrl] = useState(() => new OpportunityController(transport)); + const [oppsCtrl] = useState(() => new OpportunitiesController(transport)); + const [applyCtrl] = useState(() => new ApplyController(transport)); + const [myRolesCtrl] = useState(() => new MyRolesController(transport)); + const [adminCtrl] = useState(() => new AdminController(transport, api, queueExtrinsic)); useEffect(() => { return () => { transport.unsubscribe(); }; - }); + }, []); + + useEffect(() => { + oppCtrl.setMemberId(props.myMemberId); + oppsCtrl.setMemberId(props.myMemberId); + myRolesCtrl.setMyAddress(props.myAddress); + }, [props.myMemberId, props.myAddress]); const { basePath } = props; @@ -86,13 +83,26 @@ export const App: React.FC = (props: Props) => { /> - renderViewComponent(ApplyView(applyCtrl), props)} /> - renderViewComponent(OpportunityView(oppCtrl), props)} /> - renderViewComponent(OpportunitiesView(oppsCtrl), props)} /> - renderViewComponent(OpportunitiesView(oppsCtrl))} /> - renderViewComponent(MyRolesView(myRolesCtrl))} /> - renderViewComponent(AdminView(adminCtrl))} /> - renderViewComponent(WorkingGroupsView(wgCtrl))} /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> ); diff --git a/pioneer/packages/joy-roles/src/mocks.ts b/pioneer/packages/joy-roles/src/mocks.ts index 1ec3ac28da..b106c93dc4 100644 --- a/pioneer/packages/joy-roles/src/mocks.ts +++ b/pioneer/packages/joy-roles/src/mocks.ts @@ -1,41 +1,22 @@ -import { bool, Option, Text, u32, u64, Vec } from '@polkadot/types'; -import AccountId from '@polkadot/types/primitive/Generic/AccountId'; +import { IMembership } from '@joystream/types/members'; -import { IMembership, EntryMethod } from '@joystream/types/members'; - -import { - AcceptingApplications, - ActiveOpeningStage, - OpeningStage, - ActiveOpeningStageVariant, - ApplicationId -} from '@joystream/types/hiring'; +import { createMock } from '@joystream/types'; export function mockProfile (name: string, avatar_uri = ''): IMembership { - return { - handle: new Text(name), - avatar_uri: new Text(avatar_uri), - about: new Text(''), - registered_at_block: new u32(0), - registered_at_time: new u64(0), - entry: new EntryMethod(), - suspended: new bool(false), - subscription: new Option(u64), - root_account: new AccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'), - controller_account: new AccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp') - }; + return createMock('Membership', { + handle: name, + avatar_uri: avatar_uri, + root_account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', + controller_account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp' + }); } -export const mockStage = new OpeningStage({ - Active: new ActiveOpeningStageVariant({ - applications_added: new (Vec.with(ApplicationId))([]), - active_application_count: new u32(0), - unstaking_application_count: new u32(0), - deactivated_application_count: new u32(0), - stage: new ActiveOpeningStage({ - AcceptingApplications: new AcceptingApplications({ - started_accepting_applicants_at_block: new u32(100) - }) +export const mockStage = createMock('OpeningStage', { + Active: { + stage: createMock('ActiveOpeningStage', { + AcceptingApplications: { + started_accepting_applicants_at_block: 100 + } }) - }) + } }); diff --git a/pioneer/packages/joy-roles/src/openingStateMarkup.tsx b/pioneer/packages/joy-roles/src/openingStateMarkup.tsx index 9a2fb9912e..3136049179 100644 --- a/pioneer/packages/joy-roles/src/openingStateMarkup.tsx +++ b/pioneer/packages/joy-roles/src/openingStateMarkup.tsx @@ -10,56 +10,52 @@ export type headerMarkup = { iconSpin?: boolean; } -export const stateMarkup = new Map([ - [OpeningState.WaitingToBegin, { +export const stateMarkup: Record = { + [OpeningState.WaitingToBegin]: { class: 'waiting-to-begin', description: 'Waiting to begin', icon: 'spinner', iconSpin: true - }], - [OpeningState.AcceptingApplications, { + }, + [OpeningState.AcceptingApplications]: { class: 'active', description: 'Accepting applications', icon: 'heart' - }], - [OpeningState.InReview, { + }, + [OpeningState.InReview]: { class: 'in-review', description: 'Applications in review', icon: 'hourglass half' - }], - [OpeningState.Complete, { + }, + [OpeningState.Complete]: { class: 'complete', description: 'Hiring complete', icon: 'thumbs up' - }], - [OpeningState.Cancelled, { + }, + [OpeningState.Cancelled]: { class: 'cancelled', description: 'Cancelled', icon: 'ban' - }] -]); - -export function openingStateMarkup (state: OpeningState, key: string): T { - const markup = stateMarkup.get(state); - - if (typeof markup === 'undefined') { - return null as unknown as T; } +}; + +export function openingStateMarkup (state: OpeningState, key: K): headerMarkup[K] { + const markup = stateMarkup[state]; - return (markup as any)[key]; + return markup[key]; } export function openingClass (state: OpeningState): string { - return 'status-' + openingStateMarkup(state, 'class'); + return `status-${openingStateMarkup(state, 'class') || ''}`; } export function openingDescription (state: OpeningState): string { - return openingStateMarkup(state, 'description'); + return openingStateMarkup(state, 'description') || ''; } export function openingIcon (state: OpeningState) { - const icon = openingStateMarkup(state, 'icon'); - const spin = openingStateMarkup(state, 'iconSpin'); + const icon = openingStateMarkup(state, 'icon'); + const spin = openingStateMarkup(state, 'iconSpin'); return ; } diff --git a/pioneer/packages/joy-roles/src/tabs.stories.tsx b/pioneer/packages/joy-roles/src/tabs.stories.tsx index 03c5640628..fa6a546449 100644 --- a/pioneer/packages/joy-roles/src/tabs.stories.tsx +++ b/pioneer/packages/joy-roles/src/tabs.stories.tsx @@ -14,7 +14,7 @@ export default { export const RolesPage = () => { const tab = ( - + diff --git a/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx b/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx index 5a5d40a5cd..b1b73d8a17 100644 --- a/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx +++ b/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx @@ -3,16 +3,15 @@ import { Link } from 'react-router-dom'; import { formatBalance } from '@polkadot/util'; import { ApiPromise } from '@polkadot/api'; -import { GenericAccountId, Option, Text, Vec, u32, u128 } from '@polkadot/types'; +import { Option, Text, Vec } from '@polkadot/types'; import { Balance } from '@polkadot/types/interfaces'; -import { SingleLinkedMapEntry, Controller, View } from '@polkadot/joy-utils/index'; -import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext'; -import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; +import { Controller } from '@polkadot/joy-utils/react/helpers'; +import { View } from '@polkadot/joy-utils/react/hocs'; +import { useMyAccount } from '@polkadot/joy-utils/react/hooks'; import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types'; -import { - Accordion, +import { Accordion, Button, Card, Checkbox, @@ -26,48 +25,37 @@ import { Message, Modal, Table, - TextArea -} from 'semantic-ui-react'; + TextArea, + InputOnChangeData } from 'semantic-ui-react'; import { ITransport } from '../transport'; -import { - Application, +import { Application, ApplicationStage, ActivateOpeningAt, - ApplicationRationingPolicy, - CurrentBlock, Opening, OpeningStage, StakingPolicy, StakingAmountLimitModeKeys, - StakingAmountLimitMode -} from '@joystream/types/hiring'; + StakingAmountLimitMode } from '@joystream/types/hiring'; -import { - Membership, - MemberId -} from '@joystream/types/members'; +import { Membership, + MemberId } from '@joystream/types/members'; import { Stake, StakeId } from '@joystream/types/stake'; -import { - CuratorApplication, CuratorApplicationId, +import { CuratorApplication, CuratorApplicationId, CuratorOpening, - IOpeningPolicyCommitment, CuratorOpeningId -} from '@joystream/types/content-working-group'; + IOpeningPolicyCommitment, CuratorOpeningId } from '@joystream/types/content-working-group'; -import { - classifyOpeningStage, +import { classifyOpeningStage, OpeningStageClassification, - OpeningState -} from '../classifiers'; + OpeningState } from '../classifiers'; -import { - openingDescription -} from '../openingStateMarkup'; +import { openingDescription } from '../openingStateMarkup'; import { Add, Zero } from '../balances'; +import { createMock } from '@joystream/types'; type ids = { curatorId: number; @@ -110,7 +98,7 @@ type State = { } function newHRT (title: string): Text { - return new Text(JSON.stringify({ + return createMock('Text', JSON.stringify({ version: 1, headline: 'some headline', job: { @@ -155,57 +143,49 @@ function newHRT (title: string): Text { } const createRationingPolicyOpt = (maxApplicants: number) => - new Option( - ApplicationRationingPolicy, - new ApplicationRationingPolicy({ - max_active_applicants: new u32(maxApplicants) - }) - ); + createMock('Option', { + max_active_applicants: maxApplicants + }); const createStakingPolicyOpt = (amount: number, amount_mode: StakingAmountLimitMode): Option => - new Option( - StakingPolicy, - new StakingPolicy({ - amount: new u128(amount), - amount_mode, - crowded_out_unstaking_period_length: new Option('BlockNumber', null), - review_period_expired_unstaking_period_length: new Option('BlockNumber', null) - }) - ); + createMock('Option', { + amount, + amount_mode + }); -const STAKING_MODE_EXACT = new StakingAmountLimitMode(StakingAmountLimitModeKeys.Exact); -const STAKING_MODE_AT_LEAST = new StakingAmountLimitMode(StakingAmountLimitModeKeys.AtLeast); +const STAKING_MODE_EXACT = createMock('StakingAmountLimitMode', StakingAmountLimitModeKeys.Exact); +const STAKING_MODE_AT_LEAST = createMock('StakingAmountLimitMode', StakingAmountLimitModeKeys.AtLeast); const stockOpenings: openingDescriptor[] = [ { title: 'Test config A: no application stake, no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999) + max_review_period_length: createMock('u32', 99999) }, text: newHRT('Test configuration A') }, { title: 'Test config B: no application stake, no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999) + max_review_period_length: createMock('u32', 99999) }, text: newHRT('Test configuration B') }, { title: 'Test config C: fixed application stake (100), no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, text: newHRT('Test configuration C') }, { title: 'Test config D: fixed application stake (100), no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, @@ -213,18 +193,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config E: no application stake, fixed role stake (100), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, text: newHRT('Test configuration E') }, { title: 'Test config F: no application stake, fixed role stake (100), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, @@ -232,18 +212,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config G: minimum application stake (100), no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, text: newHRT('Test configuration G') }, { title: 'Test config H: minimum application stake (100), no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, @@ -251,18 +231,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config I: no application stake, minimum role stake (100), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, text: newHRT('Test configuration I') }, { title: 'Test config J: no application stake, minimum role stake (100), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, @@ -270,9 +250,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config K: fixed application stake (100), fixed role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) }, @@ -280,9 +260,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config L: fixed application stake (100), fixed role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) @@ -291,9 +271,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config M: Minimum application stake (100), minimum role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -301,9 +281,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config N: Minimum application stake (100), minimum role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) @@ -312,9 +292,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config O: Fixed application stake (100), minimum role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -322,15 +302,10 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config P: Fixed application stake (100), minimum role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), - application_rationing_policy: new Option( - ApplicationRationingPolicy, - new ApplicationRationingPolicy({ - max_active_applicants: new u32(10) - }) - ), + max_review_period_length: createMock('u32', 99999), + application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -338,9 +313,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config Q: Minimum application stake (100), fixed role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) }, @@ -348,9 +323,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config R: Minimum application stake (100), fixed role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) @@ -376,48 +351,51 @@ export class AdminController extends Controller { this.api = api; this.queueExtrinsic = queueExtrinsic; this.state.currentDescriptor = stockOpenings[0]; - this.updateState(); + void this.refreshState(); } - onTxSuccess = () => { this.updateState(); } + onTxSuccess = () => { this.closeModal(); void this.refreshState(); } newOpening (accountId: string, desc: openingDescriptor) { const tx = this.api.tx.contentWorkingGroup.addCuratorOpening( desc.start, desc.policy, desc.text - ) as unknown as SubmittableExtrinsic; + ); - // FIXME: Normally we would keep it open in case of errror, but due to bad design - // the values in the form are reset at this point anyway, so there is no point - this.closeModal(); this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } startAcceptingApplications (accountId: string, id = 0) { const tx = this.api.tx.contentWorkingGroup.acceptCuratorApplications(id); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } async applyAsACurator (creatorAddress: string, openingId: number) { const membershipIds = (await this.api.query.members.memberIdsByControllerAccountId(creatorAddress)) as Vec; + if (membershipIds.length === 0) { console.error('No membship ID associated with this address'); + return; } + const tx = this.api.tx.contentWorkingGroup.applyOnCuratorOpening( membershipIds[0], openingId, - new GenericAccountId(creatorAddress), - new Option(u128, 400), - new Option(u128, 400), - new Text('This is my application') - ) as unknown as SubmittableExtrinsic; + creatorAddress, + 400, + 400, + 'This is my application' + ); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId: creatorAddress }); } beginApplicantReview (accountId: string, openingId: number) { const tx = this.api.tx.contentWorkingGroup.beginCuratorApplicantReview(openingId); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } @@ -426,26 +404,25 @@ export class AdminController extends Controller { openingId, applications, null - ) as unknown as SubmittableExtrinsic; + ); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } protected async profile (id: MemberId): Promise { const member = (await this.api.query.members.membershipById(id)) as Membership; - if (member.handle.isEmpty) { + + if (member.isEmpty) { throw new Error(`Expected member profile not found! (id: ${id.toString()}`); } + return member; } protected async stakeValue (stakeId: StakeId): Promise { - const stake = new SingleLinkedMapEntry( - Stake, - await this.api.query.stake.stakes( - stakeId - ) - ); - return stake.value.value; + const stake = await this.api.query.stake.stakes(stakeId) as Stake; + + return stake.value; } protected async roleStake (application: Application): Promise { @@ -464,67 +441,51 @@ export class AdminController extends Controller { return this.stakeValue(application.active_application_staking_id.unwrap()); } - async updateState () { + async refreshState () { this.state.openings = new Map(); const nextOpeningId = await this.api.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorOpeningId; + for (let i = nextOpeningId.toNumber() - 1; i >= 0; i--) { - const curatorOpening = new SingleLinkedMapEntry( - CuratorOpening, - await this.api.query.contentWorkingGroup.curatorOpeningById(i) - ); + const curatorOpening = await this.api.query.contentWorkingGroup.curatorOpeningById(i) as CuratorOpening; - const openingId = curatorOpening.value.opening_id; + const openingId = curatorOpening.opening_id; - const baseOpening = new SingleLinkedMapEntry( - Opening, - await this.api.query.hiring.openingById( - openingId - ) - ); + const baseOpening = await this.api.query.hiring.openingById(openingId) as Opening; - const hrt = baseOpening.value.parse_human_readable_text_with_fallback(); + const hrt = baseOpening.parse_human_readable_text_with_fallback(); const title = hrt.job.title; this.state.openings.set(i, { openingId: openingId.toNumber(), curatorId: i, applications: new Array(), - state: baseOpening.value.stage, + state: baseOpening.stage, title: title, - classification: await classifyOpeningStage(this.transport, baseOpening.value) + classification: await classifyOpeningStage(this.transport, baseOpening) }); } const nextAppid = await this.api.query.contentWorkingGroup.nextCuratorApplicationId() as CuratorApplicationId; + for (let i = 0; i < nextAppid.toNumber(); i++) { - const cApplication = new SingleLinkedMapEntry( - CuratorApplication, - await this.api.query.contentWorkingGroup.curatorApplicationById(i) - ); + const cApplication = await this.api.query.contentWorkingGroup.curatorApplicationById(i) as CuratorApplication; - const appId = cApplication.value.application_id; - const baseApplications = new SingleLinkedMapEntry( - Application, - await this.api.query.hiring.applicationById( - appId - ) - ); + const appId = cApplication.application_id; + const baseApplications = await this.api.query.hiring.applicationById(appId) as Application; - const curatorOpening = this.state.openings.get( - cApplication.value.curator_opening_id.toNumber() - ) as opening; + const curatorOpening = this.state.openings.get(cApplication.curator_opening_id.toNumber()) as opening; curatorOpening.applications.push({ openingId: appId.toNumber(), curatorId: i, - stage: baseApplications.value.stage, - account: cApplication.value.role_account_id.toString(), - memberId: cApplication.value.member_id.toNumber(), - profile: (await this.profile(cApplication.value.member_id)), - applicationStake: await this.applicationStake(baseApplications.value), - roleStake: await this.roleStake(baseApplications.value), - application: baseApplications.value + stage: baseApplications.stage, + account: cApplication.role_account_id.toString(), + memberId: cApplication.member_id.toNumber(), + profile: (await this.profile(cApplication.member_id)), + applicationStake: await this.applicationStake(baseApplications), + roleStake: await this.roleStake(baseApplications), + application: baseApplications }); } @@ -547,12 +508,14 @@ type AdminContainerProps = { state: State; controller: AdminController; } + const AdminContainer = ({ state, controller }: AdminContainerProps) => { const address = useMyAccount().state.address; const containerRef = useRef(null); + return (
- + @@ -573,7 +536,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { controller.closeModal()} - mountNode={containerRef.current} // Prevent conflicts with tx-modal (after form values reset issue is fixed, see FIXME: above) + mountNode={containerRef.current} // Prevent conflicts with tx-modal > @@ -584,7 +547,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { { - [...state.openings.keys()].map(key => ) + [...state.openings.keys()].map((key) => ) }
@@ -593,7 +556,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { }; export const AdminView = View( - (state, controller) => { + ({ state, controller }) => { return ( ); @@ -627,25 +590,26 @@ const NewOpening = (props: NewOpeningProps) => { switch (value) { case 'CurrentBlock': setShowExactBlock(false); - setStart(new ActivateOpeningAt(CurrentBlock)); + setStart(createMock('ActivateOpeningAt', 'CurrentBlock')); break; case 'ExactBlock': - setStart(new ActivateOpeningAt({ ExactBlock: exactBlock })); + setStart(createMock('ActivateOpeningAt', { ExactBlock: exactBlock })); setShowExactBlock(true); break; } }; - const onChangeExactBlock = (e: any, { value }: any) => { - setExactBlock(value); - setStart(new ActivateOpeningAt({ ExactBlock: value })); + const onChangeExactBlock = (e: any, { value }: InputOnChangeData) => { + setExactBlock(typeof value === 'number' ? value : (parseInt(value) || 0)); + setStart(createMock('ActivateOpeningAt', { ExactBlock: value })); }; const [policy, setPolicy] = useState(props.desc.policy); const onChangePolicyField = (fieldName: PolicyKey, value: policyDescriptor[PolicyKey]) => { const newState = { ...policy }; + newState[fieldName] = value; setPolicy(newState); }; @@ -682,14 +646,17 @@ const NewOpening = (props: NewOpeningProps) => { ) => { if (mode === '') { const policyField = policy[fieldName]; + mode = policyField && policyField.isSome ? (policyField.unwrap().amount_mode.type as StakingAmountLimitModeKeys) : StakingAmountLimitModeKeys.Exact; // Default } + const value = createStakingPolicyOpt( stakeValue, mode === StakingAmountLimitModeKeys.Exact ? STAKING_MODE_EXACT : STAKING_MODE_AT_LEAST ); + onChangePolicyField(fieldName, value); }; @@ -709,7 +676,7 @@ const NewOpening = (props: NewOpeningProps) => { props.fn({ start: start, policy: policy, - text: new Text(text), + text: createMock('Text', text), title: '' }); }; @@ -726,7 +693,7 @@ const NewOpening = (props: NewOpeningProps) => { /> {showExactBlock === true && @@ -736,15 +703,15 @@ const NewOpening = (props: NewOpeningProps) => { onChangePolicyField('max_review_period_length', new u32(value))} + onChange={(e: any, { value }: any) => onChangePolicyField('max_review_period_length', createMock('u32', value))} /> - onStakeModeCheckboxChange(setRequireAppStakingPolicy, 'application_staking_policy', checked, 0)} /> + onStakeModeCheckboxChange(setRequireAppStakingPolicy, 'application_staking_policy', checked, 0)} /> {requireAppStakingPolicy && ( @@ -757,7 +724,7 @@ const NewOpening = (props: NewOpeningProps) => { changeStakingMode('application_staking_policy', '', value)} /> @@ -767,7 +734,7 @@ const NewOpening = (props: NewOpeningProps) => { - onStakeModeCheckboxChange(setRequireRoleStakingPolicy, 'role_staking_policy', checked, 0)} /> + onStakeModeCheckboxChange(setRequireRoleStakingPolicy, 'role_staking_policy', checked, 0)} /> {requireRoleStakingPolicy && ( @@ -780,7 +747,7 @@ const NewOpening = (props: NewOpeningProps) => { changeStakingMode('role_staking_policy', '', value)} /> @@ -792,7 +759,7 @@ const NewOpening = (props: NewOpeningProps) => {