@mui/icons-material#Delete TypeScript Examples
The following examples show how to use
@mui/icons-material#Delete.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: FileList.tsx From frontend with MIT License | 6 votes |
function FileList({ files, onDelete }: Props) {
return (
<List dense>
{files.map((file, key) => (
<ListItem
key={key}
secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
<Delete />
</IconButton>
}>
<ListItemAvatar>
<Avatar>
<UploadFile />
</Avatar>
</ListItemAvatar>
<ListItemText primary={file.type} />
<ListItemText primary={file.name} />
</ListItem>
))}
</List>
)
}
Example #2
Source File: FileList.tsx From frontend with MIT License | 5 votes |
function FileList({ files, onDelete, onSetFileRole, filesRole = [] }: Props) {
const setFileRole = (file: File) => {
return (event: SelectChangeEvent<CampaignFileRole>) => {
if (Object.values(CampaignFileRole).includes(event.target.value as CampaignFileRole)) {
onSetFileRole(file, event.target.value as CampaignFileRole)
}
}
}
return (
<List dense>
{files.map((file, key) => (
<ListItem
key={key}
secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
<Delete />
</IconButton>
}>
<ListItemAvatar>
<Avatar>
<UploadFile />
</Avatar>
</ListItemAvatar>
<ListItemText primary={file.type} />
<ListItemText primary={file.name} />
<FormControl>
<InputLabel id="choose-type-label">{'Избери роля'}</InputLabel>
<Select<CampaignFileRole>
id="choose-type"
label="Избери роля"
labelId="choose-type-label"
value={
filesRole.find((f) => f.file === file.name)?.role ?? CampaignFileRole.background
}
onChange={setFileRole(file)}>
{Object.values(CampaignFileRole).map((role) => (
<MenuItem key={role} value={role}>
{role}
</MenuItem>
))}
</Select>
</FormControl>
</ListItem>
))}
</List>
)
}
Example #3
Source File: SessionList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function SessionList() {
// TODO: fix editing functions
const [editedSessions, setEditedSessions] = useState({});
const [sessionToDelete, setSessionToDelete] = useState({
show: false,
toDelete: null,
});
const user = useRecoilValue(userData);
const sessions = user.sessions;
useTitle("Abrechnung - Sessions");
const editSession = (id) => {
if (!editedSessions.hasOwnProperty(id)) {
const newSessions = {
...editedSessions,
[id]: sessions.find((session) => session.id === id)?.name,
};
setEditedSessions(newSessions);
}
};
const stopEditSession = (id) => {
if (editedSessions.hasOwnProperty(id)) {
let newEditedSessions = { ...editedSessions };
delete newEditedSessions[id];
setEditedSessions(newEditedSessions);
}
};
const closeDeleteSessionModal = () => {
setSessionToDelete({ show: false, toDelete: null });
};
const performRename = (id) => {
if (editedSessions.hasOwnProperty(id)) {
renameSession({
sessionID: id,
name: editedSessions[id],
}).catch((err) => {
toast.error(err);
});
stopEditSession(id);
}
};
const openDeleteSessionModal = (id) => {
setSessionToDelete({ show: true, toDelete: id });
};
const confirmDeleteSession = () => {
if (sessionToDelete.toDelete !== null) {
deleteSession({ sessionID: sessionToDelete.toDelete }).catch((err) => {
toast.error(err);
});
setSessionToDelete({ show: false, toDelete: null });
}
};
const handleEditChange = (id, value) => {
const newEditedSessions = { ...editedSessions, [id]: value };
setEditedSessions(newEditedSessions);
};
const onKeyUp = (id) => (key) => {
if (key.keyCode === 13) {
performRename(id);
}
};
return (
<MobilePaper>
<Typography component="h3" variant="h5">
Login Sessions
</Typography>
<List>
{sessions.map((session) => {
if (editedSessions.hasOwnProperty(session.id)) {
return (
<ListItem key={session.id}>
<TextField
margin="normal"
variant="standard"
fullWidth
onKeyUp={onKeyUp(session.id)}
value={editedSessions[session.id]}
onChange={(event) => handleEditChange(session.id, event.target.value)}
/>
<ListItemSecondaryAction>
<Button onClick={() => performRename(session.id)}>
<Check />
</Button>
<Button onClick={() => stopEditSession(session.id)}>
<Close />
</Button>
</ListItemSecondaryAction>
</ListItem>
);
} else {
return (
<ListItem key={session.id}>
<ListItemText
primary={session.name}
secondary={
<>
<span>
Valid until{" "}
{DateTime.fromISO(session.valid_until).toLocaleString(
DateTime.DATETIME_FULL
) && "indefinitely"}
,{" "}
</span>
<span>
Last seen on{" "}
{DateTime.fromISO(session.last_seen).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</>
}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => editSession(session.id)}>
<Edit />
</IconButton>
<IconButton onClick={() => openDeleteSessionModal(session.id)}>
<Delete />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
}
})}
</List>
<Dialog open={sessionToDelete.show} onClose={closeDeleteSessionModal}>
<DialogTitle>Delete Session?</DialogTitle>
<DialogContent>
<DialogContentText>
{sessionToDelete.toDelete !== null
? `Are you sure you want to delete session ${
sessions.find((session) => session.id === sessionToDelete.toDelete)?.name
}`
: null}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button color="secondary" onClick={confirmDeleteSession}>
Yes pls
</Button>
<Button color="primary" onClick={closeDeleteSessionModal}>
No
</Button>
</DialogActions>
</Dialog>
</MobilePaper>
);
}
Example #4
Source File: index.tsx From yearn-watch-legacy with GNU Affero General Public License v3.0 | 4 votes |
SearchInput = (props: SearchInputProps) => {
const {
onFilter,
debounceWait,
totalItems,
foundItems,
totalSubItems,
foundSubItems,
} = props;
const [searchText, setSearchText] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [filterVaultsWithWarnings, setFilterVaultsWithWarnings] =
useState(false);
const [healthCheckFilter, setHealthCheckFilter] = useState('');
const debounceFilter = useCallback(
debounce((newSearchText, flags) => {
const newSearchTextLowerCase = newSearchText.toLowerCase();
onFilter(newSearchTextLowerCase, flags, healthCheckFilter);
setIsSearching(false);
}, debounceWait),
[debounceWait, isSearching]
);
// Event listener called on every change
const onChange = useCallback(
(event: ChangeEvent) => {
const value = (event.target as HTMLInputElement).value;
setIsSearching(true);
setSearchText(value);
debounceFilter(value, getCurrentFlags(filterVaultsWithWarnings));
},
[filterVaultsWithWarnings, searchText, isSearching, healthCheckFilter]
);
const onFilterVaultsWithWarnings = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setFilterVaultsWithWarnings(e.target.checked);
setIsSearching(true);
const newSearchTextLowerCase = searchText.toLowerCase();
onFilter(
newSearchTextLowerCase,
getCurrentFlags(e.target.checked),
healthCheckFilter
);
setIsSearching(false);
},
[searchText, isSearching, healthCheckFilter]
);
const handleClickClearSearch = useCallback(() => {
setSearchText('');
setFilterVaultsWithWarnings(false);
onFilter('', getCurrentFlags(false), '');
}, [onFilter]);
const healthCheckFilterChange = useCallback(
(e: SelectChangeEvent<unknown>) => {
setHealthCheckFilter((e.target as HTMLInputElement).value);
setIsSearching(true);
const newSearchTextLowerCase = searchText.toLowerCase();
onFilter(
newSearchTextLowerCase,
getCurrentFlags(filterVaultsWithWarnings),
(e.target as HTMLInputElement).value
);
setIsSearching(false);
},
[searchText, healthCheckFilter, isSearching]
);
const renderSearchingLabel = useCallback(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let render: any;
if (isSearching) {
render = (
<div>
<ProgressSpinnerBar label="results" />
</div>
);
} else {
render = (
<>
{healthCheckFilter !== '' && (
<WarningLabel
warningText={'HealthCheck Filter is ON!'}
/>
)}
<ResultsLabel
title="Vaults"
totalItems={totalItems}
foundItems={foundItems}
displayFound={true}
isSearching={isSearching}
/>
<ResultsLabel
title="Strategies"
totalItems={totalSubItems}
foundItems={foundSubItems}
displayFound={true}
isSearching={isSearching}
/>
</>
);
}
return render;
}, [isSearching, totalItems, foundItems, totalSubItems, foundSubItems]);
return (
<div>
<StyledForm>
<Grid container direction="row" alignItems="center" spacing={3}>
<Grid item xs={12} sm={6}>
<StyledContainer maxWidth="lg">
<StyledTextField
variant="outlined"
onChange={onChange}
type="search"
value={searchText}
placeholder="Search by vault/strategy address/name, strategist address, token name/symbol, share token symbol/name or API version."
InputProps={
searchText == ''
? {
startAdornment: (
<InputAdornment position="end">
<Search />
</InputAdornment>
),
}
: {
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="delete"
onClick={
handleClickClearSearch
}
size="large"
>
<Delete />
</IconButton>
</InputAdornment>
),
}
}
/>
</StyledContainer>
</Grid>
<Grid item xs={12} sm={3}>
<StyledContainer maxWidth="lg">
<StyledFormControlLabel
control={
<Switch
checked={filterVaultsWithWarnings}
onChange={onFilterVaultsWithWarnings}
color="primary"
/>
}
labelPlacement="start"
label="Vaults with warnings"
/>
</StyledContainer>
</Grid>
<Grid item xs={12} sm={3}>
<StyledContainer maxWidth="lg">
<StyledFormControlLabel
control={
<StyledSelect
displayEmpty
variant="standard"
defaultValue=""
value={healthCheckFilter}
onChange={healthCheckFilterChange}
>
<MenuItem value="">All</MenuItem>
<MenuItem value="Enabled">
Enabled
</MenuItem>
<MenuItem value="Disabled">
Disabled
</MenuItem>
<MenuItem value="None">
Not Set
</MenuItem>
</StyledSelect>
}
labelPlacement="start"
label="HealthCheck"
/>
</StyledContainer>
</Grid>
</Grid>
</StyledForm>
<StyledContainerResult maxWidth="lg">
{renderSearchingLabel()}
</StyledContainerResult>
</div>
);
}
Example #5
Source File: index.tsx From yearn-watch-legacy with GNU Affero General Public License v3.0 | 4 votes |
ProtocolsList = (props: ProtocolsListProps) => {
const classes = useStyles();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let render: any = '';
if (props.items.length === 0) {
render = (
<Container maxWidth="lg" className={classes.resultText}>
No terms added yet. Please add your terms above in the search
bar and click the search icon.
</Container>
);
}
return (
<>
{render}
{props.items.map((protocol: ProtocolTVL, index: number) => (
<Container maxWidth="lg" key={index}>
<div className={classes.root}>
<MuiAccordion className={classes.accordion}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Grid
container
className={classes.root}
spacing={2}
>
<Grid item md={11} xs={11}>
<Grid
container
spacing={1}
direction="row"
justifyContent="center"
alignItems="center"
>
{`#${
index + 1
} - ${protocol.name.toUpperCase()} - ${amountToString(
protocol.tvl
)} - # ${
protocol.strategies.length
} Strategies - TVL Impact: ${getTvlImpact(
amountToMMs(protocol.tvl)
)}`}
</Grid>
</Grid>
<Grid item md={1} xs={1}>
<Button
onClick={(event) =>
props.onRemove(
event,
protocol.name
)
}
>
<Delete
color="secondary"
fontSize="small"
className={classes.deleteIcon}
/>
</Button>
</Grid>
</Grid>
</AccordionSummary>
<Divider className={classes.divider} />
<AccordionDetails>
<Container>
<StrategyProtocolList item={protocol} />
</Container>
</AccordionDetails>
</MuiAccordion>
</div>
</Container>
))}
</>
);
}
Example #6
Source File: Workspaces.tsx From sapio-studio with Mozilla Public License 2.0 | 4 votes |
export function Workspaces(props: { idx: number; value: number }) {
const dispatch = useDispatch();
const [workspaces, set_workspaces] = React.useState<string[]>([]);
const [to_delete, set_to_delete] = React.useState<string | null>(null);
const [trigger_now, set_trigger_now] = React.useState(0);
const [show_new_workspace, set_new_workspace] = React.useState(false);
const hide_new_workspace = () => {
set_new_workspace(false);
};
const reload = () => {
set_trigger_now(trigger_now + 1);
};
React.useEffect(() => {
let cancel = false;
const update = async () => {
if (cancel) return;
try {
const list = await window.electron.sapio.workspaces.list();
set_workspaces(list);
} catch (err) {
console.error(err);
set_workspaces([]);
}
setTimeout(update, 5000);
};
update();
return () => {
cancel = true;
};
}, [trigger_now]);
const contract_rows = workspaces.map((id) => {
return {
id,
name: id,
};
});
const delete_workspace = (fname: string | number) => {
if (typeof fname === 'number') return;
set_to_delete(fname);
};
const columns: GridColumns = [
{
field: 'actions-load',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="open-folder"
icon={<FolderOpen />}
label="Open"
onClick={() => {
// TODO: Better tabbing?
dispatch(switch_wallet_tab(3));
typeof params.id === 'string' &&
dispatch(switch_workspace(params.id));
}}
/>,
],
},
{
field: 'name',
headerName: 'Name',
width: 100,
type: 'text',
flex: 1,
},
{
field: 'actions-delete',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="delete"
icon={<Delete />}
label="Delete"
onClick={() => delete_workspace(params.id)}
/>,
],
},
];
function CustomToolbar() {
return (
<GridToolbarContainer>
<Button onClick={() => set_new_workspace(true)}>
New Workspace<Add></Add>
</Button>
</GridToolbarContainer>
);
}
return (
<div hidden={props.idx !== props.value} className="WorkspaceList">
<NewWorkspace
show={show_new_workspace}
hide={hide_new_workspace}
reload={reload}
/>
<DeleteDialog
set_to_delete={() => set_to_delete(null)}
to_delete={to_delete !== null ? ['workspace', to_delete] : null}
reload={reload}
/>
{props.idx === props.value && (
<div className="WorkspaceListInner">
<div></div>
<div>
<DataGrid
components={{
Toolbar: CustomToolbar,
}}
rows={contract_rows}
columns={columns}
disableExtendRowFullWidth={false}
columnBuffer={3}
pageSize={10}
rowsPerPageOptions={[5]}
disableColumnSelector
disableSelectionOnClick
/>
</div>
<div></div>
</div>
)}
</div>
);
}
Example #7
Source File: ContractList.tsx From sapio-studio with Mozilla Public License 2.0 | 4 votes |
export function ContractList(props: { idx: number; value: number }) {
const dispatch = useDispatch();
const [contracts, set_contracts] = React.useState<string[]>([]);
const [to_delete, set_to_delete] = React.useState<string | null>(null);
const [trigger_now, set_trigger_now] = React.useState(0);
const workspace = useSelector(selectWorkspace);
React.useEffect(() => {
let cancel = false;
const update = async () => {
if (cancel) return;
try {
const list =
await window.electron.sapio.compiled_contracts.list(
workspace
);
set_contracts(list);
} catch (err) {
console.error(err);
set_contracts([]);
}
setTimeout(update, 5000);
};
update();
return () => {
cancel = true;
};
}, [trigger_now, workspace]);
const contract_rows = contracts.map((id) => {
const [mod, args, time] = id.split('-');
return {
id,
mod,
args,
time: new Date(parseInt(time!)),
};
});
const delete_contract = (fname: string | number) => {
if (typeof fname === 'number') return;
set_to_delete(fname);
};
const columns: GridColumns = [
{
field: 'actions-load',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="open-folder"
icon={<VisibilityIcon />}
label="Open"
onClick={() => {
dispatch(switch_showing('ContractViewer'));
dispatch(
open_contract_directory(
typeof params.id === 'number' ? '' : params.id
)
);
}}
/>,
],
},
{
field: 'time',
headerName: 'Time',
minWidth: 100,
type: 'dateTime',
flex: 1,
},
{
field: 'args',
headerName: 'Args Hash',
width: 100,
flex: 1,
},
{
field: 'mod',
headerName: 'Module',
width: 100,
type: 'text',
flex: 1,
},
{
field: 'actions-delete',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="delete"
icon={<Delete />}
label="Delete"
onClick={() => delete_contract(params.id)}
/>,
],
},
];
return (
<div hidden={props.idx !== props.value} className="ContractList">
<Typography variant="h4">Workspace: {workspace}</Typography>
<DeleteDialog
set_to_delete={() => set_to_delete(null)}
to_delete={to_delete === null ? null : ['contract', to_delete]}
reload={() => set_trigger_now(trigger_now + 1)}
/>
{props.idx === props.value && (
<div className="ContractListInner">
<div></div>
<div>
<DataGrid
rows={contract_rows}
columns={columns}
disableExtendRowFullWidth={false}
columnBuffer={3}
pageSize={10}
rowsPerPageOptions={[5]}
disableColumnSelector
disableSelectionOnClick
/>
</div>
<div></div>
</div>
)}
</div>
);
}
Example #8
Source File: Scheduler.tsx From NekoMaid with MIT License | 4 votes |
Scheduler: React.FC = () => {
const plugin = usePlugin()
const [id, setId] = useState(-1)
let [tasks, setTasks] = useState<Task[]>([])
const [name, setName] = useState('')
const [cron, setCron] = useState('')
const [values, setValues] = useState('')
const [whenIdle, setWhenIdle] = useState(false)
const [cronError, setCronError] = useState('')
const save = () => plugin.emit('scheduler:update', (res: boolean) => {
action(res)
plugin.emit('scheduler:fetch', setTasks)
}, JSON.stringify(tasks))
useEffect(() => { plugin.emit('scheduler:fetch', setTasks) }, [])
return <Box sx={{ minHeight: '100%', py: 3 }}>
<Toolbar />
<Container maxWidth={false}>
<Grid container spacing={3}>
<Grid item lg={4} md={12} xl={4} xs={12}>
<Card>
<CardHeader
title={lang.scheduler.title}
sx={{ position: 'relative' }}
action={<IconButton
size='small'
onClick={() => {
const task = {
name: lang.scheduler.newTask,
cron: '*/1 * * * *',
enabled: true,
whenIdle: false,
values: ['/say Hello, %server_tps% (PlaceholderAPI)', 'This is a chat message']
}
setTasks([...tasks, task])
setId(tasks.length)
setCronError('')
setCron(task.cron)
setName(task.name)
setValues(task.values.join('\n'))
setWhenIdle(false)
}}
sx={cardActionStyles}
><Add /></IconButton>}
/>
<Divider />
{tasks.length
? <List
sx={{ width: '100%' }}
component='nav'
>
{tasks.map((it, i) => <ListItem
key={i}
disablePadding
secondaryAction={<IconButton
edge='end'
onClick={() => dialog(lang.scheduler.confirmDelete)
.then(it => {
if (it == null) return
setTasks((tasks = tasks.filter((_, id) => i !== id)))
save()
})}
><Delete /></IconButton>}
sx={{ position: 'relative' }}
>
<ListItemIcon sx={{ paddingLeft: 2, position: 'absolute' }}>
<Checkbox
edge='start'
checked={it.enabled}
tabIndex={-1}
/>
</ListItemIcon>
<ListItemButton onClick={() => {
setId(i)
setCronError('')
setCron(tasks[i].cron)
setName(tasks[i].name)
setValues(tasks[i].values.join('\n'))
setWhenIdle(!!tasks[i].whenIdle)
}}><ListItemText inset primary={it.name} /></ListItemButton >
</ListItem>)}
</List>
: <CardContent><Empty /></CardContent>}
</Card>
</Grid>
<Grid item lg={8} md={12} xl={8} xs={12}>
<Card>
<CardHeader
title={lang.scheduler.editor}
sx={{ position: 'relative' }}
action={<IconButton
size='small'
onClick={() => {
tasks[id].values = values.split('\n')
tasks[id].cron = cron
tasks[id].name = name
tasks[id].whenIdle = whenIdle
save()
}}
sx={cardActionStyles}
disabled={!tasks[id] || !!cronError}
><Save /></IconButton>}
/>
<Divider />
<CardContent>
{tasks[id]
? <>
<TextField
required
fullWidth
variant='standard'
label={lang.scheduler.name}
value={name}
onChange={e => setName(e.target.value)}
/>
<TextField
fullWidth
multiline
rows={4}
value={values}
sx={{ marginTop: 3 }}
label={lang.scheduler.content}
onChange={e => setValues(e.target.value)}
/>
<FormControlLabel
control={<Switch checked={whenIdle} />}
label={lang.scheduler.whenIdle}
onChange={(e: any) => setWhenIdle(e.target.checked)}
/>
</>
: <Empty title={lang.scheduler.notSelected} />}
</CardContent>
{tasks[id] && <>
<Divider textAlign='left'>{lang.scheduler.timer}</Divider>
<CardContent>
<Box sx={{
'& .MuiTextField-root': { backgroundColor: 'inherit!important' },
'& .MuiOutlinedInput-input': { color: 'inherit!important' },
'& .MuiTypography-h6': { color: theme => theme.palette.primary.main + '!important' }
}}>
<Cron cron={cron} setCron={setCron} setCronError={setCronError} locale={currentLanguage as any} isAdmin />
</Box>
</CardContent>
</>}
</Card>
</Grid>
</Grid>
</Container>
</Box>
}
Example #9
Source File: Config.tsx From NekoMaid with MIT License | 4 votes |
configs.push({
title: lang.config.serverConfig,
component () {
const plugin = usePlugin()
const globalData = useGlobalData()
const [flag, update] = useState(0)
const [info, setInfo] = useState<Record<string, string>>({ })
const [open, setOpen] = useState(false)
const [canGetData, setCanGetData] = useState(true)
const [loading, setLoading] = useState(false)
const setValue = (field: string, value: any, isGlobal = true) => {
plugin.emit('server:set', field, value)
success()
if (isGlobal) {
(globalData as any)[field] = value
update(flag + 1)
location.reload()
}
}
const createEditButtom = (field: string, isGlobal?: boolean, isInt = true) => <IconButton
onClick={() => dialog(
{
content: lang.inputValue,
input: isInt
? {
error: true,
type: 'number',
helperText: lang.invalidValue,
validator: (it: string) => /^\d+$/.test(it) && +it >= 0
}
: { }
}).then(res => res != null && setValue(field, isInt ? parseInt(res as any) : (res || null), isGlobal))}
><Edit /></IconButton>
const infoElm: JSX.Element[] = []
for (const key in info) {
const name = (lang.config as any)[key]
infoElm.push(<ListItem key={key} sx={{ pl: 4 }}>
<ListItemText
primary={key === 'isAikarFlags' ? <Link href='https://mcflags.emc.gs' target='_blank' rel='noopener'>{name}</Link> : name}
secondary={info[key].toString()}
/>
</ListItem>)
}
return <List>
<CircularLoading loading={loading} />
<ListItem secondaryAction={globalData.canSetMaxPlayers
? createEditButtom('maxPlayers')
: undefined}>
<ListItemText primary={lang.config.maxPlayers + ': ' + globalData.maxPlayers} />
</ListItem>
<ListItem secondaryAction={createEditButtom('spawnRadius')}>
<ListItemText primary={lang.config.spawnRadius + ': ' + globalData.spawnRadius} />
</ListItem>
<ListItem secondaryAction={createEditButtom('motd', false, false)}>
<ListItemText primary={lang.config.motd} />
</ListItem>
<ListItem secondaryAction={<Switch checked={globalData.hasWhitelist} onChange={e => setValue('hasWhitelist', e.target.checked)} />}>
<ListItemText primary={lang.config.whitelist} />
</ListItem>
{canGetData && <>
<ListItemButton onClick={() => {
if (infoElm.length) setOpen(!open)
else {
setLoading(true)
plugin.emit('server:fetchInfo', (data: any) => {
setLoading(false)
if (!data) {
failed(lang.unsupported)
setCanGetData(false)
return
}
setInfo(data)
setOpen(true)
})
}
}}>
<ListItemIcon><Equalizer /></ListItemIcon>
<ListItemText primary={lang.info} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout='auto' unmountOnExit>
<List component='div' dense disablePadding>{infoElm}</List>
</Collapse>
</>}
</List>
}
},
{
title: lang.history,
component () {
const [cur, update] = useState(0)
const list: ServerRecord[] = JSON.parse(localStorage.getItem('NekoMaid:servers') || '[]')
return <List>
{list.sort((a, b) => b.time - a.time).map(it => {
const i = it.address.indexOf('?')
return <ListItem
disablePadding
key={it.address}
secondaryAction={<IconButton edge='end' size='small' onClick={() => {
localStorage.setItem('NekoMaid:servers', JSON.stringify(list.filter(s => s.address !== it.address)))
success()
update(cur + 1)
}}><Delete /></IconButton>}
>
<ListItemButton onClick={() => {
location.hash = ''
location.search = it.address
}} dense>
<ListItemAvatar><Avatar src={it.icon} variant='rounded'><HelpOutline /></Avatar></ListItemAvatar>
<ListItemText primary={<Tooltip title={it.address.slice(i + 1)}>
<span>{it.address.slice(0, i)}</span></Tooltip>} secondary={dayjs(it.time).fromNow()} />
</ListItemButton>
</ListItem>
})}
</List>
}
},
{
title: lang.config.theme,
component () {
const color = localStorage.getItem('NekoMaid:color') || 'blue'
return <CardContent sx={{ textAlign: 'center' }}>
<Box>
<ToggleButtonGroup exclusive value={localStorage.getItem('NekoMaid:colorMode') || ''} onChange={(_, it) => {
localStorage.setItem('NekoMaid:colorMode', it)
location.reload()
}}>
<ToggleButton value='light'><Brightness7 /> {lang.config.light}</ToggleButton>
<ToggleButton value=''><SettingsBrightness /> {lang.config.system}</ToggleButton>
<ToggleButton value='dark'><Brightness4 /> {lang.config.dark}</ToggleButton>
</ToggleButtonGroup>
</Box>
<Paper sx={{ marginTop: 2, width: '176px', overflow: 'hidden', display: 'inline-block' }}>
{Object.keys(colors).slice(1, 17).map((key, i) => {
const checked = color === key
const elm = <Box
key={key}
onClick={() => {
localStorage.setItem('NekoMaid:color', key)
location.reload()
}}
sx={{
backgroundColor: (colors as any)[key][600],
width: '44px',
height: '44px',
display: 'inline-block',
cursor: 'pointer'
}}
><Check htmlColor='white' sx={{ top: '10px', position: 'relative', opacity: checked ? 1 : 0 }} /></Box>
return (i + 1) % 4 === 0 ? <React.Fragment key={key}>{elm}<br /></React.Fragment> : elm
})}
</Paper>
</CardContent>
}
})
Example #10
Source File: CollectionRowActions.tsx From firecms with MIT License | 4 votes |
/**
*
* @param entity
* @param isSelected
* @param selectionEnabled
* @param size
* @param toggleEntitySelection
* @param onCopyClicked
* @param onEditClicked
* @param onDeleteClicked
* @constructor
*
* @category Collection components
*/
export function CollectionRowActions<M extends { [Key: string]: any }>({
entity,
isSelected,
selectionEnabled,
size,
toggleEntitySelection,
onCopyClicked,
onEditClicked,
onDeleteClicked
}:
{
entity: Entity<M>,
size: CollectionSize,
isSelected?: boolean,
selectionEnabled?: boolean,
toggleEntitySelection?: (selectedEntity: Entity<M>) => void
onEditClicked?: (selectedEntity: Entity<M>) => void,
onCopyClicked?: (selectedEntity: Entity<M>) => void,
onDeleteClicked?: (selectedEntity: Entity<M>) => void,
}) {
const editEnabled = Boolean(onEditClicked);
const copyEnabled = Boolean(onCopyClicked);
const deleteEnabled = Boolean(onDeleteClicked);
const classes = useTableStyles();
const [anchorEl, setAnchorEl] = React.useState<any | null>(null);
const openMenu = useCallback((event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
event.stopPropagation();
}, [setAnchorEl]);
const closeMenu = useCallback(() => {
setAnchorEl(null);
}, [setAnchorEl]);
const onCheckboxChange = (event: React.ChangeEvent) => {
if (toggleEntitySelection)
toggleEntitySelection(entity);
event.stopPropagation();
};
const onDeleteClick = useCallback((event: MouseEvent) => {
event.stopPropagation();
if (onDeleteClicked)
onDeleteClicked(entity);
setAnchorEl(null);
}, [entity, onDeleteClicked, setAnchorEl]);
const onCopyClick = useCallback((event: MouseEvent) => {
event.stopPropagation();
if (onCopyClicked)
onCopyClicked(entity);
setAnchorEl(null);
}, [entity, onCopyClicked, setAnchorEl]);
return (
<div className={classes.cellButtonsWrap}>
{(editEnabled || deleteEnabled || selectionEnabled) &&
<div className={classes.cellButtons}
>
{editEnabled &&
<Tooltip title={`Edit ${entity.id}`}>
<IconButton
onClick={(event: MouseEvent) => {
event.stopPropagation();
if (onEditClicked)
onEditClicked(entity);
}}
size="large">
<KeyboardTab/>
</IconButton>
</Tooltip>
}
{selectionEnabled &&
<Tooltip title={`Select ${entity.id}`}>
<Checkbox
checked={isSelected}
onChange={onCheckboxChange}
/>
</Tooltip>}
{(copyEnabled || deleteEnabled) &&
<IconButton onClick={openMenu} size="large">
<MoreVert/>
</IconButton>
}
{(copyEnabled || deleteEnabled) && <Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
elevation={2}
>
{deleteEnabled && <MenuItem onClick={onDeleteClick}>
<ListItemIcon>
<Delete/>
</ListItemIcon>
<ListItemText primary={"Delete"}/>
</MenuItem>}
{copyEnabled && <MenuItem onClick={onCopyClick}>
<ListItemIcon>
<FileCopy/>
</ListItemIcon>
<ListItemText primary="Copy"/>
</MenuItem>}
</Menu>}
</div>}
{size !== "xs" && (
<div className={classes.cellButtonsId}>
{entity
? <Typography
className={"mono"}
variant={"caption"}
color={"textSecondary"}> {entity.id} </Typography>
: <Skeleton variant="text"/>
}
</div>
)}
</div>
);
}
Example #11
Source File: GroupList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function GroupList() {
const [showGroupCreationModal, setShowGroupCreationModal] = useState(false);
const [showGroupDeletionModal, setShowGroupDeletionModal] = useState(false);
const [groupToDelete, setGroupToDelete] = useState(null);
const groups = useRecoilValue(groupList);
const isGuest = useRecoilValue(isGuestUser);
const openGroupDeletionModal = (groupID) => {
setGroupToDelete(groups.find((group) => group.id === groupID));
setShowGroupDeletionModal(true);
};
const closeGroupDeletionModal = () => {
setShowGroupDeletionModal(false);
setGroupToDelete(null);
};
const openGroupCreateModal = () => {
setShowGroupCreationModal(true);
};
const closeGroupCreateModal = (evt, reason) => {
if (reason !== "backdropClick") {
setShowGroupCreationModal(false);
}
};
return (
<MobilePaper>
<Typography component="h3" variant="h5">
Groups
</Typography>
{isGuest && (
<Alert severity="info">
You are a guest user on this Abrechnung and therefore not permitted to create new groups.
</Alert>
)}
<List>
{groups.length === 0 ? (
<ListItem key={0}>
<span>No Groups</span>
</ListItem>
) : (
groups.map((group) => {
return (
<ListItem sx={{ padding: 0 }} key={group.id}>
<ListItemLink to={`/groups/${group.id}`}>
<ListItemText primary={group.name} secondary={group.description} />
</ListItemLink>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="delete-group"
onClick={() => openGroupDeletionModal(group.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
})
)}
</List>
{!isGuest && (
<>
<Grid container justifyContent="center">
<IconButton color="primary" onClick={openGroupCreateModal}>
<Add />
</IconButton>
</Grid>
<GroupCreateModal show={showGroupCreationModal} onClose={closeGroupCreateModal} />
</>
)}
<GroupDeleteModal
show={showGroupDeletionModal}
onClose={closeGroupDeletionModal}
groupToDelete={groupToDelete}
/>
</MobilePaper>
);
}
Example #12
Source File: GroupInvites.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function GroupInvites({ group }) {
const [showModal, setShowModal] = useState(false);
const invites = useRecoilValue(groupInvites(group.id));
const members = useRecoilValue(groupMembers(group.id));
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const isGuest = useRecoilValue(isGuestUser);
useTitle(`${group.name} - Invite Links`);
const deleteToken = (id) => {
deleteGroupInvite({ groupID: group.id, inviteID: id }).catch((err) => {
toast.error(err);
});
};
const getMemberUsername = (member_id) => {
const member = members.find((member) => member.user_id === member_id);
if (member === undefined) {
return "unknown";
}
return member.username;
};
const selectLink = (event) => {
const node = event.target;
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
};
const copyToClipboard = (content) => {
navigator.clipboard.writeText(content);
toast.info("Link copied to clipboard!");
};
return (
<MobilePaper>
<Typography component="h3" variant="h5">
Active Invite Links
</Typography>
{isGuest && (
<Alert severity="info">
You are a guest user on this Abrechnung and therefore not permitted to create group invites.
</Alert>
)}
<List>
{invites.length === 0 ? (
<ListItem>
<ListItemText primary="No Links" />
</ListItem>
) : (
invites.map((invite) => (
<ListItem key={invite.id}>
<ListItemText
primary={
invite.token === null ? (
<span>token hidden, was created by another member</span>
) : (
<span onClick={selectLink}>
{window.location.origin}/invite/
{invite.token}
</span>
)
}
secondary={
<>
{invite.description}, created by {getMemberUsername(invite.created_by)}, valid
until{" "}
{DateTime.fromISO(invite.valid_until).toLocaleString(DateTime.DATETIME_FULL)}
{invite.single_use && ", single use"}
{invite.join_as_editor && ", join as editor"}
</>
}
/>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() =>
copyToClipboard(`${window.location.origin}/invite/${invite.token}`)
}
>
<ContentCopy />
</IconButton>
<IconButton color="error" onClick={() => deleteToken(invite.id)}>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))
)}
</List>
{userPermissions.can_write && !isGuest && (
<>
<Grid container justifyContent="center">
<IconButton color="primary" onClick={() => setShowModal(true)}>
<Add />
</IconButton>
</Grid>
<InviteLinkCreate show={showModal} onClose={() => setShowModal(false)} group={group} />
</>
)}
</MobilePaper>
);
}
Example #13
Source File: AccountList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function AccountList({ group }) {
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);
const [showPersonalAccountCreationModal, setShowPersonalAccountCreationModal] = useState(false);
const [showClearingAccountCreationModal, setShowClearingAccountCreationModal] = useState(false);
const [activeTab, setActiveTab] = useState("personal");
const [searchValuePersonal, setSearchValuePersonal] = useState("");
const [searchValueClearing, setSearchValueClearing] = useState("");
const [showPersonalAccountEditModal, setShowPersonalAccountEditModal] = useState(false);
const [showClearingAccountEditModal, setShowClearingAccountEditModal] = useState(false);
const [clearingAccountToCopy, setClearingAccountToCopy] = useState(undefined);
const [accountToEdit, setAccountToEdit] = useState(null);
const [clearingAccountToEdit, setClearingAccountToEdit] = useState(null);
const setAccounts = useSetRecoilState(groupAccounts(group.id));
const personalAccounts = useRecoilValue(personalAccountsSeenByUser(group.id));
const clearingAccounts = useRecoilValue(clearingAccountsSeenByUser(group.id));
const allAccounts = useRecoilValue(accountsSeenByUser(group.id));
const [accountToDelete, setAccountToDelete] = useState(null);
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const currentUser = useRecoilValue(userData);
const memberIDToUsername = useRecoilValue(groupMemberIDsToUsername(group.id));
const [filteredPersonalAccounts, setFilteredPersonalAccounts] = useState([]);
const [filteredClearingAccounts, setFilteredClearingAccounts] = useState([]);
useEffect(() => {
if (searchValuePersonal != null && searchValuePersonal !== "") {
setFilteredPersonalAccounts(
personalAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValuePersonal.toLowerCase()) ||
t.description.toLowerCase().includes(searchValuePersonal.toLowerCase())
);
})
);
} else {
return setFilteredPersonalAccounts(personalAccounts);
}
}, [personalAccounts, searchValuePersonal, setFilteredPersonalAccounts]);
useEffect(() => {
if (searchValueClearing != null && searchValueClearing !== "") {
setFilteredClearingAccounts(
clearingAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValueClearing.toLowerCase()) ||
t.description.toLowerCase().includes(searchValueClearing.toLowerCase())
);
})
);
} else {
return setFilteredClearingAccounts(clearingAccounts);
}
}, [clearingAccounts, searchValueClearing, setFilteredClearingAccounts]);
useTitle(`${group.name} - Accounts`);
const openAccountEdit = (account) => {
setAccountToEdit(account);
setShowPersonalAccountEditModal(true);
};
const closeAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountEditModal(false);
setAccountToEdit(null);
}
};
const openClearingAccountEdit = (account) => {
setClearingAccountToEdit(account);
setShowClearingAccountEditModal(true);
};
const closeClearingAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountEditModal(false);
setClearingAccountToEdit(null);
}
};
const confirmDeleteAccount = () => {
if (accountToDelete !== null) {
deleteAccount({ accountID: accountToDelete })
.then((account) => {
updateAccount(account, setAccounts);
setAccountToDelete(null);
})
.catch((err) => {
toast.error(err);
});
}
};
const openCreateDialog = () => {
setClearingAccountToCopy(undefined);
setShowClearingAccountCreationModal(true);
};
const copyClearingAccount = (account) => {
setClearingAccountToCopy(account);
setShowClearingAccountCreationModal(true);
};
return (
<>
<MobilePaper>
<TabContext value={activeTab}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList onChange={(e, newValue) => setActiveTab(newValue)} centered>
<Tab
value="personal"
label={
<TextBadge badgeContent={personalAccounts.length} color="primary">
<span>Personal Accounts</span>
</TextBadge>
}
/>
<Tab
label={
<TextBadge badgeContent={clearingAccounts.length} color="primary">
<span>Clearing Accounts</span>
</TextBadge>
}
value="clearing"
/>
</TabList>
</Box>
<TabPanel value="personal">
<List>
{personalAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValuePersonal}
onChange={(e) => setSearchValuePersonal(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValuePersonal("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredPersonalAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText
primary={
<div>
<span>{account.name}</span>
{account.owning_user_id === currentUser.id ? (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="primary"
label="you"
/>
</span>
) : (
account.owning_user_id !== null && (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="secondary"
label={
memberIDToUsername[
account.owning_user_id
]
}
/>
</span>
)
)}
</div>
}
secondary={account.description}
/>
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Personal Account">
<IconButton
color="primary"
onClick={() => setShowPersonalAccountCreationModal(true)}
>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateAccountModal
show={showPersonalAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountCreationModal(false);
}
}}
group={group}
/>
<EditAccountModal
show={showPersonalAccountEditModal}
onClose={closeAccountEdit}
account={accountToEdit}
group={group}
/>
</>
)}
</TabPanel>
<TabPanel value="clearing">
<List>
{clearingAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValueClearing}
onChange={(e) => setSearchValueClearing(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValueClearing("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredClearingAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText primary={account.name} secondary={account.description} />
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openClearingAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="primary"
onClick={() => copyClearingAccount(account)}
>
<ContentCopy />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Clearing Account">
<IconButton color="primary" onClick={openCreateDialog}>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateClearingAccountModal
show={showClearingAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountCreationModal(false);
}
}}
initialValues={clearingAccountToCopy}
group={group}
/>
<EditClearingAccountModal
show={showClearingAccountEditModal}
onClose={closeClearingAccountEdit}
account={clearingAccountToEdit}
group={group}
/>
</>
)}
</TabPanel>
</TabContext>
</MobilePaper>
{userPermissions.can_write && (
<>
<SpeedDial
ariaLabel="Create Account"
sx={{ position: "fixed", bottom: 20, right: 20 }}
icon={<SpeedDialIcon />}
// onClose={() => setSpeedDialOpen(false)}
// onOpen={() => setSpeedDialOpen(true)}
onClick={toggleSpeedDial}
open={speedDialOpen}
>
<SpeedDialAction
icon={<PersonalAccountIcon />}
tooltipTitle="Personal"
tooltipOpen
onClick={() => setShowPersonalAccountCreationModal(true)}
/>
<SpeedDialAction
icon={<ClearingAccountIcon />}
tooltipTitle="Clearing"
tooltipOpen
onClick={openCreateDialog}
/>
</SpeedDial>
<Dialog maxWidth="xs" aria-labelledby="confirmation-dialog-title" open={accountToDelete !== null}>
<DialogTitle id="confirmation-dialog-title">Confirm delete account</DialogTitle>
<DialogContent dividers>
Are you sure you want to delete the account "
{allAccounts.find((acc) => acc.id === accountToDelete)?.name}"
</DialogContent>
<DialogActions>
<Button autoFocus onClick={() => setAccountToDelete(null)} color="primary">
Cancel
</Button>
<Button onClick={confirmDeleteAccount} color="error">
Ok
</Button>
</DialogActions>
</Dialog>
</>
)}
</>
);
}
Example #14
Source File: TransactionPositions.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
function PositionTableRow({
position,
updatePosition,
transactionAccounts,
showAdvanced,
copyPosition,
updatePositionUsage,
showAccountSelect,
showAddAccount,
deletePosition,
}) {
const validateFloat = (value) => {
return !(value === null || value === undefined || value === "" || isNaN(parseFloat(value)));
};
return (
<>
<TableCell key={`position-${position.id}-name`}>
<WrappedTextField
key={`position-${position.id}-name`}
value={position.name}
id={`position-${position.id}-name`}
onChange={(value) => updatePosition(position, value, position.price, position.communist_shares)}
validate={(value) => value !== "" && value != null}
/>
</TableCell>
<TableCell key={`position-${position.id}-communist`} align="right">
<WrappedTextField
key={`position-${position.id}-communist`}
id={`position-${position.id}-communist`}
value={position.price}
style={{ width: 70 }}
onChange={(value) =>
updatePosition(position, position.name, parseFloat(value), position.communist_shares)
}
validate={validateFloat}
errorMsg={"float required"}
/>
</TableCell>
{transactionAccounts.map((accountID) => (
<TableCell align="right" key={accountID}>
{showAdvanced ? (
<ShareInput
value={
position.usages.hasOwnProperty(String(accountID))
? position.usages[String(accountID)]
: 0
}
onChange={(value) => updatePositionUsage(position, accountID, value)}
inputProps={{ tabIndex: -1 }}
/>
) : (
<Checkbox
name={`${accountID}-checked`}
checked={position.usages.hasOwnProperty(String(accountID))}
onChange={(event) => updatePositionUsage(position, accountID, event.target.checked ? 1 : 0)}
inputProps={{ tabIndex: -1 }}
/>
)}
</TableCell>
))}
{showAccountSelect && <TableCell></TableCell>}
{showAddAccount && <TableCell></TableCell>}
<TableCell align="right">
{showAdvanced ? (
<ShareInput
value={position.communist_shares}
onChange={(value) => updatePosition(position, position.name, position.price, parseFloat(value))}
inputProps={{ tabIndex: -1 }}
/>
) : (
<Checkbox
name="communist-checked"
checked={position.communist_shares !== 0}
onChange={(event) =>
updatePosition(position, position.name, position.price, event.target.checked ? 1 : 0)
}
inputProps={{ tabIndex: -1 }}
/>
)}
</TableCell>
<TableCell>
<IconButton onClick={() => copyPosition(position)} tabIndex={-1}>
<ContentCopy />
</IconButton>
<IconButton onClick={() => deletePosition(position)} tabIndex={-1}>
<Delete />
</IconButton>
</TableCell>
</>
);
}
Example #15
Source File: TransactionActions.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function TransactionActions({ groupID, transaction }) {
const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = useState(false);
const history = useHistory();
const userPermissions = useRecoilValue(currUserPermissions(groupID));
const setTransactions = useSetRecoilState(groupTransactions(transaction.group_id));
const localTransactionChanges = useRecoilValue(pendingTransactionDetailChanges(transaction.id));
const localPositionChanges = useRecoilValue(pendingTransactionPositionChanges(transaction.id));
const resetLocalTransactionChanges = useResetRecoilState(pendingTransactionDetailChanges(transaction.id));
const resetLocalPositionChanges = useResetRecoilState(pendingTransactionPositionChanges(transaction.id));
const updateTransactionAndClearLocal = useRecoilTransaction_UNSTABLE(
({ get, set, reset }) =>
(transaction: TransactionBackend) => {
set(groupTransactions(transaction.group_id), (currTransactions) => {
return currTransactions.map((t) => (t.id === transaction.id ? transaction : t));
});
reset(pendingTransactionDetailChanges(transaction.id));
reset(pendingTransactionPositionChanges(transaction.id));
}
);
const edit = () => {
if (!transaction.is_wip) {
createTransactionChange({
transactionID: transaction.id,
})
.then((t) => {
updateTransactionAndClearLocal(t);
})
.catch((err) => {
toast.error(err);
});
}
};
const abortEdit = () => {
if (transaction.is_wip) {
if (transaction.has_committed_changes) {
discardTransactionChange({
transactionID: transaction.id,
})
.then((t) => {
updateTransactionAndClearLocal(t);
})
.catch((err) => {
toast.error(err);
});
} else {
history.push(`/groups/${groupID}/`);
}
}
};
const commitEdit = () => {
if (transaction.is_wip) {
// update the transaction given the currently pending changes
// find out which local changes we have and send them to da server
const positions = Object.values(localPositionChanges.modified)
.concat(
Object.values(localPositionChanges.added).map((position) => ({
...position,
id: -1,
}))
)
.map((p) => ({
id: p.id,
name: p.name,
communist_shares: p.communist_shares,
price: p.price,
usages: p.usages,
deleted: p.deleted,
}));
if (Object.keys(localTransactionChanges).length > 0) {
updateTransaction({
transactionID: transaction.id,
description: transaction.description,
value: transaction.value,
billedAt: transaction.billed_at,
currencySymbol: transaction.currency_symbol,
currencyConversionRate: transaction.currency_conversion_rate,
creditorShares: transaction.creditor_shares,
debitorShares: transaction.debitor_shares,
...localTransactionChanges,
positions: positions.length > 0 ? positions : null,
})
.then((t) => {
updateTransactionAndClearLocal(t);
})
.catch((err) => {
toast.error(err);
});
} else if (positions.length > 0) {
updateTransactionPositions({
transactionID: transaction.id,
positions: positions,
})
.then((t) => {
updateTransactionAndClearLocal(t);
})
.catch((err) => {
toast.error(err);
});
} else {
commitTransaction({ transactionID: transaction.id })
.then((t) => {
updateTransactionAndClearLocal(t);
})
.catch((err) => {
toast.error(err);
});
}
}
};
const confirmDeleteTransaction = () => {
deleteTransaction({ transactionID: transaction.id })
.then((t) => {
// TODO: use recoil transaction
updateTransactionInState(t, setTransactions);
resetLocalPositionChanges();
resetLocalTransactionChanges();
history.push(`/groups/${groupID}/`);
})
.catch((err) => {
toast.error(err);
});
};
return (
<>
<Grid container justifyContent="space-between">
<Grid item sx={{ display: "flex", alignItems: "center" }}>
<IconButton
sx={{ display: { xs: "none", md: "inline-flex" } }}
component={RouterLink}
to={`/groups/${groupID}/`}
>
<ChevronLeft />
</IconButton>
<Chip color="primary" label={transaction.type} />
</Grid>
<Grid item>
{userPermissions.can_write && (
<>
{transaction.is_wip ? (
<>
<Button color="primary" onClick={commitEdit}>
Save
</Button>
<Button color="error" onClick={abortEdit}>
Cancel
</Button>
</>
) : (
<IconButton color="primary" onClick={edit}>
<Edit />
</IconButton>
)}
<IconButton color="error" onClick={() => setConfirmDeleteDialogOpen(true)}>
<Delete />
</IconButton>
</>
)}
</Grid>
</Grid>
<Dialog maxWidth="xs" aria-labelledby="confirmation-dialog-title" open={confirmDeleteDialogOpen}>
<DialogTitle id="confirmation-dialog-title">Confirm delete transaction</DialogTitle>
<DialogContent dividers>
Are you sure you want to delete the transaction "{transaction.description}"
</DialogContent>
<DialogActions>
<Button autoFocus onClick={() => setConfirmDeleteDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={confirmDeleteTransaction} color="error">
Ok
</Button>
</DialogActions>
</Dialog>
</>
);
}
Example #16
Source File: FileGallery.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function FileGallery({ transaction }) {
const [files, setFiles] = useState([]); // map of file id to object
const [active, setActive] = useState(0);
const setTransactions = useSetRecoilState(groupTransactions(transaction.group_id));
const [showUploadDialog, setShowUploadDialog] = useState(false);
const [showImage, setShowImage] = useState(false);
useEffect(() => {
const newFileIDs = new Set(transaction.files.map((file) => file.id));
const filteredFiles = files.reduce((map, file) => {
map[file.id] = file;
return map;
}, {});
for (const loadedFile of files) {
if (!newFileIDs.has(loadedFile.id)) {
URL.revokeObjectURL(loadedFile.objectUrl); // clean up memory
delete filteredFiles[loadedFile.id];
}
}
setFiles(Object.values(filteredFiles)); // TODO: maybe include placeholders
setActive(Math.max(0, Math.min(active, transaction.files.length - 1)));
const newFiles = transaction.files.filter((file) => !filteredFiles.hasOwnProperty(file.id));
Promise.all(
newFiles.map((newFile) => {
return fetchFile({
fileID: newFile.id,
blobID: newFile.blob_id,
}).then((resp) => {
const objectUrl = URL.createObjectURL(resp.data);
return {
...newFile,
objectUrl: objectUrl,
};
});
})
)
.then((newlyLoadedFiles) => {
setFiles([...Object.values(filteredFiles), ...newlyLoadedFiles]);
})
.catch((err) => {
toast.error(`Error loading file: ${err}`);
});
}, [transaction]);
const toNextImage = () => {
if (active < files.length - 1) {
setActive(active + 1);
}
};
const toPrevImage = () => {
if (active > 0) {
setActive(active - 1);
}
};
const doShowImage = (img) => {
setShowImage(true);
};
const deleteSelectedFile = () => {
if (active < files.length) {
// sanity check, should not be needed
deleteFile({ fileID: files[active].id })
.then((t) => {
updateTransactionInState(t, setTransactions);
setShowImage(false);
})
.catch((err) => {
toast.error(`Error deleting file: ${err}`);
});
}
};
// @ts-ignore
return (
<>
<Grid
container
justifyContent="center"
alignItems="center"
style={{
position: "relative",
height: "200px",
width: "100%",
}}
>
{files.length === 0 ? (
<img height="100%" src={placeholderImg} alt="placeholder" />
) : (
files.map((item, idx) => (
<Transition key={item.id} in={active === idx} timeout={duration}>
{(state) => (
<img
height="100%"
style={{
...defaultStyle,
...transitionStyles[state],
}}
onClick={() => doShowImage(item)}
src={item.objectUrl}
srcSet={item.objectUrl}
alt={item.filename.split(".")[0]}
loading="lazy"
/>
)}
</Transition>
))
)}
<Chip
sx={{ position: "absolute", top: "5px", right: "10px" }}
size="small"
label={`${Math.min(files.length, active + 1)} / ${files.length}`}
/>
{active > 0 && (
<IconButton onClick={toPrevImage} sx={{ position: "absolute", top: "40%", left: "10px" }}>
<ChevronLeft />
</IconButton>
)}
{active < files.length - 1 && (
<IconButton onClick={toNextImage} sx={{ position: "absolute", top: "40%", right: "10px" }}>
<ChevronRight />
</IconButton>
)}
{transaction.is_wip && (
<>
<IconButton
color="primary"
sx={{
position: "absolute",
top: "80%",
right: "10px",
}}
onClick={() => setShowUploadDialog(true)}
>
<AddCircle fontSize="large" />
</IconButton>
<ImageUploadDialog
transaction={transaction}
show={showUploadDialog}
onClose={() => setShowUploadDialog(false)}
/>
</>
)}
</Grid>
<Dialog open={showImage} onClose={() => setShowImage(false)} scroll="body">
{active < files.length && <DialogTitle>{files[active].filename.split(".")[0]}</DialogTitle>}
<DialogContent>
<Grid
container
justifyContent="center"
alignItems="center"
style={{
position: "relative",
}}
>
{active < files.length && (
<img
height="100%"
width="100%"
src={files[active]?.objectUrl}
srcSet={files[active]?.objectUrl}
alt={files[active]?.filename}
loading="lazy"
/>
)}
{active > 0 && (
<IconButton
onClick={toPrevImage}
sx={{
position: "absolute",
top: "40%",
left: "0px",
}}
>
<ChevronLeft />
</IconButton>
)}
{active < files.length - 1 && (
<IconButton
onClick={toNextImage}
sx={{
position: "absolute",
top: "40%",
right: "0px",
}}
>
<ChevronRight />
</IconButton>
)}
</Grid>
</DialogContent>
{transaction.is_wip && (
<DialogActions>
<Button startIcon={<Delete />} onClick={deleteSelectedFile} variant="outlined" color="error">
Delete
</Button>
</DialogActions>
)}
</Dialog>
</>
);
}
Example #17
Source File: EntityCollectionView.tsx From firecms with MIT License | 4 votes |
/**
* This component is in charge of binding a datasource path with an {@link EntityCollection}
* where it's configuration is defined. It includes an infinite scrolling table,
* 'Add' new entities button,
*
* This component is the default one used for displaying entity collections
* and is in charge of generating all the specific actions and customization
* of the lower level {@link CollectionTable}
*
* Please **note** that you only need to use this component if you are building
* a custom view. If you just need to create a default view you can do it
* exclusively with config options.
*
* If you need a lower level implementation with more granular options, you
* can use {@link CollectionTable}.
*
* If you need a table that is not bound to the datasource or entities and
* properties at all, you can check {@link Table}
*
* @param path
* @param collection
* @constructor
* @category Components
*/
export function EntityCollectionView<M extends { [Key: string]: any }>({
path,
collection: baseCollection
}: EntityCollectionViewProps<M>
) {
const sideEntityController = useSideEntityController();
const context = useFireCMSContext();
const authController = useAuthController();
const navigationContext = useNavigation();
const theme = useTheme();
const largeLayout = useMediaQuery(theme.breakpoints.up("md"));
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
const collectionResolver = navigationContext.getCollectionResolver<M>(path);
if (!collectionResolver) {
throw Error(`Couldn't find the corresponding collection view for the path: ${path}`);
}
const onCollectionModifiedForUser = useCallback((partialCollection: PartialEntityCollection<any>) => {
navigationContext.onCollectionModifiedForUser(path, partialCollection);
}, [path]);
const collection: EntityCollection<M> = collectionResolver ?? baseCollection;
const { schemaResolver } = collectionResolver;
const exportable = collection.exportable === undefined || collection.exportable;
const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
const hoverRow = collection.inlineEditing !== undefined && !collection.inlineEditing;
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const selectionController = useSelectionController<M>();
const usedSelectionController = collection.selectionController ?? selectionController;
const {
selectedEntities,
toggleEntitySelection,
isEntitySelected,
setSelectedEntities
} = usedSelectionController;
useEffect(() => {
setDeleteEntityClicked(undefined);
}, [selectedEntities]);
const onEntityClick = useCallback((entity: Entity<M>) => {
return sideEntityController.open({
entityId: entity.id,
path,
permissions: collection.permissions,
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
}, [path, collection, sideEntityController]);
const onNewClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
return sideEntityController.open({
path,
permissions: collection.permissions,
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
}, [path, collection, sideEntityController]);
const internalOnEntityDelete = useCallback((_path: string, entity: Entity<M>) => {
setSelectedEntities(selectedEntities.filter((e) => e.id !== entity.id));
}, [selectedEntities, setSelectedEntities]);
const internalOnMultipleEntitiesDelete = useCallback((_path: string, entities: Entity<M>[]) => {
setSelectedEntities([]);
setDeleteEntityClicked(undefined);
}, [setSelectedEntities]);
const checkInlineEditing = useCallback((entity: Entity<any>) => {
if (!canEdit(collection.permissions, entity, authController, path, context)) {
return false;
}
return collection.inlineEditing === undefined || collection.inlineEditing;
}, [collection.inlineEditing, collection.permissions, path]);
const onColumnResize = useCallback(({
width,
key
}: OnColumnResizeParams) => {
// Only for property columns
if (!collection.schema.properties[key]) return;
const property: Partial<AnyProperty> = { columnWidth: width };
const updatedFields: PartialEntityCollection<any> = { schema: { properties: { [key as keyof M]: property } } };
if (onCollectionModifiedForUser)
onCollectionModifiedForUser(updatedFields)
}, [collection.schema.properties, onCollectionModifiedForUser]);
const onSizeChanged = useCallback((size: CollectionSize) => {
if (onCollectionModifiedForUser)
onCollectionModifiedForUser({ defaultSize: size })
}, [onCollectionModifiedForUser]);
const open = anchorEl != null;
const title = useMemo(() => (
<div style={{
padding: "4px"
}}>
<Typography
variant="h6"
style={{
lineHeight: "1.0",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
maxWidth: "160px",
cursor: collection.description ? "pointer" : "inherit"
}}
onClick={collection.description
? (e) => {
setAnchorEl(e.currentTarget);
e.stopPropagation();
}
: undefined}
>
{`${collection.name}`}
</Typography>
<Typography
style={{
display: "block",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
maxWidth: "160px",
direction: "rtl",
textAlign: "left"
}}
variant={"caption"}
color={"textSecondary"}>
{`/${path}`}
</Typography>
{collection.description &&
<Popover
id={"info-dialog"}
open={open}
anchorEl={anchorEl}
elevation={1}
onClose={() => {
setAnchorEl(null);
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
>
<Box m={2}>
<Markdown source={collection.description}/>
</Box>
</Popover>
}
</div>
), [collection.description, collection.name, path, open, anchorEl]);
const tableRowActionsBuilder = useCallback(({
entity,
size
}: { entity: Entity<any>, size: CollectionSize }) => {
const isSelected = isEntitySelected(entity);
const createEnabled = canCreate(collection.permissions, authController, path, context);
const editEnabled = canEdit(collection.permissions, entity, authController, path, context);
const deleteEnabled = canDelete(collection.permissions, entity, authController, path, context);
const onCopyClicked = (clickedEntity: Entity<M>) => sideEntityController.open({
entityId: clickedEntity.id,
path,
copy: true,
permissions: {
edit: editEnabled,
create: createEnabled,
delete: deleteEnabled
},
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
const onEditClicked = (clickedEntity: Entity<M>) => sideEntityController.open({
entityId: clickedEntity.id,
path,
permissions: {
edit: editEnabled,
create: createEnabled,
delete: deleteEnabled
},
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
return (
<CollectionRowActions
entity={entity}
isSelected={isSelected}
selectionEnabled={selectionEnabled}
size={size}
toggleEntitySelection={toggleEntitySelection}
onEditClicked={onEditClicked}
onCopyClicked={createEnabled ? onCopyClicked : undefined}
onDeleteClicked={deleteEnabled ? setDeleteEntityClicked : undefined}
/>
);
}, [usedSelectionController, sideEntityController, collection.permissions, authController, path]);
const toolbarActionsBuilder = useCallback((_: { size: CollectionSize, data: Entity<any>[] }) => {
const addButton = canCreate(collection.permissions, authController, path, context) && onNewClick && (largeLayout
? <Button
onClick={onNewClick}
startIcon={<Add/>}
size="large"
variant="contained"
color="primary">
Add {collection.schema.name}
</Button>
: <Button
onClick={onNewClick}
size="medium"
variant="contained"
color="primary"
>
<Add/>
</Button>);
const multipleDeleteEnabled = selectedEntities.every((entity) => canDelete(collection.permissions, entity, authController, path, context));
const onMultipleDeleteClick = (event: React.MouseEvent) => {
event.stopPropagation();
setDeleteEntityClicked(selectedEntities);
};
const multipleDeleteButton = selectionEnabled &&
<Tooltip
title={multipleDeleteEnabled ? "Multiple delete" : "You have selected one entity you cannot delete"}>
<span>
{largeLayout && <Button
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
startIcon={<Delete/>}
onClick={onMultipleDeleteClick}
color={"primary"}
>
<p style={{ minWidth: 24 }}>({selectedEntities?.length})</p>
</Button>}
{!largeLayout &&
<IconButton
color={"primary"}
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
onClick={onMultipleDeleteClick}
size="large">
<Delete/>
</IconButton>}
</span>
</Tooltip>;
const extraActions = collection.extraActions
? collection.extraActions({
path,
collection,
selectionController: usedSelectionController,
context
})
: undefined;
const exportButton = exportable &&
<ExportButton schema={collection.schema}
schemaResolver={schemaResolver}
exportConfig={typeof collection.exportable === "object" ? collection.exportable : undefined}
path={path}/>;
return (
<>
{extraActions}
{multipleDeleteButton}
{exportButton}
{addButton}
</>
);
}, [usedSelectionController, path, collection, largeLayout]);
return (
<>
<CollectionTable
key={`collection_table_${path}`}
title={title}
path={path}
collection={collection}
schemaResolver={schemaResolver}
onSizeChanged={onSizeChanged}
inlineEditing={checkInlineEditing}
onEntityClick={onEntityClick}
onColumnResize={onColumnResize}
tableRowActionsBuilder={tableRowActionsBuilder}
toolbarActionsBuilder={toolbarActionsBuilder}
hoverRow={hoverRow}
/>
{deleteEntityClicked && <DeleteEntityDialog entityOrEntitiesToDelete={deleteEntityClicked}
path={path}
schema={collection.schema}
schemaResolver={schemaResolver}
callbacks={collection.callbacks}
open={!!deleteEntityClicked}
onEntityDelete={internalOnEntityDelete}
onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
onClose={() => setDeleteEntityClicked(undefined)}/>}
</>
);
}