mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 15:04:50 +01:00
FIX: Don't render sidebar if not on large device (#1940)
This commit is contained in:
parent
8d4d6372ed
commit
eec6a01f1a
10 changed files with 53 additions and 197 deletions
|
@ -69,10 +69,7 @@ export class BlueButton extends Component {
|
|||
backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor;
|
||||
fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor;
|
||||
}
|
||||
let buttonWidth = this.props.width ? this.props.width : width / 1.5;
|
||||
if ('noMinWidth' in this.props) {
|
||||
buttonWidth = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
|
@ -84,7 +81,6 @@ export class BlueButton extends Component {
|
|||
height: 45,
|
||||
maxHeight: 45,
|
||||
borderRadius: 25,
|
||||
minWidth: buttonWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
|
@ -107,10 +103,6 @@ export const BlueButtonHook = props => {
|
|||
backgroundColor = colors.buttonDisabledBackgroundColor;
|
||||
fontColor = colors.buttonDisabledTextColor;
|
||||
}
|
||||
let buttonWidth = props.width ? props.width : width / 1.5;
|
||||
if ('noMinWidth' in props) {
|
||||
buttonWidth = 0;
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
|
@ -122,7 +114,6 @@ export const BlueButtonHook = props => {
|
|||
height: 45,
|
||||
maxHeight: 45,
|
||||
borderRadius: 25,
|
||||
minWidth: buttonWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
|
@ -144,10 +135,10 @@ export class SecondButton extends Component {
|
|||
backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor;
|
||||
fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor;
|
||||
}
|
||||
let buttonWidth = this.props.width ? this.props.width : width / 1.5;
|
||||
if ('noMinWidth' in this.props) {
|
||||
buttonWidth = 0;
|
||||
}
|
||||
// let buttonWidth = this.props.width ? this.props.width : width / 1.5;
|
||||
// if ('noMinWidth' in this.props) {
|
||||
// buttonWidth = 0;
|
||||
// }
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
|
@ -159,7 +150,6 @@ export class SecondButton extends Component {
|
|||
height: 45,
|
||||
maxHeight: 45,
|
||||
borderRadius: 25,
|
||||
minWidth: buttonWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
|
@ -2089,7 +2079,8 @@ export class WalletsCarousel extends Component {
|
|||
};
|
||||
|
||||
snapToItem = item => {
|
||||
this.walletsCarousel.current.snapToItem(item);
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this.walletsCarousel?.current?.snapToItem(item);
|
||||
};
|
||||
|
||||
onLayout = () => {
|
||||
|
|
|
@ -264,11 +264,12 @@ function DrawerRoot() {
|
|||
const dimensions = useWindowDimensions();
|
||||
const isLargeScreen = Platform.OS === 'android' ? isTablet() : dimensions.width >= Dimensions.get('screen').width / 3 && isTablet();
|
||||
const drawerStyle = { width: '0%' };
|
||||
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
drawerStyle={isLargeScreen ? null : drawerStyle}
|
||||
drawerType={isLargeScreen ? 'permanent' : null}
|
||||
drawerContent={props => <DrawerList {...props} />}
|
||||
drawerContent={props => (isLargeScreen ? <DrawerList {...props} /> : null)}
|
||||
>
|
||||
<Drawer.Screen name="Navigation" component={Navigation} options={{ headerShown: false, gestureEnabled: false }} />
|
||||
</Drawer.Navigator>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
32B5A32A2334450100F8D608 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5A3292334450100F8D608 /* Bridge.swift */; };
|
||||
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
|
||||
6DF25A9F249DB97E001D06F5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */; };
|
||||
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */ = {isa = PBXBuildFile; productRef = 6DFC806F24EA0B6C007B8700 /* EFQRCode */; };
|
||||
6DFC807024EA0B6C007B8700 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */; };
|
||||
6DFC807224EA2FA9007B8700 /* ViewQRCodefaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFC807124EA2FA9007B8700 /* ViewQRCodefaceController.swift */; };
|
||||
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B468CC34D5B41F3950078EF /* libsqlite3.0.tbd */; };
|
||||
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
|
||||
|
@ -339,7 +339,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */,
|
||||
6DFC807024EA0B6C007B8700 /* BuildFile in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -694,7 +694,7 @@
|
|||
);
|
||||
name = "BlueWalletWatch Extension";
|
||||
packageProductDependencies = (
|
||||
6DFC806F24EA0B6C007B8700 /* EFQRCode */,
|
||||
6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */,
|
||||
);
|
||||
productName = "BlueWalletWatch Extension";
|
||||
productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */;
|
||||
|
@ -793,7 +793,7 @@
|
|||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
packageReferences = (
|
||||
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
|
||||
6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */,
|
||||
);
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1929,7 +1929,7 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */ = {
|
||||
6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/EFPrefix/EFQRCode.git";
|
||||
requirement = {
|
||||
|
@ -1940,9 +1940,9 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
6DFC806F24EA0B6C007B8700 /* EFQRCode */ = {
|
||||
6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */;
|
||||
package = 6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */;
|
||||
productName = EFQRCode;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
|
|
@ -125,9 +125,8 @@ const ReceiveDetails = () => {
|
|||
backgroundColor: BlueCurrentTheme.colors.elevated,
|
||||
},
|
||||
share: {
|
||||
alignItems: 'center',
|
||||
alignContent: 'flex-end',
|
||||
marginBottom: 24,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
modalButton: {
|
||||
backgroundColor: BlueCurrentTheme.colors.modalButton,
|
||||
|
|
|
@ -92,7 +92,6 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 4,
|
||||
},
|
||||
createButton: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginTop: 32,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { StatusBar, View, TouchableOpacity, InteractionManager, StyleSheet, Alert, useWindowDimensions } from 'react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { StatusBar, View, TouchableOpacity, StyleSheet, Alert, useWindowDimensions } from 'react-native';
|
||||
import { DrawerContentScrollView } from '@react-navigation/drawer';
|
||||
import { WalletsCarousel, BlueNavigationStyle, BlueHeaderDefaultMainHooks } from '../../BlueComponents';
|
||||
import { Icon } from 'react-native-elements';
|
||||
|
@ -14,11 +14,11 @@ import { useTheme, useRoute } from '@react-navigation/native';
|
|||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
const EV = require('../../blue_modules/events');
|
||||
const BlueApp: AppStorage = require('../../BlueApp');
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||
|
||||
const DrawerList = props => {
|
||||
console.log('drawerList rendering...');
|
||||
const walletsCarousel = useRef();
|
||||
const [wallets, setWallets] = useState(BlueApp.getWallets().concat(false));
|
||||
const wallets = useRoute().params?.wallets || BlueApp.getWallets() || [];
|
||||
const height = useWindowDimensions().height;
|
||||
const { colors } = useTheme();
|
||||
const { selectedWallet } = useRoute().params || '';
|
||||
|
@ -27,83 +27,6 @@ const DrawerList = props => {
|
|||
backgroundColor: colors.brandingColor,
|
||||
},
|
||||
});
|
||||
let lastSnappedTo = 0;
|
||||
|
||||
const refreshTransactions = () => {
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
let noErr = true;
|
||||
try {
|
||||
// await BlueElectrum.ping();
|
||||
await BlueElectrum.waitTillConnected();
|
||||
const balanceStart = +new Date();
|
||||
await BlueApp.fetchWalletBalances(lastSnappedTo || 0);
|
||||
const balanceEnd = +new Date();
|
||||
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
const start = +new Date();
|
||||
await BlueApp.fetchWalletTransactions(lastSnappedTo || 0);
|
||||
const end = +new Date();
|
||||
console.log('fetch tx took', (end - start) / 1000, 'sec');
|
||||
} catch (err) {
|
||||
noErr = false;
|
||||
console.warn(err);
|
||||
}
|
||||
if (noErr) await BlueApp.saveToDisk(); // caching
|
||||
|
||||
redrawScreen();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
|
||||
console.log('drawerList wallets changed');
|
||||
}, [wallets]);
|
||||
|
||||
const redrawScreen = (scrollToEnd = false) => {
|
||||
console.log('drawerList redrawScreen()');
|
||||
|
||||
const newWallets = BlueApp.getWallets().concat(false);
|
||||
if (scrollToEnd) {
|
||||
scrollToEnd = newWallets.length > wallets.length;
|
||||
}
|
||||
|
||||
setWallets(newWallets);
|
||||
if (scrollToEnd) {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
walletsCarousel.current?.snapToItem(wallets.length - 2);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet.
|
||||
// placing event subscription here so it gets exclusively re-subscribed more often. otherwise we would
|
||||
// have to unsubscribe on unmount and resubscribe again on mount.
|
||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, refreshTransactions, true);
|
||||
|
||||
EV(EV.enum.WALLETS_COUNT_CHANGED, () => redrawScreen(true));
|
||||
|
||||
console.log('drawerList useEffect');
|
||||
// the idea is that upon wallet launch we will refresh
|
||||
// all balances and all transactions here:
|
||||
redrawScreen();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
const balanceStart = +new Date();
|
||||
await BlueApp.fetchWalletBalances();
|
||||
const balanceEnd = +new Date();
|
||||
console.log('fetch all wallet balances took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
const start = +new Date();
|
||||
await BlueApp.fetchWalletTransactions();
|
||||
const end = +new Date();
|
||||
console.log('fetch all wallet txs took', (end - start) / 1000, 'sec');
|
||||
redrawScreen();
|
||||
await BlueApp.saveToDisk();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleClick = index => {
|
||||
console.log('click', index);
|
||||
|
@ -156,82 +79,13 @@ const DrawerList = props => {
|
|||
}
|
||||
};
|
||||
|
||||
const onSnapToItem = index => {
|
||||
console.log('onSnapToItem', index);
|
||||
lastSnappedTo = index;
|
||||
if (index < BlueApp.getWallets().length) {
|
||||
// not the last
|
||||
}
|
||||
|
||||
if (wallets[index].type === PlaceholderWallet.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// now, lets try to fetch balance and txs for this wallet in case it has changed
|
||||
lazyRefreshWallet(index);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decides whether wallet with such index shoud be refreshed,
|
||||
* refreshes if yes and redraws the screen
|
||||
* @param index {Integer} Index of the wallet.
|
||||
* @return {Promise.<void>}
|
||||
*/
|
||||
const lazyRefreshWallet = async index => {
|
||||
/** @type {Array.<AbstractWallet>} wallets */
|
||||
const wallets = BlueApp.getWallets();
|
||||
if (!wallets[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldBalance = wallets[index].getBalance();
|
||||
let noErr = true;
|
||||
let didRefresh = false;
|
||||
|
||||
try {
|
||||
if (wallets[index] && wallets[index].type !== PlaceholderWallet.type && wallets[index].timeToRefreshBalance()) {
|
||||
console.log('snapped to, and now its time to refresh wallet #', index);
|
||||
await wallets[index].fetchBalance();
|
||||
if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) {
|
||||
console.log('balance changed, thus txs too');
|
||||
// balance changed, thus txs too
|
||||
await wallets[index].fetchTransactions();
|
||||
redrawScreen();
|
||||
didRefresh = true;
|
||||
} else if (wallets[index].timeToRefreshTransaction()) {
|
||||
console.log(wallets[index].getLabel(), 'thinks its time to refresh TXs');
|
||||
await wallets[index].fetchTransactions();
|
||||
if (wallets[index].fetchPendingTransactions) {
|
||||
await wallets[index].fetchPendingTransactions();
|
||||
}
|
||||
if (wallets[index].fetchUserInvoices) {
|
||||
await wallets[index].fetchUserInvoices();
|
||||
await wallets[index].fetchBalance(); // chances are, paid ln invoice was processed during `fetchUserInvoices()` call and altered user's balance, so its worth fetching balance again
|
||||
}
|
||||
redrawScreen();
|
||||
didRefresh = true;
|
||||
} else {
|
||||
console.log('balance not changed');
|
||||
}
|
||||
}
|
||||
} catch (Err) {
|
||||
noErr = false;
|
||||
console.warn(Err);
|
||||
}
|
||||
|
||||
if (noErr && didRefresh) {
|
||||
await BlueApp.saveToDisk(); // caching
|
||||
}
|
||||
};
|
||||
|
||||
const renderWalletsCarousel = () => {
|
||||
return (
|
||||
<WalletsCarousel
|
||||
removeClippedSubviews={false}
|
||||
data={wallets}
|
||||
data={wallets.concat(false)}
|
||||
onPress={handleClick}
|
||||
handleLongPress={handleLongPress}
|
||||
onSnapToItem={onSnapToItem}
|
||||
ref={walletsCarousel}
|
||||
testID="WalletsList"
|
||||
vertical
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global alert */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Platform, Dimensions, View, Keyboard, StatusBar, StyleSheet } from 'react-native';
|
||||
import { Platform, View, Keyboard, StatusBar, StyleSheet } from 'react-native';
|
||||
import {
|
||||
BlueFormMultiInput,
|
||||
BlueButtonLink,
|
||||
|
@ -25,7 +25,6 @@ import RNFS from 'react-native-fs';
|
|||
import DocumentPicker from 'react-native-document-picker';
|
||||
import ScanQRCode from '../send/ScanQRCode';
|
||||
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
||||
const { width } = Dimensions.get('window');
|
||||
const isDesktop = getSystemName() === 'Mac OS X';
|
||||
|
||||
const WalletsImport = () => {
|
||||
|
@ -44,7 +43,7 @@ const WalletsImport = () => {
|
|||
},
|
||||
center: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 16,
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
});
|
||||
|
@ -228,9 +227,6 @@ const WalletsImport = () => {
|
|||
testID="DoImport"
|
||||
disabled={importText.trim().length === 0}
|
||||
title={loc.wallets.import_do_import}
|
||||
buttonStyle={{
|
||||
width: width / 1.5,
|
||||
}}
|
||||
onPress={importButtonPressed}
|
||||
/>
|
||||
<BlueSpacing20 />
|
||||
|
|
|
@ -66,9 +66,28 @@ export default class WalletsList extends Component {
|
|||
// all balances and all transactions here:
|
||||
this.redrawScreen();
|
||||
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
const balanceStart = +new Date();
|
||||
await BlueApp.fetchWalletBalances();
|
||||
const balanceEnd = +new Date();
|
||||
console.log('fetch all wallet balances took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
const start = +new Date();
|
||||
await BlueApp.fetchWalletTransactions();
|
||||
const end = +new Date();
|
||||
console.log('fetch all wallet txs took', (end - start) / 1000, 'sec');
|
||||
await BlueApp.saveToDisk();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.setState(prev => ({ timeElapsed: prev.timeElapsed + 1 }));
|
||||
}, 60000);
|
||||
this.redrawScreen();
|
||||
|
||||
this._unsubscribe = this.props.navigation.addListener('focus', this.onNavigationEventFocus);
|
||||
}
|
||||
|
||||
|
@ -115,6 +134,11 @@ export default class WalletsList extends Component {
|
|||
redrawScreen = (scrollToEnd = false) => {
|
||||
console.log('wallets/list redrawScreen()');
|
||||
|
||||
// here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet.
|
||||
// placing event subscription here so it gets exclusively re-subscribed more often. otherwise we would
|
||||
// have to unsubscribe on unmount and resubscribe again on mount.
|
||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, this.refreshTransactions, true);
|
||||
|
||||
if (BlueApp.getBalance() !== 0) {
|
||||
A(A.ENUM.GOT_NONZERO_BALANCE);
|
||||
} else {
|
||||
|
@ -135,9 +159,10 @@ export default class WalletsList extends Component {
|
|||
wallets,
|
||||
},
|
||||
() => {
|
||||
this.props.navigation.navigate('DrawerRoot', { wallets: BlueApp.getWallets() });
|
||||
if (scrollToEnd) {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this.walletsCarousel.current?.snapToItem(this.state.wallets.length - 2);
|
||||
this.walletsCarousel?.current?.snapToItem(this.state.wallets.length - 2);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -60,12 +60,6 @@ const PleaseBackup = () => {
|
|||
flexWrap: 'wrap',
|
||||
marginTop: 14,
|
||||
},
|
||||
ok: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
});
|
||||
|
||||
const handleBackButton = useCallback(() => {
|
||||
|
@ -111,12 +105,8 @@ const PleaseBackup = () => {
|
|||
|
||||
<View style={styles.secret}>{renderSecret()}</View>
|
||||
|
||||
<View style={styles.ok}>
|
||||
<View style={styles.flex}>
|
||||
<BlueSpacing20 />
|
||||
<BlueButton testID="PleasebackupOk" onPress={handleBackButton} title={loc.pleasebackup.ok} />
|
||||
</View>
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
<BlueButton testID="PleasebackupOk" onPress={handleBackButton} title={loc.pleasebackup.ok} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeBlueArea>
|
||||
|
|
|
@ -655,6 +655,7 @@ const WalletTransactions = () => {
|
|||
InteractionManager.runAfterInteractions(async () => {
|
||||
setItemPriceUnit(wallet.getPreferredBalanceUnit());
|
||||
BlueApp.saveToDisk();
|
||||
navigate('DrawerRoot', { wallets: BlueApp.getWallets() });
|
||||
})
|
||||
}
|
||||
onManageFundsPressed={() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue