@mui/icons-material#Add TypeScript Examples

The following examples show how to use @mui/icons-material#Add. 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: AddToQueue.tsx    From multi-downloader-nx with MIT License 6 votes vote down vote up
AddToQueue: React.FC = () => {
  const [isOpen, setOpen] = React.useState(false);

  return <Box>
    <EpisodeListing />
    <Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md'>
      <Box sx={{ border: '2px solid white', p: 2 }}>
        <SearchBox />
        <Divider variant='middle' className="divider-width" light sx={{ color: 'text.primary', fontSize: '1.2rem' }}>Options</Divider>
        <DownloadSelector onFinish={() => setOpen(false)} />
      </Box>
    </Dialog>
    <Button variant='contained' onClick={() => setOpen(true)}>
      <Add />
    </Button>
  </Box>
}
Example #2
Source File: UseEquipped.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
function NewItem({ onAdd, list }: { onAdd: (ck: CharacterKey) => void, list: CharacterKey[] }) {
  const { t } = useTranslation("page_character")
  const [show, onOpen, onClose] = useBoolState(false)
  const filter = useCallback((char?: ICachedCharacter) => {
    if (!char) return false
    return !list.includes(char.key)
  }, [list])
  return <>
    <CharacterSelectionModal show={show} onHide={onClose} onSelect={onAdd} filter={filter} />
    <Button fullWidth sx={{ height: itemSize }} color="info" onClick={onOpen} startIcon={<Add />} >
      <Trans t={t} i18nKey="tabOptimize.useEquipped.modal.add">Add character to list</Trans>
    </Button>
  </>
}
Example #3
Source File: SidebarGroupList.tsx    From abrechnung with GNU Affero General Public License v3.0 5 votes vote down vote up
export default function SidebarGroupList({ group = null }) {
    const groups = useRecoilValue(groupList);
    const isGuest = useRecoilValue(isGuestUser);
    const [showGroupCreationModal, setShowGroupCreationModal] = useState(false);

    const openGroupCreateModal = () => {
        setShowGroupCreationModal(true);
    };

    const closeGroupCreateModal = (evt, reason) => {
        if (reason !== "backdropClick") {
            setShowGroupCreationModal(false);
        }
    };

    return (
        <>
            <List sx={{ pt: 0 }}>
                <ListItem sx={{ pt: 0, pb: 0 }}>
                    <ListItemText secondary="Groups" />
                </ListItem>
                {groups.map((it) => (
                    <ListItemLink key={it.id} to={`/groups/${it.id}`} selected={group && group.id === it.id}>
                        <ListItemText primary={it.name} />
                    </ListItemLink>
                ))}
                {!isGuest && (
                    <ListItem sx={{ padding: 0 }}>
                        <Grid container justifyContent="center">
                            <IconButton size="small" onClick={openGroupCreateModal}>
                                <Add />
                            </IconButton>
                        </Grid>
                    </ListItem>
                )}
            </List>
            {!isGuest && <GroupCreateModal show={showGroupCreationModal} onClose={closeGroupCreateModal} />}
        </>
    );
}
Example #4
Source File: PurchaseDetails.tsx    From abrechnung with GNU Affero General Public License v3.0 5 votes vote down vote up
export default function PurchaseDetails({ group, transaction }) {
    const [showPositions, setShowPositions] = useState(false);

    return (
        <>
            <MobilePaper>
                <TransactionActions groupID={group.id} transaction={transaction} />
                <Divider sx={{ marginBottom: 1, marginTop: 1 }} />
                <Grid container>
                    <Grid item xs={12} md={transaction.is_wip || transaction.files.length > 0 ? 6 : 12}>
                        <TransactionDescription group={group} transaction={transaction} />
                        <TransactionBilledAt group={group} transaction={transaction} />

                        <TransactionCreditorShare
                            group={group}
                            transaction={transaction}
                            isEditing={transaction.is_wip}
                            label="Paid by"
                        />

                        <TransactionValue group={group} transaction={transaction} />
                    </Grid>

                    {(transaction.is_wip || transaction.files.length > 0) && (
                        <Grid item xs={12} md={6} sx={{ marginTop: { xs: 1 } }}>
                            <FileGallery transaction={transaction} />
                        </Grid>
                    )}
                    <Grid item xs={12}>
                        {transaction.is_wip ? (
                            <PurchaseDebitorShares
                                group={group}
                                transaction={transaction}
                                showPositions={showPositions}
                            />
                        ) : (
                            <PurchaseDebitorSharesReadOnly group={group} transaction={transaction} />
                        )}
                    </Grid>
                </Grid>
            </MobilePaper>

            {!showPositions &&
            transaction.is_wip &&
            transaction.positions.find((item) => !item.deleted) === undefined ? (
                <Grid container justifyContent="center" sx={{ marginTop: 2 }}>
                    <Button startIcon={<Add />} onClick={() => setShowPositions(true)}>
                        Add Positions
                    </Button>
                </Grid>
            ) : (showPositions && transaction.is_wip) ||
              transaction.positions.find((item) => !item.deleted) !== undefined ? (
                <TransactionPositions group={group} transaction={transaction} />
            ) : (
                <></>
            )}
        </>
    );
}
Example #5
Source File: Chat.tsx    From sapio-studio with Mozilla Public License 2.0 5 votes vote down vote up
function Users() {
    const [users, set_users] = React.useState<
        { nickname: string; key: string }[]
    >([]);
    React.useEffect(() => {
        let cancel: ReturnType<typeof window.setTimeout>;
        async function f() {
            await window.electron.chat.init();
            set_users(await window.electron.chat.list_users());
            cancel = setTimeout(f, 5000);
        }
        cancel = setTimeout(f, 0);
        return () => {
            clearTimeout(cancel);
        };
    }, []);
    const [add_new_user, set_add_new_user] = React.useState(false);
    function CustomToolbar() {
        return (
            <GridToolbarContainer>
                <Button onClick={() => set_add_new_user(true)}>
                    New User<Add></Add>
                </Button>
            </GridToolbarContainer>
        );
    }
    return (
        <div>
            <NewNickname
                show={add_new_user}
                hide={() => set_add_new_user(false)}
            ></NewNickname>

            <DataGrid
                components={{ Toolbar: CustomToolbar }}
                rows={users.map((v) => {
                    return { id: v.key, ...v };
                })}
                columns={UserGrid}
                disableExtendRowFullWidth={false}
                columnBuffer={3}
                pageSize={10}
                rowsPerPageOptions={[5]}
                disableColumnSelector
                disableSelectionOnClick
            />
        </div>
    );
}
Example #6
Source File: EntityCollectionView.tsx    From firecms with MIT License 4 votes vote down vote up
/**
 * 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)}/>}
        </>
    );
}
Example #7
Source File: TransactionList.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function TransactionList({ group }) {
    const [speedDialOpen, setSpeedDialOpen] = useState(false);
    const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);

    const [showTransferCreateDialog, setShowTransferCreateDialog] = useState(false);
    const [showPurchaseCreateDialog, setShowPurchaseCreateDialog] = useState(false);
    const transactions = useRecoilValue(transactionsSeenByUser(group.id));
    const currentUser = useRecoilValue(userData);
    const userPermissions = useRecoilValue(currUserPermissions(group.id));
    const userAccounts = useRecoilValue(accountsOwnedByUser({ groupID: group.id, userID: currentUser.id }));
    const groupAccountMap = useRecoilValue(accountIDsToName(group.id));

    const theme: Theme = useTheme();
    const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));

    const [filteredTransactions, setFilteredTransactions] = useState([]);

    const [searchValue, setSearchValue] = useState("");

    const [sortMode, setSortMode] = useState("last_changed"); // last_changed, description, value, billed_at

    useEffect(() => {
        let filtered = transactions;
        if (searchValue != null && searchValue !== "") {
            filtered = transactions.filter((t) => t.filter(searchValue, groupAccountMap));
        }
        filtered = [...filtered].sort(getTransactionSortFunc(sortMode));

        setFilteredTransactions(filtered);
    }, [searchValue, setFilteredTransactions, sortMode, transactions, userAccounts]);

    useTitle(`${group.name} - Transactions`);

    const openPurchaseCreateDialog = () => {
        setShowPurchaseCreateDialog(true);
    };

    const openTransferCreateDialog = () => {
        setShowTransferCreateDialog(true);
    };

    return (
        <>
            <MobilePaper>
                <Box
                    sx={{
                        display: "flex",
                        flexDirection: { xs: "column", sm: "column", md: "row", lg: "row" },
                        alignItems: { md: "flex-end" },
                        pl: "16px",
                        justifyContent: "space-between",
                    }}
                >
                    <Box sx={{ display: "flex-item" }}>
                        <Box sx={{ minWidth: "56px", pt: "16px" }}>
                            <SearchIcon sx={{ color: "action.active" }} />
                        </Box>
                        <Input
                            value={searchValue}
                            onChange={(e) => setSearchValue(e.target.value)}
                            placeholder="Search…"
                            inputProps={{
                                "aria-label": "search",
                            }}
                            sx={{ pt: "16px" }}
                            endAdornment={
                                <InputAdornment position="end">
                                    <IconButton
                                        aria-label="clear search input"
                                        onClick={(e) => setSearchValue("")}
                                        edge="end"
                                    >
                                        <Clear />
                                    </IconButton>
                                </InputAdornment>
                            }
                        />
                        <FormControl variant="standard" sx={{ minWidth: 120, ml: 3 }}>
                            <InputLabel id="select-sort-by-label">Sort by</InputLabel>
                            <Select
                                labelId="select-sort-by-label"
                                id="select-sort-by"
                                label="Sort by"
                                onChange={(evt) => setSortMode(evt.target.value)}
                                value={sortMode}
                            >
                                <MenuItem value="last_changed">Last changed</MenuItem>
                                <MenuItem value="description">Description</MenuItem>
                                <MenuItem value="value">Value</MenuItem>
                                <MenuItem value="billed_at">Date</MenuItem>
                            </Select>
                        </FormControl>
                    </Box>
                    {!isSmallScreen && (
                        <Box sx={{ display: "flex-item" }}>
                            <div style={{ padding: "8px" }}>
                                <Add color="primary" />
                            </div>
                            <Tooltip title="Create Purchase">
                                <IconButton color="primary" onClick={openPurchaseCreateDialog}>
                                    <PurchaseIcon />
                                </IconButton>
                            </Tooltip>
                            <Tooltip title="Create Transfer">
                                <IconButton color="primary" onClick={openTransferCreateDialog}>
                                    <TransferIcon />
                                </IconButton>
                            </Tooltip>
                        </Box>
                    )}
                </Box>
                <Divider sx={{ mt: 1 }} />
                <List>
                    {transactions.length === 0 ? (
                        <Alert severity="info">No Transactions</Alert>
                    ) : (
                        filteredTransactions.map((transaction) => (
                            <TransactionListEntry key={transaction.id} group={group} transaction={transaction} />
                        ))
                    )}
                </List>
                <TransferCreateModal
                    group={group}
                    show={showTransferCreateDialog}
                    onClose={(evt, reason) => {
                        if (reason !== "backdropClick") {
                            setShowTransferCreateDialog(false);
                        }
                    }}
                />
                <PurchaseCreateModal
                    group={group}
                    show={showPurchaseCreateDialog}
                    onClose={(evt, reason) => {
                        if (reason !== "backdropClick") {
                            setShowPurchaseCreateDialog(false);
                        }
                    }}
                />
            </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={<PurchaseIcon />}
                        tooltipTitle="Purchase"
                        tooltipOpen
                        onClick={openPurchaseCreateDialog}
                    />
                    <SpeedDialAction
                        icon={<TransferIcon />}
                        tooltipTitle="Transfer"
                        tooltipOpen
                        onClick={openTransferCreateDialog}
                    />
                </SpeedDial>
            )}
        </>
    );
}
Example #8
Source File: TransactionPositions.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function TransactionPositions({ group, transaction }: PropTypes) {
    const classes = useStyles();
    const accounts = useRecoilValue(accountsSeenByUser(group.id));
    const [localPositionChanges, setLocalPositionChanges] = useRecoilState(
        pendingTransactionPositionChanges(transaction.id)
    );
    const [showAdvanced, setShowAdvanced] = useState(false);

    const [positions, setPositions] = useState([]);

    useEffect(() => {
        setPositions(
            transaction.positions
                .map((p) => ({ ...p, is_empty: false }))
                .concat([
                    {
                        ...localPositionChanges.empty,
                        is_empty: true,
                    },
                ])
        );
    }, [transaction, setPositions, localPositionChanges]);

    // find all accounts that take part in the transaction, either via debitor shares or purchase items
    // TODO: should we add creditor accounts as well?
    const positionAccounts: Array<number> = Array.from(
        new Set<number>(
            positions
                .map((item) => Object.keys(item.usages))
                .flat()
                .map((id) => parseInt(id))
        )
    );

    const [additionalPurchaseItemAccounts, setAdditionalPurchaseItemAccounts] = useState([]);
    const transactionAccounts: Array<number> = Array.from(
        new Set<number>(
            Object.keys(transaction.debitor_shares)
                .map((id) => parseInt(id))
                .concat(positionAccounts)
                .concat(additionalPurchaseItemAccounts)
        )
    );

    const showAddAccount = transactionAccounts.length < accounts.length;

    const [showAccountSelect, setShowAccountSelect] = useState(false);

    const totalPositionValue = positions.reduce((acc, curr) => acc + curr.price, 0);
    const sharedTransactionValue = transaction.value - totalPositionValue;

    const purchaseItemSumForAccount = (accountID) => {
        return transaction.account_balances.hasOwnProperty(accountID)
            ? transaction.account_balances[accountID].positions
            : 0;
    };

    const updatePosition = (position, name, price, communistShares) => {
        if (position.is_empty) {
            return updateEmptyPosition(position, name, price, communistShares);
        }
        if (position.only_local) {
            setLocalPositionChanges((currPositions) => {
                let mappedAdded = { ...currPositions.added };
                mappedAdded[position.id] = {
                    ...position,
                    name: name,
                    price: price,
                    communist_shares: communistShares,
                };
                return {
                    modified: currPositions.modified,
                    added: mappedAdded,
                    empty: currPositions.empty,
                };
            });
        } else {
            setLocalPositionChanges((currPositions) => {
                let mappedModified = { ...currPositions.modified };
                mappedModified[position.id] = {
                    ...position,
                    name: name,
                    price: price,
                    communist_shares: communistShares,
                };
                return {
                    modified: mappedModified,
                    empty: currPositions.empty,
                    added: currPositions.added,
                };
            });
        }
    };

    const updatePositionUsage = (position, accountID, shares) => {
        if (position.is_empty) {
            return updateEmptyPositionUsage(position, accountID, shares);
        }
        if (position.only_local) {
            setLocalPositionChanges((currPositions) => {
                let mappedAdded = { ...currPositions.added };
                let usages = { ...currPositions.added[position.id].usages };
                if (shares === 0) {
                    delete usages[accountID];
                } else {
                    usages[accountID] = shares;
                }
                mappedAdded[position.id] = {
                    ...currPositions.added[position.id],
                    usages: usages,
                };
                return {
                    modified: currPositions.modified,
                    added: mappedAdded,
                    empty: currPositions.empty,
                };
            });
        } else {
            setLocalPositionChanges((currPositions) => {
                let mappedModified = { ...currPositions.modified };
                let usages;
                if (mappedModified.hasOwnProperty(position.id)) {
                    // we already did change something locally
                    usages = { ...currPositions.modified[position.id].usages };
                } else {
                    // we first need to copy
                    usages = { ...position.usages };
                }

                if (shares === 0) {
                    delete usages[accountID];
                } else {
                    usages[accountID] = shares;
                }
                mappedModified[position.id] = {
                    ...position,
                    ...currPositions.modified[position.id],
                    usages: usages,
                };
                return {
                    modified: mappedModified,
                    added: currPositions.added,
                    empty: currPositions.empty,
                };
            });
        }
    };

    const deletePosition = (position) => {
        if (position.is_empty) {
            return resetEmptyPosition();
        }

        if (position.only_local) {
            setLocalPositionChanges((currPositions) => {
                let mappedAdded = { ...currPositions.added };
                delete mappedAdded[position.id];
                return {
                    modified: currPositions.modified,
                    added: mappedAdded,
                    empty: currPositions.empty,
                };
            });
        } else {
            setLocalPositionChanges((currPositions) => {
                let mappedModified = { ...currPositions.modified };
                mappedModified[position.id] = {
                    ...position,
                    deleted: true,
                };
                return {
                    modified: mappedModified,
                    added: currPositions.added,
                    empty: currPositions.empty,
                };
            });
        }
    };

    const nextEmptyPositionID = (localPositions: LocalPositionChanges) => {
        return Math.min(...Object.values(localPositions.added).map((p) => p.id), -1, localPositions.empty.id) - 1;
    };

    const resetEmptyPosition = () => {
        setLocalPositionChanges((currValue) => ({
            modified: currValue.modified,
            added: currValue.added,
            empty: {
                id: nextEmptyPositionID(currValue),
                name: "",
                price: 0,
                communist_shares: 0,
                usages: {},
                deleted: false,
            },
        }));
    };

    const updateEmptyPosition = (position, name, price, communistShares) => {
        if (name !== "" && name != null) {
            const copyOfEmpty = { ...position, name: name, price: price, communist_shares: communistShares };
            setLocalPositionChanges((currPositions) => {
                let mappedAdded = { ...currPositions.added };
                mappedAdded[position.id] = copyOfEmpty;
                return {
                    modified: currPositions.modified,
                    added: mappedAdded,
                    empty: {
                        id: nextEmptyPositionID(currPositions),
                        name: "",
                        price: 0,
                        communist_shares: 0,
                        usages: {},
                        deleted: false,
                    },
                };
            });
        } else {
            setLocalPositionChanges((currPositions) => {
                return {
                    modified: currPositions.modified,
                    added: currPositions.added,
                    empty: {
                        ...position,
                        name: name,
                        price: price,
                        communist_shares: communistShares,
                    },
                };
            });
        }
    };

    const updateEmptyPositionUsage = (position, accountID, value) => {
        setLocalPositionChanges((currPositions) => {
            let newUsages = { ...position.usages };
            if (value === 0) {
                delete newUsages[accountID];
            } else {
                newUsages[accountID] = value;
            }
            return {
                modified: currPositions.modified,
                added: currPositions.added,
                empty: {
                    ...position,
                    usages: newUsages,
                },
            };
        });
    };

    const copyPosition = (position) => {
        setLocalPositionChanges((currPositions) => {
            const newPosition = {
                ...position,
                id: nextEmptyPositionID(currPositions),
            };
            let mappedAdded = { ...currPositions.added };
            mappedAdded[newPosition.id] = newPosition;
            return {
                modified: currPositions.modified,
                added: mappedAdded,
                empty: currPositions.empty,
            };
        });
    };

    const addPurchaseItemAccount = (account) => {
        setShowAccountSelect(false);
        setAdditionalPurchaseItemAccounts((currAdditionalAccounts) =>
            Array.from(new Set<number>([...currAdditionalAccounts, parseInt(account.id)]))
        );
    };

    return (
        <MobilePaper sx={{ marginTop: 2 }}>
            <Grid container direction="row" justifyContent="space-between">
                <Typography>Positions</Typography>
                {transaction.is_wip && (
                    <FormControlLabel
                        control={<Checkbox name={`show-advanced`} />}
                        checked={showAdvanced}
                        onChange={(event: React.ChangeEvent<HTMLInputElement>) => setShowAdvanced(event.target.checked)}
                        label="Advanced"
                    />
                )}
            </Grid>
            <TableContainer>
                <Table className={classes.table} stickyHeader aria-label="purchase items" size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell>Name</TableCell>
                            <TableCell align="right">Price</TableCell>
                            {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
                                <TableCell align="right" sx={{ minWidth: 80 }} key={accountID}>
                                    {accounts.find((account) => account.id === accountID).name}
                                </TableCell>
                            ))}
                            {transaction.is_wip && (
                                <>
                                    {showAccountSelect && (
                                        <TableCell align="right">
                                            <AccountSelect
                                                group={group}
                                                exclude={transactionAccounts}
                                                onChange={addPurchaseItemAccount}
                                            />
                                        </TableCell>
                                    )}
                                    {showAddAccount && (
                                        <TableCell align="right">
                                            <IconButton onClick={() => setShowAccountSelect(true)}>
                                                <Add />
                                            </IconButton>
                                        </TableCell>
                                    )}
                                </>
                            )}
                            <TableCell align="right">Shared</TableCell>
                            {transaction.is_wip && <TableCell></TableCell>}
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {transaction.is_wip
                            ? positions.map((position, idx) => (
                                  <TableRow hover key={position.id}>
                                      <PositionTableRow
                                          position={position}
                                          deletePosition={deletePosition}
                                          transactionAccounts={transactionAccounts}
                                          copyPosition={copyPosition}
                                          updatePosition={updatePosition}
                                          updatePositionUsage={updatePositionUsage}
                                          showAdvanced={showAdvanced}
                                          showAccountSelect={showAccountSelect}
                                          showAddAccount={showAddAccount}
                                      />
                                  </TableRow>
                              ))
                            : positions.map(
                                  (position) =>
                                      !position.is_empty && (
                                          <TableRow hover key={position.id}>
                                              <TableCell>{position.name}</TableCell>
                                              <TableCell align="right" style={{ minWidth: 80 }}>
                                                  {position.price.toFixed(2)} {transaction.currency_symbol}
                                              </TableCell>
                                              {positionAccounts.map((accountID) => (
                                                  <TableCell align="right" key={accountID}>
                                                      {position.usages.hasOwnProperty(String(accountID))
                                                          ? position.usages[String(accountID)]
                                                          : 0}
                                                  </TableCell>
                                              ))}
                                              <TableCell align="right">{position.communist_shares}</TableCell>
                                          </TableRow>
                                      )
                              )}
                        <TableRow hover>
                            <TableCell>
                                <Typography sx={{ fontWeight: "bold" }}>Total:</Typography>
                            </TableCell>
                            <TableCell align="right">
                                {totalPositionValue.toFixed(2)} {transaction.currency_symbol}
                            </TableCell>
                            {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
                                <TableCell align="right" key={accountID}>
                                    {purchaseItemSumForAccount(accountID).toFixed(2)} {transaction.currency_symbol}
                                </TableCell>
                            ))}
                            <TableCell align="right" colSpan={showAddAccount ? 2 : 1}>
                                {(
                                    positions.reduce((acc, curr) => acc + curr.price, 0) -
                                    Object.values(transaction.account_balances).reduce(
                                        (acc, curr) => acc + curr.positions,
                                        0
                                    )
                                ).toFixed(2)}{" "}
                                {transaction.currency_symbol}
                            </TableCell>
                            {transaction.is_wip && <TableCell></TableCell>}
                        </TableRow>
                        <TableRow hover>
                            <TableCell>
                                <Typography sx={{ fontWeight: "bold" }}>Remaining:</Typography>
                            </TableCell>
                            <TableCell align="right">
                                {sharedTransactionValue.toFixed(2)} {transaction.currency_symbol}
                            </TableCell>
                            {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
                                <TableCell align="right" key={accountID}></TableCell>
                            ))}
                            <TableCell align="right" colSpan={showAddAccount ? 2 : 1}></TableCell>
                            {transaction.is_wip && <TableCell></TableCell>}
                        </TableRow>
                    </TableBody>
                </Table>
            </TableContainer>
        </MobilePaper>
    );
}
Example #9
Source File: AccountList.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
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 #10
Source File: GroupInvites.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
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 #11
Source File: GroupList.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
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: ArtifactEditor.tsx    From genshin-optimizer with MIT License 4 votes vote down vote up
export default function ArtifactEditor({ artifactIdToEdit = "", cancelEdit, allowUpload = false, allowEmpty = false, disableEditSetSlot: disableEditSlotProp = false }:
  { artifactIdToEdit?: string, cancelEdit: () => void, allowUpload?: boolean, allowEmpty?: boolean, disableEditSetSlot?: boolean }) {
  const { t } = useTranslation("artifact")

  const artifactSheets = usePromise(ArtifactSheet.getAll, [])

  const { database } = useContext(DatabaseContext)

  const [show, setShow] = useState(false)

  const [dirtyDatabase, setDirtyDatabase] = useForceUpdate()
  useEffect(() => database.followAnyArt(setDirtyDatabase), [database, setDirtyDatabase])

  const [editorArtifact, artifactDispatch] = useReducer(artifactReducer, undefined)
  const artifact = useMemo(() => editorArtifact && parseArtifact(editorArtifact), [editorArtifact])

  const [modalShow, setModalShow] = useState(false)

  const [{ processed, outstanding }, dispatchQueue] = useReducer(queueReducer, { processed: [], outstanding: [] })
  const firstProcessed = processed[0] as ProcessedEntry | undefined
  const firstOutstanding = outstanding[0] as OutstandingEntry | undefined

  const processingImageURL = usePromise(firstOutstanding?.imageURL, [firstOutstanding?.imageURL])
  const processingResult = usePromise(firstOutstanding?.result, [firstOutstanding?.result])

  const remaining = processed.length + outstanding.length

  const image = firstProcessed?.imageURL ?? processingImageURL
  const { artifact: artifactProcessed, texts } = firstProcessed ?? {}
  // const fileName = firstProcessed?.fileName ?? firstOutstanding?.fileName ?? "Click here to upload Artifact screenshot files"

  const disableEditSetSlot = disableEditSlotProp || !!artifact?.location

  useEffect(() => {
    if (!artifact && artifactProcessed)
      artifactDispatch({ type: "overwrite", artifact: artifactProcessed })
  }, [artifact, artifactProcessed, artifactDispatch])

  useEffect(() => {
    const numProcessing = Math.min(maxProcessedCount - processed.length, maxProcessingCount, outstanding.length)
    const processingCurrent = numProcessing && !outstanding[0].result
    outstanding.slice(0, numProcessing).forEach(processEntry)
    if (processingCurrent)
      dispatchQueue({ type: "processing" })
  }, [processed.length, outstanding])

  useEffect(() => {
    if (processingResult)
      dispatchQueue({ type: "processed", ...processingResult })
  }, [processingResult, dispatchQueue])

  const uploadFiles = useCallback((files: FileList) => {
    setShow(true)
    dispatchQueue({ type: "upload", files: [...files].map(file => ({ file, fileName: file.name })) })
  }, [dispatchQueue, setShow])
  const clearQueue = useCallback(() => dispatchQueue({ type: "clear" }), [dispatchQueue])

  useEffect(() => {
    const pasteFunc = (e: any) => uploadFiles(e.clipboardData.files)
    allowUpload && window.addEventListener('paste', pasteFunc);
    return () => {
      if (allowUpload) window.removeEventListener('paste', pasteFunc)
    }
  }, [uploadFiles, allowUpload])

  const onUpload = useCallback(
    e => {
      uploadFiles(e.target.files)
      e.target.value = null // reset the value so the same file can be uploaded again...
    },
    [uploadFiles],
  )

  const { old, oldType }: { old: ICachedArtifact | undefined, oldType: "edit" | "duplicate" | "upgrade" | "" } = useMemo(() => {
    const databaseArtifact = dirtyDatabase && artifactIdToEdit && database._getArt(artifactIdToEdit)
    if (databaseArtifact) return { old: databaseArtifact, oldType: "edit" }
    if (artifact === undefined) return { old: undefined, oldType: "" }
    const { duplicated, upgraded } = dirtyDatabase && database.findDuplicates(artifact)
    return { old: duplicated[0] ?? upgraded[0], oldType: duplicated.length !== 0 ? "duplicate" : "upgrade" }
  }, [artifact, artifactIdToEdit, database, dirtyDatabase])

  const { artifact: cachedArtifact, errors } = useMemo(() => {
    if (!artifact) return { artifact: undefined, errors: [] as Displayable[] }
    const validated = validateArtifact(artifact, artifactIdToEdit)
    if (old) {
      validated.artifact.location = old.location
      validated.artifact.exclude = old.exclude
    }
    return validated
  }, [artifact, artifactIdToEdit, old])

  // Overwriting using a different function from `databaseArtifact` because `useMemo` does not
  // guarantee to trigger *only when* dependencies change, which is necessary in this case.
  useEffect(() => {
    if (artifactIdToEdit === "new") {
      setShow(true)
      artifactDispatch({ type: "reset" })
    }
    const databaseArtifact = artifactIdToEdit && dirtyDatabase && database._getArt(artifactIdToEdit)
    if (databaseArtifact) {
      setShow(true)
      artifactDispatch({ type: "overwrite", artifact: deepClone(databaseArtifact) })
    }
  }, [artifactIdToEdit, database, dirtyDatabase])

  const sheet = artifact ? artifactSheets?.[artifact.setKey] : undefined
  const reset = useCallback(() => {
    cancelEdit?.();
    dispatchQueue({ type: "pop" })
    artifactDispatch({ type: "reset" })
  }, [cancelEdit, artifactDispatch])
  const update = useCallback((newValue: Partial<IArtifact>) => {
    const newSheet = newValue.setKey ? artifactSheets![newValue.setKey] : sheet!

    function pick<T>(value: T | undefined, available: readonly T[], prefer?: T): T {
      return (value && available.includes(value)) ? value : (prefer ?? available[0])
    }

    if (newValue.setKey) {
      newValue.rarity = pick(artifact?.rarity, newSheet.rarity, Math.max(...newSheet.rarity) as ArtifactRarity)
      newValue.slotKey = pick(artifact?.slotKey, newSheet.slots)
    }
    if (newValue.rarity)
      newValue.level = artifact?.level ?? 0
    if (newValue.level)
      newValue.level = clamp(newValue.level, 0, 4 * (newValue.rarity ?? artifact!.rarity))
    if (newValue.slotKey)
      newValue.mainStatKey = pick(artifact?.mainStatKey, Artifact.slotMainStats(newValue.slotKey))

    if (newValue.mainStatKey) {
      newValue.substats = [0, 1, 2, 3].map(i =>
        (artifact && artifact.substats[i].key !== newValue.mainStatKey) ? artifact!.substats[i] : { key: "", value: 0 })
    }
    artifactDispatch({ type: "update", artifact: newValue })
  }, [artifact, artifactSheets, sheet, artifactDispatch])
  const setSubstat = useCallback((index: number, substat: ISubstat) => {
    artifactDispatch({ type: "substat", index, substat })
  }, [artifactDispatch])
  const isValid = !errors.length
  const canClearArtifact = (): boolean => window.confirm(t`editor.clearPrompt` as string)
  const { rarity = 5, level = 0, slotKey = "flower" } = artifact ?? {}
  const { currentEfficiency = 0, maxEfficiency = 0 } = cachedArtifact ? Artifact.getArtifactEfficiency(cachedArtifact, allSubstatFilter) : {}
  const preventClosing = processed.length || outstanding.length
  const onClose = useCallback(
    (e) => {
      if (preventClosing) e.preventDefault()
      setShow(false)
      cancelEdit()
    }, [preventClosing, setShow, cancelEdit])

  const theme = useTheme();
  const grmd = useMediaQuery(theme.breakpoints.up('md'));

  const element = artifact ? allElementsWithPhy.find(ele => artifact.mainStatKey.includes(ele)) : undefined
  const color = artifact
    ? element ?? "success"
    : "primary"

  return <ModalWrapper open={show} onClose={onClose} >
    <Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: show ? "100%" : 64 }} />}><CardDark >
      <UploadExplainationModal modalShow={modalShow} hide={() => setModalShow(false)} />
      <CardHeader
        title={<Trans t={t} i18nKey="editor.title" >Artifact Editor</Trans>}
        action={<CloseButton disabled={!!preventClosing} onClick={onClose} />}
      />
      <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
        <Grid container spacing={1} columns={{ xs: 1, md: 2 }} >
          {/* Left column */}
          <Grid item xs={1} display="flex" flexDirection="column" gap={1}>
            {/* set & rarity */}
            <ButtonGroup sx={{ display: "flex", mb: 1 }}>
              {/* Artifact Set */}
              <ArtifactSetSingleAutocomplete
                size="small"
                disableClearable
                artSetKey={artifact?.setKey ?? ""}
                setArtSetKey={setKey => update({ setKey: setKey as ArtifactSetKey })}
                sx={{ flexGrow: 1 }}
                disabled={disableEditSetSlot}
              />
              {/* rarity dropdown */}
              <ArtifactRarityDropdown rarity={artifact ? rarity : undefined} onChange={r => update({ rarity: r })} filter={r => !!sheet?.rarity?.includes?.(r)} disabled={disableEditSetSlot || !sheet} />
            </ButtonGroup>

            {/* level */}
            <Box component="div" display="flex">
              <CustomNumberTextField id="filled-basic" label="Level" variant="filled" sx={{ flexShrink: 1, flexGrow: 1, mr: 1, my: 0 }} margin="dense" size="small"
                value={level} disabled={!sheet} placeholder={`0~${rarity * 4}`} onChange={l => update({ level: l })}
              />
              <ButtonGroup >
                <Button onClick={() => update({ level: level - 1 })} disabled={!sheet || level === 0}>-</Button>
                {rarity ? [...Array(rarity + 1).keys()].map(i => 4 * i).map(i => <Button key={i} onClick={() => update({ level: i })} disabled={!sheet || level === i}>{i}</Button>) : null}
                <Button onClick={() => update({ level: level + 1 })} disabled={!sheet || level === (rarity * 4)}>+</Button>
              </ButtonGroup>
            </Box>

            {/* slot */}
            <Box component="div" display="flex">
              <ArtifactSlotDropdown disabled={disableEditSetSlot || !sheet} slotKey={slotKey} onChange={slotKey => update({ slotKey })} />
              <CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
                <Suspense fallback={<Skeleton width="60%" />}>
                  <Typography color="text.secondary">
                    {sheet?.getSlotName(artifact!.slotKey) ? <span><ImgIcon src={sheet.slotIcons[artifact!.slotKey]} /> {sheet?.getSlotName(artifact!.slotKey)}</span> : t`editor.unknownPieceName`}
                  </Typography>
                </Suspense>
              </CardLight>
            </Box>

            {/* main stat */}
            <Box component="div" display="flex">
              <DropdownButton startIcon={element ? uncoloredEleIcons[element] : (artifact?.mainStatKey ? StatIcon[artifact.mainStatKey] : undefined)}
                title={<b>{artifact ? KeyMap.getArtStr(artifact.mainStatKey) : t`mainStat`}</b>} disabled={!sheet} color={color} >
                {Artifact.slotMainStats(slotKey).map(mainStatK =>
                  <MenuItem key={mainStatK} selected={artifact?.mainStatKey === mainStatK} disabled={artifact?.mainStatKey === mainStatK} onClick={() => update({ mainStatKey: mainStatK })} >
                    <ListItemIcon>{StatIcon[mainStatK]}</ListItemIcon>
                    <ListItemText>{KeyMap.getArtStr(mainStatK)}</ListItemText>
                  </MenuItem>)}
              </DropdownButton>
              <CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
                <Typography color="text.secondary">
                  {artifact ? `${cacheValueString(Artifact.mainStatValue(artifact.mainStatKey, rarity, level), KeyMap.unit(artifact.mainStatKey))}${KeyMap.unit(artifact.mainStatKey)}` : t`mainStat`}
                </Typography>
              </CardLight>
            </Box>

            {/* Current/Max Substats Efficiency */}
            <SubstatEfficiencyDisplayCard valid={isValid} efficiency={currentEfficiency} t={t} />
            {currentEfficiency !== maxEfficiency && <SubstatEfficiencyDisplayCard max valid={isValid} efficiency={maxEfficiency} t={t} />}

            {/* Image OCR */}
            {allowUpload && <CardLight>
              <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
                {/* TODO: artifactDispatch not overwrite */}
                <Suspense fallback={<Skeleton width="100%" height="100" />}>
                  <Grid container spacing={1} alignItems="center">
                    <Grid item flexGrow={1}>
                      <label htmlFor="contained-button-file">
                        <InputInvis accept="image/*" id="contained-button-file" multiple type="file" onChange={onUpload} />
                        <Button component="span" startIcon={<PhotoCamera />}>
                          Upload Screenshot (or Ctrl-V)
                        </Button>
                      </label>
                    </Grid>
                    <Grid item>
                      <Button color="info" sx={{ px: 2, minWidth: 0 }} onClick={() => setModalShow(true)}><Typography><FontAwesomeIcon icon={faQuestionCircle} /></Typography></Button>
                    </Grid>
                  </Grid>
                  {image && <Box display="flex" justifyContent="center">
                    <Box component="img" src={image} width="100%" maxWidth={350} height="auto" alt="Screenshot to parse for artifact values" />
                  </Box>}
                  {remaining > 0 && <CardDark sx={{ pl: 2 }} ><Grid container spacing={1} alignItems="center" >
                    {!firstProcessed && firstOutstanding && <Grid item>
                      <CircularProgress size="1em" />
                    </Grid>}
                    <Grid item flexGrow={1}>
                      <Typography>
                        <span>
                          Screenshots in file-queue: <b>{remaining}</b>
                          {/* {process.env.NODE_ENV === "development" && ` (Debug: Processed ${processed.length}/${maxProcessedCount}, Processing: ${outstanding.filter(entry => entry.result).length}/${maxProcessingCount}, Outstanding: ${outstanding.length})`} */}
                        </span>
                      </Typography>
                    </Grid>
                    <Grid item>
                      <Button size="small" color="error" onClick={clearQueue}>Clear file-queue</Button>
                    </Grid>
                  </Grid></CardDark>}
                </Suspense>
              </CardContent>
            </CardLight>}
          </Grid>

          {/* Right column */}
          <Grid item xs={1} display="flex" flexDirection="column" gap={1}>
            {/* substat selections */}
            {[0, 1, 2, 3].map((index) => <SubstatInput key={index} index={index} artifact={cachedArtifact} setSubstat={setSubstat} />)}
            {texts && <CardLight><CardContent>
              <div>{texts.slotKey}</div>
              <div>{texts.mainStatKey}</div>
              <div>{texts.mainStatVal}</div>
              <div>{texts.rarity}</div>
              <div>{texts.level}</div>
              <div>{texts.substats}</div>
              <div>{texts.setKey}</div>
            </CardContent></CardLight>}
          </Grid>
        </Grid>

        {/* Duplicate/Updated/Edit UI */}
        {old && <Grid container sx={{ justifyContent: "space-around" }} spacing={1} >
          <Grid item xs={12} md={5.5} lg={4} ><CardLight>
            <Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{oldType !== "edit" ? (oldType === "duplicate" ? t`editor.dupArt` : t`editor.upArt`) : t`editor.beforeEdit`}</Typography>
            <ArtifactCard artifactObj={old} />
          </CardLight></Grid>
          {grmd && <Grid item md={1} display="flex" alignItems="center" justifyContent="center" >
            <CardLight sx={{ display: "flex" }}><ChevronRight sx={{ fontSize: 40 }} /></CardLight>
          </Grid>}
          <Grid item xs={12} md={5.5} lg={4} ><CardLight>
            <Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{t`editor.preview`}</Typography>
            <ArtifactCard artifactObj={cachedArtifact} />
          </CardLight></Grid>
        </Grid>}

        {/* Error alert */}
        {!isValid && <Alert variant="filled" severity="error" >{errors.map((e, i) => <div key={i}>{e}</div>)}</Alert>}

        {/* Buttons */}
        <Grid container spacing={2}>
          <Grid item>
            {oldType === "edit" ?
              <Button startIcon={<Add />} onClick={() => {
                database.updateArt(editorArtifact!, old!.id);
                if (allowEmpty) reset()
                else {
                  setShow(false)
                  cancelEdit()
                }
              }} disabled={!editorArtifact || !isValid} color="primary">
                {t`editor.btnSave`}
              </Button> :
              <Button startIcon={<Add />} onClick={() => {
                database.createArt(artifact!);
                if (allowEmpty) reset()
                else {
                  setShow(false)
                  cancelEdit()
                }
              }} disabled={!artifact || !isValid} color={oldType === "duplicate" ? "warning" : "primary"}>
                {t`editor.btnAdd`}
              </Button>}
          </Grid>
          <Grid item flexGrow={1}>
            {allowEmpty && <Button startIcon={<Replay />} disabled={!artifact} onClick={() => { canClearArtifact() && reset() }} color="error">{t`editor.btnClear`}</Button>}
          </Grid>
          <Grid item>
            {process.env.NODE_ENV === "development" && <Button color="info" startIcon={<Shuffle />} onClick={async () => artifactDispatch({ type: "overwrite", artifact: await randomizeArtifact() })}>{t`editor.btnRandom`}</Button>}
          </Grid>
          {old && oldType !== "edit" && <Grid item>
            <Button startIcon={<Update />} onClick={() => { database.updateArt(editorArtifact!, old.id); allowEmpty ? reset() : setShow(false) }} disabled={!editorArtifact || !isValid} color="success">{t`editor.btnUpdate`}</Button>
          </Grid>}
        </Grid>
      </CardContent>
    </CardDark ></Suspense>
  </ModalWrapper>
}
Example #13
Source File: Scheduler.tsx    From NekoMaid with MIT License 4 votes vote down vote up
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 #14
Source File: Chat.tsx    From sapio-studio with Mozilla Public License 2.0 4 votes vote down vote up
function Channels() {
    const [channels, set_channels] = React.useState<{ channel_id: string }[]>(
        []
    );
    React.useEffect(() => {
        let cancel: ReturnType<typeof window.setTimeout>;
        async function f() {
            set_channels(await window.electron.chat.list_channels());
            cancel = setTimeout(f, 5000);
        }
        cancel = setTimeout(f, 0);
        return () => {
            clearTimeout(cancel);
        };
    }, []);
    const [channel, set_channel] = React.useState<string | null>(null);
    const [add_new_channel, set_add_new_channel] =
        React.useState<boolean>(false);
    const ChannelColumns: GridColumns = [
        {
            field: 'actions-load',
            type: 'actions',
            flex: 0.2,
            getActions: (params) => [
                <GridActionsCellItem
                    key="open-folder"
                    icon={<VisibilityIcon />}
                    label="Open"
                    onClick={() => {
                        typeof params.id === 'string' && set_channel(params.id);
                    }}
                />,
            ],
        },
        {
            field: 'channel_id',
            headerName: 'Channel',
            minWidth: 100,
            type: 'text',
            flex: 1,
        },
    ];
    function CustomToolbar() {
        return (
            <GridToolbarContainer>
                <Button onClick={() => set_add_new_channel(true)}>
                    Create New Channel<Add></Add>
                </Button>
            </GridToolbarContainer>
        );
    }
    return (
        <div>
            <NewChannel
                show={add_new_channel}
                hide={() => set_add_new_channel(false)}
            />
            {channel === null && (
                <DataGrid
                    components={{ Toolbar: CustomToolbar }}
                    rows={channels.map((v) => {
                        return { id: v.channel_id, ...v };
                    })}
                    columns={ChannelColumns}
                    disableExtendRowFullWidth={false}
                    columnBuffer={3}
                    pageSize={10}
                    rowsPerPageOptions={[5]}
                    disableColumnSelector
                    disableSelectionOnClick
                />
            )}
            {channel !== null && (
                <Channel
                    channel_id={channel}
                    close={() => {
                        set_channel(null);
                    }}
                ></Channel>
            )}
        </div>
    );
}
Example #15
Source File: Workspaces.tsx    From sapio-studio with Mozilla Public License 2.0 4 votes vote down vote up
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 #16
Source File: websiteCardNew.tsx    From Search-Next with GNU General Public License v3.0 4 votes vote down vote up
WebsiteCardNew: React.FC<WebsiteCardNewProps> = (props) => {
  const { datasource } = props;
  const { name, intro, color, url } = datasource;

  const onAdd = () => {
    const res = addSite({
      name,
      url: url.substring(0, url.lastIndexOf('/')),
    });
    if (res) toast.success('添加成功');
  };

  const onCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(url);
      toast.success(`已复制 ${name} (${url})`);
    } else {
      const copy = new Clipboard(`.copy-button_${name}`);
      copy.on('success', (e) => {
        toast.success(`已复制 ${name} (${url})`);
      });
      copy.on('error', function (e) {
        toast.warning(
          `您的浏览器不支持复制功能,请点击跳转到该网站手动复制地址`,
        );
      });
    }
  };

  const onMore = () => {
    toast.warning('功能开发中...');
  };

  return (
    <div
      className={classNames(
        'cursor-pointer shadow-md rounded border-b-2',
        css`
          --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
            0 1px 3px 0 ${hexToRgba(color ?? '#000', 0.45).rgba} !important;
          border-bottom-color: ${color};
        `,
      )}
    >
      <CardActionArea>
        <Tooltip title={intro || '暂无介绍'}>
          <div className="p-3 flex gap-3" onClick={() => window.open(url)}>
            <Avatar
              // style={{ backgroundColor: color }}
              src={getWebIconByUrl(url)}
            >
              {name.split('')[0].toUpperCase()}
            </Avatar>
            <div className="flex-grow overflow-hidden">
              <p className="font-bold text-base whitespace-nowrap overflow-x-hidden">
                {name}
              </p>
              <Overflow>{(intro as any) || ('暂无介绍' as any)}</Overflow>
            </div>
          </div>
        </Tooltip>
      </CardActionArea>
      <div>
        <ButtonGroup
          disableElevation
          variant="text"
          size="small"
          className={classNames(
            'w-full h-full flex',
            css`
              justify-content: flex-end;
              button {
                height: 100%;
                border-right: 0px !important;
              }
            `,
          )}
        >
          <Tooltip title="添加到首页">
            <Button onClick={onAdd}>
              <Add />
            </Button>
          </Tooltip>
          <Tooltip title="复制网站链接">
            <Button
              className={`copy-button_${name}`}
              data-clipboard-text={url}
              onClick={onCopy}
            >
              <CopyAll />
            </Button>
          </Tooltip>
          {false && (
            <Tooltip title="更多">
              <Button onClick={onMore}>
                <MoreHoriz />
              </Button>
            </Tooltip>
          )}
        </ButtonGroup>
      </div>
    </div>
  );
}