-
Trevor Farlow authored
Introduces a **ResponsiveProvider** as the sole location that tracks window size and makes that info available to the entire app. This can be used for media queries and size-based component switching. --------- Co-authored-by:
Jed Fox <git@jedfox.com>
Trevor Farlow authoredIntroduces a **ResponsiveProvider** as the sole location that tracks window size and makes that info available to the entire app. This can be used for media queries and size-based component switching. --------- Co-authored-by:
Jed Fox <git@jedfox.com>
BudgetList.js 7.84 KiB
import React, { useState, useRef } from 'react';
import { connect } from 'react-redux';
import * as actions from 'loot-core/src/client/actions';
import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
import Loading from '../../icons/AnimatedLoading';
import CloudCheck from '../../icons/v1/CloudCheck';
import CloudDownload from '../../icons/v1/CloudDownload';
import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple';
import FileDouble from '../../icons/v1/FileDouble';
import CloudUnknown from '../../icons/v2/CloudUnknown';
import Key from '../../icons/v2/Key';
import RefreshArrow from '../../icons/v2/RefreshArrow';
import { styles, colors } from '../../style';
import tokens from '../../tokens';
import { View, Text, Button, Tooltip, Menu } from '../common';
function getFileDescription(file) {
if (file.state === 'unknown') {
return (
'This is a cloud-based file but its state is unknown because you ' +
'are offline.'
);
}
if (file.encryptKeyId) {
if (file.hasKey) {
return 'This file is encrypted and you have key to access it.';
}
return 'This file is encrypted and you do not have the key for it.';
}
return null;
}
function FileMenu({ onDelete, onClose }) {
function onMenuSelect(type) {
onClose();
switch (type) {
case 'delete':
onDelete();
break;
default:
}
}
let items = [{ name: 'delete', text: 'Delete' }];
return <Menu onMenuSelect={onMenuSelect} items={items} />;
}
function DetailButton({ state, onDelete }) {
let [menuOpen, setMenuOpen] = useState(false);
return (
<View>
<Button
bare
onClick={e => {
e.stopPropagation();
setMenuOpen(true);
}}
>
<DotsHorizontalTriple style={{ width: 16, height: 16 }} />
</Button>
{menuOpen && (
<Tooltip
position="bottom-right"
style={{ padding: 0 }}
onClose={() => setMenuOpen(false)}
>
<FileMenu
state={state}
onDelete={onDelete}
onClose={() => setMenuOpen(false)}
/>
</Tooltip>
)}
</View>
);
}
function FileState({ file }) {
let Icon;
let status;
let color;
switch (file.state) {
case 'unknown':
Icon = CloudUnknown;
status = 'Network unavailable';
color = colors.n7;
break;
case 'remote':
Icon = CloudDownload;
status = 'Available for download';
break;
case 'local':
case 'broken':
Icon = FileDouble;
status = 'Local';
break;
default:
Icon = CloudCheck;
status = 'Syncing';
break;
}
return (
<View
style={{
color,
alignItems: 'center',
flexDirection: 'row',
marginTop: 8,
}}
>
<Icon
color={color}
style={{
width: 18,
height: 18,
color: 'currentColor',
}}
/>
<Text style={{ marginLeft: 5 }}>{status}</Text>
</View>
);
}
function File({ file, onSelect, onDelete }) {
let selecting = useRef(false);
async function _onSelect(file) {
// Never allow selecting the file while uploading/downloading, and
// make sure to never allow duplicate clicks
if (!selecting.current) {
selecting.current = true;
await onSelect(file);
selecting.current = false;
}
}
return (
<View
onClick={() => _onSelect(file)}
title={getFileDescription(file)}
style={[
{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
...styles.shadow,
margin: 10,
padding: '12px 15px',
backgroundColor: 'white',
borderRadius: 6,
flexShrink: 0,
cursor: 'pointer',
':hover': {
backgroundColor: colors.hover,
},
},
]}
>
<View style={{ alignItems: 'flex-start' }}>
<Text style={[{ fontSize: 16, fontWeight: 700 }]}>{file.name}</Text>
<FileState file={file} />
</View>
<View
style={{ flex: '0 0 auto', flexDirection: 'row', alignItems: 'center' }}
>
{file.encryptKeyId && (
<Key
style={{
width: 13,
height: 13,
marginRight: 8,
color: file.hasKey ? colors.b5 : colors.n8,
}}
/>
)}
<DetailButton state={file.state} onDelete={() => onDelete(file)} />
</View>
</View>
);
}
function BudgetTable({ files, onSelect, onDelete }) {
return (
<View
style={{
flexGrow: 1,
[`@media (min-width: ${tokens.breakpoint_small})`]: {
flexGrow: 0,
maxHeight: 310,
},
overflow: 'auto',
'& *': { userSelect: 'none' },
}}
>
{files.map(file => (
<File
key={file.id || file.cloudFileId}
file={file}
onSelect={onSelect}
onDelete={onDelete}
/>
))}
</View>
);
}
function RefreshButton({ onRefresh }) {
let [loading, setLoading] = useState(false);
async function _onRefresh() {
setLoading(true);
await onRefresh();
setLoading(false);
}
let Icon = loading ? Loading : RefreshArrow;
return (
<Button bare style={{ padding: 10, marginRight: 5 }} onClick={_onRefresh}>
<Icon
color={colors.n1}
style={{ width: 18, height: 18, color: colors.n1 }}
/>
</Button>
);
}
function BudgetList({
files = [],
getUserData,
loadAllFiles,
pushModal,
loadBudget,
createBudget,
downloadBudget,
}) {
const [creating, setCreating] = useState(false);
const onCreate = ({ testMode } = {}) => {
if (!creating) {
setCreating(true);
createBudget({ testMode });
}
};
return (
<View
style={{
flex: 1,
justifyContent: 'center',
marginInline: -20,
marginTop: 20,
width: '100vw',
[`@media (min-width: ${tokens.breakpoint_small})`]: {
maxWidth: tokens.breakpoint_small,
width: '100%',
},
}}
>
<View>
<Text style={[styles.veryLargeText, { margin: 20 }]}>Files</Text>
<View
style={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
justifyContent: 'center',
marginRight: 5,
}}
>
<RefreshButton
onRefresh={() => {
getUserData();
loadAllFiles();
}}
/>
</View>
</View>
<BudgetTable
files={files}
actions={actions}
onSelect={file => {
if (file.state === 'remote') {
downloadBudget(file.cloudFileId);
} else {
loadBudget(file.id);
}
}}
onDelete={file => pushModal('delete-budget', { file })}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
padding: 25,
paddingLeft: 5,
}}
>
<Button
bare
style={{
marginLeft: 10,
color: colors.n4,
}}
onClick={e => {
e.preventDefault();
pushModal('import');
}}
>
Import file
</Button>
<Button primary onClick={onCreate} style={{ marginLeft: 15 }}>
Create new file
</Button>
{isNonProductionEnvironment() && (
<Button
primary
onClick={() => onCreate({ testMode: true })}
style={{ marginLeft: 15 }}
>
Create test file
</Button>
)}
</View>
</View>
);
}
export default connect(
state => ({
files: state.budgets.allFiles,
isLoggedIn: !!state.user.data,
}),
actions,
)(BudgetList);