Display accounts first-and-foremost on all creations (#426)
* Display accounts first-and-foremost on all creations * Fix tests
This commit is contained in:
parent
04d0b2700f
commit
ae5d076de9
@ -14,6 +14,7 @@ Changes:
|
||||
- Derived accounts uses the parent genesisHash by default (attaches to same chain)
|
||||
- Make import from seed, QR & JSON options available on first-start
|
||||
- Adjust popup width, allowing full display of e.g. addresses
|
||||
- Always display network selection on all accounts
|
||||
- Handling signing rejections (any order) transparently
|
||||
- Small overall UI and use adjustments
|
||||
- Latest upstream polkadot-js dependencies
|
||||
|
||||
@ -65,7 +65,7 @@ const WINDOW_OPTS = {
|
||||
top: 150,
|
||||
type: 'popup',
|
||||
url: chrome.extension.getURL('index.html'),
|
||||
width: 480
|
||||
width: 560
|
||||
};
|
||||
|
||||
function getId (): string {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { RequestAuthorizeTab } from '@polkadot/extension-base/background/types';
|
||||
import { ThemeProps } from '../../types';
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../components';
|
||||
@ -21,14 +21,20 @@ interface Props {
|
||||
|
||||
function Request ({ authId, className, isFirst, request: { origin }, url }: Props): React.ReactElement<Props> {
|
||||
const onAction = useContext(ActionContext);
|
||||
const _onApprove = (): Promise<void> =>
|
||||
approveAuthRequest(authId)
|
||||
|
||||
const _onApprove = useCallback(
|
||||
() => approveAuthRequest(authId)
|
||||
.then(() => onAction())
|
||||
.catch((error: Error) => console.error(error));
|
||||
const _onReject = (): Promise<void> =>
|
||||
rejectAuthRequest(authId)
|
||||
.catch((error: Error) => console.error(error)),
|
||||
[authId, onAction]
|
||||
);
|
||||
|
||||
const _onReject = useCallback(
|
||||
() => rejectAuthRequest(authId)
|
||||
.then(() => onAction())
|
||||
.catch((error: Error) => console.error(error));
|
||||
.catch((error: Error) => console.error(error)),
|
||||
[authId, onAction]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
|
||||
import { BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
|
||||
import { Name, Password } from '../../partials';
|
||||
|
||||
interface Props {
|
||||
@ -14,7 +14,7 @@ interface Props {
|
||||
onCreate: (name: string, password: string) => void | Promise<void | boolean>;
|
||||
}
|
||||
|
||||
function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.ReactElement<Props> {
|
||||
function AccountName ({ isBusy, onBackClick, onCreate }: Props): React.ReactElement<Props> {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
|
||||
@ -29,13 +29,7 @@ function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
{name && <Password onChange={setPassword} />}
|
||||
{name && password && (
|
||||
<Address
|
||||
address={address}
|
||||
name={name}
|
||||
/>
|
||||
)}
|
||||
<Password onChange={setPassword} />
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
<BackButton onClick={onBackClick} />
|
||||
@ -52,4 +46,4 @@ function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountName;
|
||||
export default React.memo(AccountName);
|
||||
|
||||
@ -42,7 +42,7 @@ describe('Create Account', () => {
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input'), name);
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
|
||||
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
|
||||
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
|
||||
|
||||
@ -89,20 +89,20 @@ describe('Create Account', () => {
|
||||
|
||||
it('only account name input is visible at first', () => {
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('input should not be highlighted as error until first interaction', () => {
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(false);
|
||||
});
|
||||
|
||||
it('after typing less than 3 characters into name input, password input is not visible', async () => {
|
||||
await enterName('ab');
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('ErrorMessage').text()).toBe('Account name is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('ErrorMessage').first().text()).toBe('Account name is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
@ -110,12 +110,12 @@ describe('Create Account', () => {
|
||||
it('input should keep showing error when something has been typed but then erased', async () => {
|
||||
await enterName('ab');
|
||||
await enterName('');
|
||||
expect(wrapper.find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, first password input is visible', async () => {
|
||||
await enterName('abc');
|
||||
expect(wrapper.find(Input).first().prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).first().prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
|
||||
@ -33,12 +33,11 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Warning>Please write down your wallet’s mnemonic seed and keep it in a safe place. <br />
|
||||
Mnemonic seed is used to restore your wallet. Keep it carefully in case you lose your assets.</Warning>
|
||||
<MnemonicSeed
|
||||
onCopy={_onCopy}
|
||||
seed={seed}
|
||||
/>
|
||||
<Warning>Please write down your wallet’s mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets.</Warning>
|
||||
<VerticalSpace />
|
||||
<Checkbox
|
||||
checked={isMnemonicSaved}
|
||||
@ -57,4 +56,4 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
export default Mnemonic;
|
||||
export default React.memo(Mnemonic);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { ActionContext, Loading } from '../../components';
|
||||
import { ActionContext, Address, Loading } from '../../components';
|
||||
import { createAccountSuri, createSeed } from '../../messaging';
|
||||
import { HeaderWithSteps } from '../../partials';
|
||||
import AccountName from './AccountName';
|
||||
@ -27,6 +27,7 @@ export default function CreateAccount (): React.ReactElement {
|
||||
// this should always be the case
|
||||
if (name && password && account) {
|
||||
setIsBusy(true);
|
||||
|
||||
createAccountSuri(name, password, account.seed)
|
||||
.then(() => onAction('/'))
|
||||
.catch((error: Error): void => {
|
||||
@ -47,23 +48,31 @@ export default function CreateAccount (): React.ReactElement {
|
||||
step={step}
|
||||
text='Create an account: '
|
||||
/>
|
||||
<Loading>{account && (
|
||||
step === 1
|
||||
? (
|
||||
<Mnemonic
|
||||
onNextStep={_onNextStep}
|
||||
seed={account.seed}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AccountName
|
||||
address={account.address}
|
||||
isBusy={isBusy}
|
||||
onBackClick={_onPreviousStep}
|
||||
onCreate={_onCreate}
|
||||
/>
|
||||
)
|
||||
)}</Loading>
|
||||
<Loading>
|
||||
<div>
|
||||
<Address
|
||||
address={account?.address}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
{account && (
|
||||
step === 1
|
||||
? (
|
||||
<Mnemonic
|
||||
onNextStep={_onNextStep}
|
||||
seed={account.seed}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AccountName
|
||||
address={account.address}
|
||||
isBusy={isBusy}
|
||||
onBackClick={_onPreviousStep}
|
||||
onCreate={_onCreate}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Loading>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,13 +12,14 @@ import { Address, Svg } from '../../components';
|
||||
import useOutsideClick from '../../hooks/useOutsideClick';
|
||||
|
||||
interface Props {
|
||||
allAddresses: string[];
|
||||
allAddresses: [string, string | null][];
|
||||
className?: string;
|
||||
onSelect: (address: string) => void;
|
||||
selectedAddress: string;
|
||||
selectedGenesis: string | null;
|
||||
}
|
||||
|
||||
function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }: Props): React.ReactElement<Props> {
|
||||
function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress, selectedGenesis }: Props): React.ReactElement<Props> {
|
||||
const [isDropdownVisible, setDropdownVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -34,16 +35,22 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }
|
||||
onClick={_toggleDropdown}
|
||||
ref={ref}
|
||||
>
|
||||
<Address address={selectedAddress} />
|
||||
<Address
|
||||
address={selectedAddress}
|
||||
genesisHash={selectedGenesis}
|
||||
/>
|
||||
</div>
|
||||
<HiddenOptions visible={isDropdownVisible}>
|
||||
{allAddresses.map((address) => (
|
||||
{allAddresses.map(([address, genesisHash]) => (
|
||||
<div
|
||||
data-parent-option
|
||||
key={address}
|
||||
onClick={_selectParent(address)}
|
||||
>
|
||||
<Address address={address} />
|
||||
<Address
|
||||
address={address}
|
||||
genesisHash={genesisHash}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</HiddenOptions>
|
||||
@ -54,7 +61,7 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }
|
||||
const HiddenOptions = styled.div<{ visible: boolean }>`
|
||||
position: absolute;
|
||||
visibility: ${({ visible }): string => (visible ? 'visible' : 'hidden')};
|
||||
width: 430px;
|
||||
width: 510px;
|
||||
z-index: 100;
|
||||
background: ${({ theme }: ThemeProps): string => theme.bodyColor};
|
||||
max-height: ${({ visible }): string => (visible ? '200px' : '0')};
|
||||
@ -77,7 +84,7 @@ export default styled(AddressDropdown)(({ theme }: ThemeProps) => `
|
||||
& > div:first-child > ${Address}::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
top: 66%;
|
||||
transform: translateY(-50%);
|
||||
right: 11px;
|
||||
width: 30px;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { assert } from '@polkadot/util';
|
||||
|
||||
@ -15,10 +15,11 @@ import DerivationPath from './DerivationPath';
|
||||
interface Props {
|
||||
isLocked?: boolean;
|
||||
parentAddress: string;
|
||||
parentGenesis: string | null;
|
||||
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
|
||||
}
|
||||
|
||||
export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }: Props): React.ReactElement<Props> {
|
||||
export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const { accounts, hierarchy } = useContext(AccountContext);
|
||||
@ -30,6 +31,13 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }
|
||||
|
||||
const passwordInputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const allAddresses = useMemo(
|
||||
() => hierarchy
|
||||
.filter(({ isExternal }) => !isExternal)
|
||||
.map(({ address, genesisHash }): [string, string | null] => [address, genesisHash || null]),
|
||||
[hierarchy]
|
||||
);
|
||||
|
||||
const _goCreate = useCallback(
|
||||
() => onAction('/account/create'),
|
||||
[onAction]
|
||||
@ -91,13 +99,19 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }
|
||||
)}
|
||||
<DisableableArea isDisabled={!shouldAccountBeDerived}>
|
||||
{isLocked
|
||||
? <Address address={parentAddress} />
|
||||
? (
|
||||
<Address
|
||||
address={parentAddress}
|
||||
genesisHash={parentGenesis}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Label label='Choose Parent Account:'>
|
||||
<AddressDropdown
|
||||
allAddresses={hierarchy.filter(({ isExternal }) => !isExternal).map(({ address }) => address)}
|
||||
allAddresses={allAddresses}
|
||||
onSelect={_onParentChange}
|
||||
selectedAddress={parentAddress}
|
||||
selectedGenesis={parentGenesis}
|
||||
/>
|
||||
</Label>
|
||||
)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
|
||||
@ -37,22 +37,24 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
const [parentPassword, setParentPassword] = useState<string | null>(null);
|
||||
|
||||
const parentGenesis = useMemo(
|
||||
() => accounts.find((a) => a.address === parentAddress)?.genesisHash || null,
|
||||
[accounts, parentAddress]
|
||||
);
|
||||
|
||||
const _onCreate = useCallback(() => {
|
||||
if (!account || !name || !password || !parentPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentAccount = accounts.find((a) => a.address === parentAddress);
|
||||
const genesisHash = parentAccount?.genesisHash || null;
|
||||
|
||||
setIsBusy(true);
|
||||
deriveAccount(parentAddress, account.suri, parentPassword, name, password, genesisHash)
|
||||
deriveAccount(parentAddress, account.suri, parentPassword, name, password, parentGenesis)
|
||||
.then(() => onAction('/'))
|
||||
.catch((error): void => {
|
||||
setIsBusy(false);
|
||||
console.error(error);
|
||||
});
|
||||
}, [account, accounts, name, password, onAction, parentAddress, parentPassword]);
|
||||
}, [account, name, password, onAction, parentAddress, parentGenesis, parentPassword]);
|
||||
|
||||
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => {
|
||||
setAccount(account);
|
||||
@ -74,23 +76,23 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
isLocked={isLocked}
|
||||
onDerivationConfirmed={_onDerivationConfirmed}
|
||||
parentAddress={parentAddress}
|
||||
/>
|
||||
)}
|
||||
{account && (
|
||||
<Name
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
)}
|
||||
{account && name && <Password onChange={setPassword}/>}
|
||||
{account && name && password && (
|
||||
<Address
|
||||
address={account.address}
|
||||
name={name}
|
||||
parentGenesis={parentGenesis}
|
||||
/>
|
||||
)}
|
||||
{account && (
|
||||
<>
|
||||
<div>
|
||||
<Address
|
||||
address={account.address}
|
||||
genesisHash={parentGenesis}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<Name
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
<Password onChange={setPassword} />
|
||||
<VerticalSpace/>
|
||||
<ButtonArea>
|
||||
<BackButton onClick={_onBackClick}/>
|
||||
@ -108,4 +110,4 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
export default Derive;
|
||||
export default React.memo(Derive);
|
||||
|
||||
@ -36,14 +36,16 @@ export default function ImportQr (): React.ReactElement {
|
||||
)}
|
||||
{account && (
|
||||
<>
|
||||
<div>
|
||||
<Address
|
||||
{...account}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<Name
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
<Address
|
||||
{...account}
|
||||
name={name}
|
||||
/>
|
||||
<VerticalSpace />
|
||||
{name && (
|
||||
<ButtonArea>
|
||||
|
||||
@ -40,10 +40,14 @@ export default function Import (): React.ReactElement {
|
||||
const _onChangeSeed = useCallback(
|
||||
async (suri: string | null): Promise<void> => {
|
||||
if (suri) {
|
||||
setAccount(await validateSeed(suri));
|
||||
} else {
|
||||
setAccount(null);
|
||||
try {
|
||||
return setAccount(await validateSeed(suri));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setAccount(null);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@ -69,6 +73,12 @@ export default function Import (): React.ReactElement {
|
||||
showBackArrow
|
||||
text='Import account'
|
||||
/>
|
||||
<div>
|
||||
<Address
|
||||
address={account?.address}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<ValidatedInput
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
component={SeedInput}
|
||||
@ -78,14 +88,14 @@ export default function Import (): React.ReactElement {
|
||||
rowsCount={2}
|
||||
validator={isSeedValid}
|
||||
/>
|
||||
{account && <Name onChange={setName} />}
|
||||
{account && name && <Password onChange={setPassword} />}
|
||||
{account && (
|
||||
<>
|
||||
<Name onChange={setName} />
|
||||
<Password onChange={setPassword} />
|
||||
</>
|
||||
)}
|
||||
{account && name && password && (
|
||||
<>
|
||||
<Address
|
||||
address={account.address}
|
||||
name={name}
|
||||
/>
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
<NextStepButton
|
||||
|
||||
@ -96,10 +96,13 @@ export default function Upload (): React.ReactElement {
|
||||
text='Restore from JSON'
|
||||
/>
|
||||
<div>
|
||||
<Address
|
||||
address={(isFileValid && address) || null}
|
||||
name={(isFileValid && json?.meta.name as string) || null}
|
||||
/>
|
||||
<div>
|
||||
<Address
|
||||
address={(isFileValid && address) || null}
|
||||
genesisHash={(isFileValid && json?.meta.genesisHash as string) || null}
|
||||
name={(isFileValid && json?.meta.name as string) || null}
|
||||
/>
|
||||
</div>
|
||||
<InputFileWithLabel
|
||||
accept={acceptedFormats}
|
||||
isError={!isFileValid}
|
||||
|
||||
@ -48,6 +48,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
|
||||
|
||||
useEffect((): void => {
|
||||
setIsLocked(null);
|
||||
|
||||
!isExternal && isSignLocked(signId)
|
||||
.then((isLocked) => setIsLocked(isLocked))
|
||||
.catch((error: Error) => console.error(error));
|
||||
|
||||
@ -40,7 +40,7 @@ function MnemonicSeed ({ className, onCopy, seed }: Props): React.ReactElement<P
|
||||
}
|
||||
|
||||
export default styled(MnemonicSeed)(({ theme }: ThemeProps) => `
|
||||
margin-top: 21px;
|
||||
margin-bottom: 21px;
|
||||
|
||||
.buttonsRow {
|
||||
display: flex;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<title>polkadot{.js}</title>
|
||||
</head>
|
||||
<body style="height: 600px; margin:0; overflow-x:hidden; text-align:center">
|
||||
<div id="root" style="box-sizing:border-box; margin:0 auto; padding:0; text-align:left; width:570px; height: calc(100% - 2px); max-width: 100%;"></div>
|
||||
<div id="root" style="box-sizing:border-box; margin:0 auto; padding:0; text-align:left; width:560px; height: calc(100% - 2px); max-width: 100%;"></div>
|
||||
<script src='./extension.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user