@mui/material#Tabs TypeScript Examples

The following examples show how to use @mui/material#Tabs. 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: Wallet.tsx    From sapio-studio with Mozilla Public License 2.0 6 votes vote down vote up
export function Wallet(props: { bitcoin_node_manager: BitcoinNodeManager }) {
    const dispatch = useDispatch();
    const idx = useSelector(selectWalletTab);
    const handleChange = (_: any, idx: TabIndexes) => {
        dispatch(switch_wallet_tab(idx));
    };
    return (
        <div className="Wallet">
            <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
                <Tabs
                    value={idx}
                    onChange={handleChange}
                    aria-label="basic tabs example"
                >
                    <Tab label="Send"></Tab>
                    <Tab label="Send History"></Tab>
                    <Tab label="Workspaces"></Tab>
                    <Tab label="Contracts"></Tab>
                </Tabs>
            </Box>
            <Box sx={{ overflowY: 'scroll', height: '100%' }}>
                <WalletSend value={0} idx={idx} {...props}></WalletSend>
                <WalletHistory value={1} idx={idx} {...props}></WalletHistory>
                <Workspaces value={2} idx={idx}></Workspaces>
                <ContractList value={3} idx={idx}></ContractList>
            </Box>
        </div>
    );
}
Example #2
Source File: LibraryOptions.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
function Options() {
    const [currentTab, setCurrentTab] = useState<number>(0);

    return (
        <Box>
            <Tabs
                key={currentTab}
                value={currentTab}
                variant="fullWidth"
                onChange={(e, newTab) => setCurrentTab(newTab)}
                indicatorColor="primary"
                textColor="primary"
            >
                <Tab label="Filter" value={0} />
                <Tab label="Sort" value={1} />
                <Tab label="Display" value={2} />
            </Tabs>
            {filtersTab(currentTab)}
            {sortsTab(currentTab)}
            {dispalyTab(currentTab)}
        </Box>
    );
}
Example #3
Source File: index.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
function TabNav({ tab }: { tab: string }) {
  const { t } = useTranslation("page_character")
  return <Tabs
    value={tab}
    variant="scrollable"
    allowScrollButtonsMobile
    sx={{
      "& .MuiTab-root:hover": {
        transition: "background-color 0.25s ease",
        backgroundColor: "rgba(255,255,255,0.1)"
      },
    }}
  >
    <Tab sx={{ minWidth: "20%" }} value="overview" label={t("tabs.overview")} icon={<Person />} component={RouterLink} to="" />
    <Tab sx={{ minWidth: "20%" }} value="talent" label={t("tabs.talent")} icon={<FactCheck />} component={RouterLink} to="talent" />
    <Tab sx={{ minWidth: "20%" }} value="equip" label={t("tabs.equip")} icon={<Checkroom />} component={RouterLink} to="equip" />
    <Tab sx={{ minWidth: "20%" }} value="teambuffs" label={t("tabs.teambuffs")} icon={<Groups />} component={RouterLink} to="teambuffs" />
    <Tab sx={{ minWidth: "20%" }} value="optimize" label={t("tabs.optimize")} icon={<Calculate />} component={RouterLink} to="optimize" />
  </Tabs>
}
Example #4
Source File: DepositWithdrawDialog.tsx    From wrap.scrt.network with MIT License 5 votes vote down vote up
export default function DepositWithdrawDialog({
  token,
  secretjs,
  secretAddress,
  balances,
  isOpen,
  setIsOpen,
}: {
  token: Token;
  secretjs: SecretNetworkClient | null;
  secretAddress: string;
  balances: Map<string, string>;
  isOpen: boolean;
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
  const [selectedTab, setSelectedTab] = useState<string>("deposit");
  const closeDialog = () => {
    setIsOpen(false);
    setSelectedTab("deposit");
  };

  return (
    <Dialog open={isOpen} fullWidth={true} onClose={closeDialog}>
      <TabContext value={selectedTab}>
        <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
          <Tabs
            value={selectedTab}
            variant="fullWidth"
            onChange={(_event: React.SyntheticEvent, newSelectedTab: string) =>
              setSelectedTab(newSelectedTab)
            }
          >
            <Tab label="IBC Deposit" value={"deposit"} />
            <Tab label="IBC Withdraw" value={"withdraw"} />
          </Tabs>
        </Box>
        <TabPanel value={"deposit"}>
          <Deposit
            token={token}
            secretAddress={secretAddress}
            onSuccess={(txhash) => {
              closeDialog();
              console.log("success", txhash);
            }}
            onFailure={(error) => console.error(error)}
          />
        </TabPanel>
        <TabPanel value={"withdraw"}>
          <Withdraw
            token={token}
            secretjs={secretjs}
            secretAddress={secretAddress}
            balances={balances}
            onSuccess={(txhash) => {
              closeDialog();
              console.log("success", txhash);
            }}
            onFailure={(error) => console.error(error)}
          />
        </TabPanel>
      </TabContext>
    </Dialog>
  );
}
Example #5
Source File: Profiler.tsx    From NekoMaid with MIT License 5 votes vote down vote up
Profiler: React.FC = () => {
  const plugin = usePlugin()
  const theme = useTheme()
  const globalData = useGlobalData()
  const [tab, setTab] = useState(0)
  const [key, setTKey] = useState(0)
  const [status, setStatus] = useState(!!globalData.profilerStarted)
  useEffect(() => {
    const off = plugin.on('profiler:status', setStatus)
    return () => { off() }
  }, [])

  const transitionDuration = {
    enter: theme.transitions.duration.enteringScreen,
    exit: theme.transitions.duration.leavingScreen
  }
  const Elm = components[tab]

  const createFab = (onClick: any, children: any, show: boolean) => <Zoom
    in={show}
    timeout={transitionDuration}
    style={{ transitionDelay: (show ? transitionDuration.exit : 0) + 'ms' }}
    unmountOnExit
  >
    <Fab
      color='primary'
      sx={{ position: 'fixed', bottom: { xs: 16, sm: 40 }, right: { xs: 16, sm: 40 }, zIndex: 3 }}
      onClick={onClick}
    >{children}</Fab>
  </Zoom>

  return <Box sx={{ minHeight: status || !tab ? '100%' : undefined }}>
    <Toolbar />
    <Paper square variant='outlined' sx={{ margin: '0 -1px', position: 'fixed', width: 'calc(100% + 1px)', zIndex: 3 }}>
      <Tabs value={tab} onChange={(_, it) => setTab(it)} variant='scrollable' scrollButtons='auto'>
        <Tab label={lang.profiler.summary} />
        <Tab label='Timings' disabled={!globalData.hasTimings} />
        <Tab label={lang.profiler.plugins} />
        <Tab label={lang.profiler.carbageCollection} />
        <Tab label={lang.profiler.entities} />
        <Tab label={lang.profiler.heap} />
        <Tab label={lang.profiler.threads} />
      </Tabs>
    </Paper>
    <Tabs />
    {tab < 4 && !status ? <Box sx={{ textAlign: 'center', marginTop: '40vh' }}>{lang.profiler.notStarted}</Box> : Elm && <Elm key={key} />}
    {createFab(() => plugin.emit('profiler:status', !status), status ? <Stop /> : <PlayArrow />, tab < 4)}
    {createFab(() => setTKey(key + 1), <Refresh />, tab >= 4)}
  </Box>
}
Example #6
Source File: index.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
export default function PageDocumentation() {
  // const { t } = useTranslation("documentation")
  ReactGA.send({ hitType: "pageview", page: '/doc' })

  const { params: { currentTab } } = useMatch("/doc/:currentTab") ?? { params: { currentTab: "" } }

  return <CardDark sx={{ my: 1 }}>
    <Grid container sx={{ px: 2, py: 1 }}>
      <Grid item flexGrow={1}>
        <Typography variant="h6">
          Documentation
        </Typography>
      </Grid>
      <Grid item>
        <Typography variant="h6">
          <SqBadge color="info">Version. 2</SqBadge>
        </Typography>
      </Grid>
    </Grid>
    <Divider />
    <CardContent>
      <Grid container spacing={1}>
        <Grid item xs={12} md={2}>
          <CardLight sx={{ height: "100%" }}>
            <Tabs
              orientation="vertical"
              value={currentTab}
              aria-label="Documentation Navigation"
              sx={{ borderRight: 1, borderColor: 'divider' }}
            >
              <Tab label="Overview" value="" component={Link} to="" />
              <Tab label={"Key naming convention"} value="KeyNaming" component={Link} to="KeyNaming" />
              <Tab label={<code>StatKey</code>} value="StatKey" component={Link} to="StatKey" />
              <Tab label={<code>ArtifactSetKey</code>} value="ArtifactSetKey" component={Link} to="ArtifactSetKey" />
              <Tab label={<code>CharacterKey</code>} value="CharacterKey" component={Link} to="CharacterKey" />
              <Tab label={<code>WeaponKey</code>} value="WeaponKey" component={Link} to="WeaponKey" />
              <Tab label={<code>MaterialKey</code>} value="MaterialKey" component={Link} to="MaterialKey" />
              <Tab label={"Version History"} value="VersionHistory" component={Link} to="VersionHistory" />
            </Tabs>
          </CardLight>
        </Grid>
        <Grid item xs={12} md={10}>
          <CardLight sx={{ height: "100%" }}>
            <CardContent>
              <Suspense fallback={<Skeleton variant="rectangular" width="100%" height={600} />}>
                <Routes>
                  <Route index element={<Overview />} />
                  <Route path="/VersionHistory" element={<VersionHistoryPane />} />
                  <Route path="/MaterialKey" element={<MaterialKeyPane />} />
                  <Route path="/ArtifactSetKey" element={<ArtifactSetKeyPane />} />
                  <Route path="/WeaponKey" element={<WeaponKeyPane />} />
                  <Route path="/CharacterKey" element={<CharacterKeyPane />} />
                  <Route path="/StatKey" element={<StatKeyPane />} />
                  <Route path="/KeyNaming" element={<KeyNamingPane />} />
                </Routes>
              </Suspense>
            </CardContent>
          </CardLight>
        </Grid>
      </Grid>
    </CardContent>
  </CardDark>
}
Example #7
Source File: BaseSettingsModal.tsx    From rewind with MIT License 5 votes vote down vote up
export function BaseSettingsModal(props: SettingsProps) {
  const { onClose, tabs, opacity, onOpacityChange, tabIndex, onTabIndexChange } = props;

  const handleTabChange = (event: any, newValue: any) => {
    onTabIndexChange(newValue);
  };

  const displayedTab = tabs[tabIndex].component;

  return (
    <Paper
      sx={{
        filter: `opacity(${opacity}%)`,
        height: "100%",
        display: "flex",
        flexDirection: "column",
        position: "relative",
      }}
      elevation={2}
    >
      <Stack sx={{ py: 1, px: 2, alignItems: "center" }} direction={"row"} gap={1}>
        <SettingsIcon />
        <Typography fontWeight={"bolder"}>Settings</Typography>
        <Box flexGrow={1} />
        <IconButton onClick={onClose}>
          <Close />
        </IconButton>
      </Stack>
      <Divider />
      <Stack direction={"row"} sx={{ flexGrow: 1, overflow: "auto" }}>
        {/*TODO: Holy moly, the CSS here needs to be changed a bit*/}
        <Tabs
          orientation="vertical"
          variant="scrollable"
          value={tabIndex}
          onChange={handleTabChange}
          sx={{ borderRight: 1, borderColor: "divider", position: "absolute" }}
        >
          {tabs.map(({ label }, index) => (
            <Tab label={label} key={index} tabIndex={index} sx={{ textTransform: "none" }} />
          ))}
        </Tabs>
        <Box sx={{ marginLeft: "90px" }}>{displayedTab}</Box>
      </Stack>
      <Divider />
      <Stack sx={{ px: 2, py: 1, flexDirection: "row", alignItems: "center" }}>
        <PromotionFooter />
        <Box flexGrow={1} />
        <Stack direction={"row"} alignItems={"center"} gap={2}>
          <IconButton onClick={() => onOpacityChange(MIN_OPACITY)}>
            <VisibilityOff />
          </IconButton>
          <Slider
            value={opacity}
            onChange={(_, v) => onOpacityChange(v as number)}
            step={5}
            min={MIN_OPACITY}
            max={MAX_OPACITY}
            valueLabelFormat={(value: number) => `${value}%`}
            sx={{ width: "12em" }}
            valueLabelDisplay={"auto"}
          />
          <IconButton onClick={() => onOpacityChange(MAX_OPACITY)}>
            <Visibility />
          </IconButton>
        </Stack>
      </Stack>
    </Paper>
  );
}
Example #8
Source File: Header.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
function HeaderContent({ anchor }) {
  const theme = useTheme();
  const isLarge = useMediaQuery(theme.breakpoints.up('lg'));
  const isMobile = useMediaQuery(theme.breakpoints.down('md'));

  const { t } = useTranslation("ui")

  const { params: { currentTab } } = useMatch({ path: "/:currentTab", end: false }) ?? { params: { currentTab: "" } };
  if (isMobile) return <MobileHeader anchor={anchor} currentTab={currentTab} />
  return <AppBar position="static" sx={{ bgcolor: "#343a40", display: "flex", flexWrap: "nowrap" }} elevation={0} id={anchor} >
    <Tabs
      value={currentTab}
      variant="scrollable"
      scrollButtons="auto"

      sx={{
        "& .MuiTab-root": {
          px: 1,
          flexDirection: "row",
          minWidth: 40,
          minHeight: "auto",
        },
        "& .MuiTab-root:hover": {
          transition: "background-color 0.5s ease",
          backgroundColor: "rgba(255,255,255,0.1)"
        },
        "& .MuiTab-root > .MuiTab-iconWrapper": {
          mb: 0,
          mr: 0.5
        },
      }}
    >
      <Tab value="" component={RouterLink} to="/" label={<Typography variant="h6" sx={{ px: 1 }}>
        <Trans t={t} i18nKey="pageTitle">Genshin Optimizer</Trans>
      </Typography>} />
      {content.map(({ i18Key, value, to, svg }) => <Tab key={value} value={value} component={RouterLink} to={to} icon={<FontAwesomeIcon icon={svg} />} label={t(i18Key)} />)}
      <Box flexGrow={1} />
      {links.map(({ i18Key, href, label, svg }) => <Tab key={label} component="a" href={href} target="_blank" icon={<FontAwesomeIcon icon={svg} />} onClick={e => ReactGA.outboundLink({ label }, () => { })} label={isLarge && t(i18Key)} />)}
    </Tabs>
  </AppBar>
}
Example #9
Source File: TemplateDetailPage.tsx    From fluttertemplates.dev with MIT License 4 votes vote down vote up
function RenderBody(props: TemplateCardProps) {
  const router = useRouter();

  const [selectedTab, setSelectedTab] = useState(0);
  const handleTabChange = (event: any, newValue: any) => {
    setSelectedTab(newValue);
  };

  const _frontmatter = props.frontmatter;
  const _hasMdContent = props.content.toString().length != 0;

  function renderTabs(selectedTab: number) {
    if (_hasMdContent && selectedTab == 0) {
      return (
        <div
          style={{
            width: "100%",
            height: "80%",
            overflowX: "hidden",
            overflowY: "auto",
          }}
        >
          <ReactMarkdown
            children={props.content}
            remarkPlugins={[remarkGfm]}
            rehypePlugins={[rehypeRaw]}
            linkTarget="_blank"
            components={{
              img: ({ node, ...props }) => (
                <img {...props} width="100%" height="100%" />
              ),

              tr: ({ node, ...props }) => (
                <Grid container spacing={1}>
                  {props.children}
                </Grid>
              ),
              td: ({ node, ...props }) => (
                <Grid item xs={4}>
                  {props.children}
                </Grid>
              ),
            }}
          />
        </div>
      );
    } else if (
      (!_hasMdContent && selectedTab == 0) ||
      (_hasMdContent && selectedTab == 1)
    ) {
      return (
        <Code
          codeGistUrl={_frontmatter.codeGistUrl}
          fullCodeUrl={_frontmatter.codeUrl}
        />
      );
    } else if (
      (!_hasMdContent && selectedTab == 1) ||
      (_hasMdContent && selectedTab == 2)
    ) {
      return <PackagesUsed packages={_frontmatter.packages ?? []} />;
    } else {
      return <PageNotFoundPage />;
    }
  }

  return (
    <Grid
      container
      style={{
        marginTop: "2rem",
      }}
      spacing={2}
      justifyContent="center"
    >
      {!(_frontmatter.isProtected ?? false) && (
        <Grid
          item
          md={6}
          style={{
            height: "90vh",
            width: "100%",
          }}
        >
          <Card elevation={0}>
            <Typography
              component="h5"
              variant="h5"
              style={{
                fontWeight: "bold",
                marginLeft: "1rem",
                marginTop: "1rem",
              }}
            >
              {_frontmatter.title}
            </Typography>
            {_frontmatter.categories && _frontmatter.categories.length > 0 && (
              <div
                style={{
                  marginLeft: "1rem",
                  marginBottom: "-10px",
                }}
              >
                <CategoriesList
                  categories={_frontmatter.categories}
                  selected={""}
                  showAll={false}
                />
              </div>
            )}

            <Tabs
              value={selectedTab}
              onChange={handleTabChange}
              indicatorColor="secondary"
              textColor="inherit"
              centered
            >
              {_hasMdContent && (
                <Tab
                  label="About"
                  icon={
                    <InfoOutlined
                      style={{
                        marginTop: "8px",
                        marginRight: "8px",
                      }}
                    />
                  }
                />
              )}
              <Tab
                label="Code"
                icon={
                  <CodeOutlined
                    style={{
                      marginTop: "8px",
                      marginRight: "8px",
                    }}
                  />
                }
              />
              <Tab
                label="Packages Used"
                icon={
                  <AttachmentOutlined
                    style={{
                      marginTop: "8px",
                      marginRight: "8px",
                    }}
                  />
                }
              />
            </Tabs>
          </Card>
          {renderTabs(selectedTab)}
        </Grid>
      )}
      <Grid
        item
        md={6}
        style={{
          display: "flex",
          justifyContent: "center",
        }}
      >
        <div
          style={{
            height: "80vh",
            width: "calc(80vh/17 * 9)",
            margin: "8px",
          }}
        >
          <CustomIframe
            url={_frontmatter.demoUrl}
            showLoadingIndicator={true}
            style={{
              borderRadius: "16px",
              border: "4px solid grey",
            }}
          />
        </div>
      </Grid>
    </Grid>
  );
}
Example #10
Source File: Settings.tsx    From sapio-studio with Mozilla Public License 2.0 4 votes vote down vote up
export function SettingsInner() {
    const [idx, set_idx] = React.useState<number>(0);
    const [dialog_node, set_dialog_node] = React.useState<
        [string | null, string[]]
    >([null, []]);
    const handleChange = (_: any, idx: number) => {
        set_idx(idx);
    };

    const test_bitcoind = async () => {
        window.electron
            .bitcoin_command([{ method: 'getbestblockhash', parameters: [] }])
            .then((h) =>
                set_dialog_node(['Connection Seems OK:', [`Best Hash ${h[0]}`]])
            )
            .catch((e) => {
                console.log('GOT', JSON.stringify(e));
                const r = e.message;
                if (typeof e.message === 'string') {
                    const err = JSON.parse(r);
                    if (
                        err instanceof Object &&
                        'code' in err &&
                        'name' in err &&
                        'message' in err
                    ) {
                        set_dialog_node([
                            '¡Connection Not Working!',
                            [
                                `Name: ${err.name}`,
                                `Message: ${err.message}`,
                                `Error Code: ${err.code}`,
                            ],
                        ]);
                        return;
                    } else if (typeof err === 'string') {
                        set_dialog_node([
                            '¡Connection Not Working!',
                            [`${err}`],
                        ]);
                        return;
                    }
                }
                set_dialog_node(['¡Unknown Error!', [`${r.toString()}`]]);
            });
    };

    const test_sapio = async () => {
        window.electron.sapio
            .show_config()
            .then((conf) => {
                if ('ok' in conf)
                    set_dialog_node([
                        'Sapio-CLI is Working!\nUsing Configuration:',
                        [`${conf.ok}`],
                    ]);
                else
                    set_dialog_node(['¡Configuration Error!', [`${conf.err}`]]);
            })
            .catch((e) =>
                set_dialog_node(['¡Configuration Error!', [`${e.toString()}`]])
            );
    };
    const check_emulator = async () => {
        window.electron.emulator.read_log().then((log) => {
            if (log.length) {
                const json = JSON.parse(log);
                set_dialog_node([
                    'Emulator Status:',
                    [
                        `interface: ${json.interface}`,
                        `pk: ${json.pk}`,
                        `sync: ${json.sync}`,
                    ],
                ]);
            } else {
                set_dialog_node(['Emulator Status:', ['Not Running']]);
            }
        });
    };

    return (
        <div className="Settings">
            <Box className="SettingsNav">
                <Tabs
                    orientation="vertical"
                    value={idx}
                    onChange={handleChange}
                    aria-label="basic tabs example"
                >
                    <Tab label="Guide"></Tab>
                    <Tab label="Sapio CLI"></Tab>
                    <Tab label="Bitcoin"></Tab>
                    <Tab label="Emulator"></Tab>
                    <Tab label="Display"></Tab>
                </Tabs>
            </Box>
            <Box className="SettingsPanes">
                <Dialog
                    open={
                        Boolean(dialog_node[0]) ||
                        Boolean(dialog_node[1].length)
                    }
                    onClose={() => set_dialog_node([null, []])}
                    aria-labelledby="alert-dialog-title"
                    aria-describedby="alert-dialog-description"
                >
                    <DialogTitle id="alert-dialog-title">
                        {dialog_node[0]}
                    </DialogTitle>
                    <DialogContent>
                        <div id="alert-dialog-description">
                            {dialog_node[1].map((txt) => (
                                <DialogContentText key={txt}>
                                    {txt}
                                </DialogContentText>
                            ))}
                        </div>
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={() => set_dialog_node([null, []])}>
                            Close
                        </Button>
                    </DialogActions>
                </Dialog>
                <Guide idx={idx} my_idx={0} />
                <SettingPane name={'sapio_cli'} value={idx} idx={1}>
                    <Button
                        onClick={test_sapio}
                        variant="contained"
                        color="info"
                        size="large"
                    >
                        Test Sapio-Cli
                    </Button>
                </SettingPane>
                <SettingPane name={'bitcoin'} value={idx} idx={2}>
                    <Button
                        onClick={test_bitcoind}
                        variant="contained"
                        color="info"
                        size="large"
                    >
                        Test Connection
                    </Button>
                </SettingPane>
                <SettingPane name={'local_oracle'} value={idx} idx={3}>
                    <Button
                        variant="contained"
                        color="success"
                        size="large"
                        onClick={window.electron.emulator.start}
                    >
                        Start
                    </Button>
                    <Button
                        sx={{ marginLeft: '20px' }}
                        variant="contained"
                        color="error"
                        size="large"
                        onClick={window.electron.emulator.kill}
                    >
                        Kill
                    </Button>
                    <Button
                        sx={{ marginLeft: '20px' }}
                        variant="contained"
                        color="info"
                        size="large"
                        onClick={check_emulator}
                    >
                        Check Status
                    </Button>
                </SettingPane>
                <SettingPane name={'display'} value={idx} idx={4} />
            </Box>
        </div>
    );
}
Example #11
Source File: ItemViewer.tsx    From NekoMaid with MIT License 4 votes vote down vote up
ItemEditor: React.FC = () => {
  const plugin = usePlugin()
  const theme = useTheme()
  const [item, setItem] = useState<Item | undefined>()
  const [types, setTypes] = useState<string[]>([])
  const [tab, setTab] = useState(0)
  const [level, setLevel] = useState(1)
  const [enchantment, setEnchantment] = useState<string | undefined>()
  const [nbtText, setNBTText] = useState('')
  const nbt: NBT = item?.nbt ? parse(item.nbt) : { id: 'minecraft:' + (item?.type || 'air').toLowerCase(), Count: new Byte(1) } as any
  useEffect(() => {
    if (!item || types.length) return
    plugin.emit('item:fetch', (a: string[], b: string[]) => {
      setTypes(a)
      enchantments = b
    })
  }, [item])
  useEffect(() => {
    _setItem = (it: any) => {
      setItem(it)
      setNBTText(it.nbt ? stringify(parse(it.nbt), { pretty: true }) : '')
    }
    return () => { _setItem = null }
  }, [])
  const cancel = () => {
    setItem(undefined)
    if (_resolve) {
      _resolve(false)
      _resolve = null
    }
  }
  const update = () => {
    const newItem: any = { ...item }
    if (nbt) {
      newItem.nbt = stringify(nbt as any)
      setNBTText(stringify(nbt, { pretty: true }))
    }
    setItem(newItem)
  }
  const isAir = item?.type === 'AIR'
  const name = nbt?.tag?.display?.Name
  const enchantmentMap: Record<string, true> = { }
  return <Dialog open={!!item} onClose={cancel}>
    <DialogTitle>{lang.itemEditor.title}</DialogTitle>
    <DialogContent sx={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
      {item && <Box sx={{ display: 'flex', width: '100%', justifyContent: 'center' }}>
        <ItemViewer item={item} />
        <Autocomplete
          options={types}
          sx={{ maxWidth: 300, marginLeft: 1, flexGrow: 1 }}
          value={item?.type}
          onChange={(_, it) => {
            item.type = it || 'AIR'
            if (nbt) nbt.id = 'minecraft:' + (it ? it.toLowerCase() : 'air')
            update()
          }}
          getOptionLabel={it => {
            const locatedName = getName(it.toLowerCase())
            return (locatedName ? locatedName + ' ' : '') + it
          }}
          renderInput={(params) => <TextField {...params} label={lang.itemEditor.itemType} size='small' variant='standard' />}
        />
      </Box>}
      <Tabs centered value={tab} onChange={(_, it) => setTab(it)} sx={{ marginBottom: 2 }}>
        <Tab label={lang.itemEditor.baseAttribute} disabled={isAir} />
        <Tab label={minecraft['container.enchant']} disabled={isAir} />
        <Tab label='NBT' disabled={isAir} />
      </Tabs>
      {nbt && tab === 0 && <Grid container spacing={1} rowSpacing={1}>
        <Grid item xs={12} md={6}><TextField
          fullWidth
          label={lang.itemEditor.count}
          type='number'
          variant='standard'
          value={nbt.Count}
          disabled={isAir}
          onChange={e => {
            nbt.Count = new Byte(item!.amount = parseInt(e.target.value))
            update()
          }}
        /></Grid>
        <Grid item xs={12} md={6}><TextField
          fullWidth
          label={lang.itemEditor.damage}
          type='number'
          variant='standard'
          value={nbt.tag?.Damage}
          disabled={isAir}
          onChange={e => {
            set(nbt, 'tag.Damage', parseInt(e.target.value))
            update()
          }}
        /></Grid>
        <Grid item xs={12} md={6}>
          <TextField
            fullWidth
            label={lang.itemEditor.displayName}
            variant='standard'
            disabled={isAir}
            value={name ? stringifyTextComponent(JSON.parse(name)) : ''}
            onChange={e => {
              set(nbt, 'tag.display.Name', JSON.stringify(item!.name = e.target.value))
              update()
            }}
          />
          <FormControlLabel
            label={minecraft['item.unbreakable']}
            disabled={isAir}
            checked={nbt.tag?.Unbreakable?.value === 1}
            control={<Checkbox
              checked={nbt.tag?.Unbreakable?.value === 1}
              onChange={e => {
                set(nbt, 'tag.Unbreakable', new Byte(+e.target.checked))
                update()
              }} />
            }
          />
        </Grid>
        <Grid item xs={12} md={6}><TextField
          fullWidth
          multiline
          label={lang.itemEditor.lore}
          variant='standard'
          maxRows={5}
          disabled={isAir}
          value={nbt.tag?.display?.Lore?.map(l => stringifyTextComponent(JSON.parse(l)))?.join('\n') || ''}
          onChange={e => {
            set(nbt, 'tag.display.Lore', e.target.value.split('\n').map(text => JSON.stringify(text)))
            update()
          }}
        /></Grid>
      </Grid>}
      {nbt && tab === 1 && <Grid container spacing={1} sx={{ width: '100%' }}>
        {nbt.tag?.Enchantments?.map((it, i) => {
          enchantmentMap[it.id] = true
          return <Grid item key={i}><Chip label={getEnchantmentName(it)} onDelete={() => {
            nbt?.tag?.Enchantments?.splice(i, 1)
            update()
          }} /></Grid>
        })}
        <Grid item><Chip label={lang.itemEditor.newEnchantment} color='primary' onClick={() => {
          setEnchantment('')
          setLevel(1)
        }} /></Grid>
        <Dialog onClose={() => setEnchantment(undefined)} open={enchantment != null}>
          <DialogTitle>{lang.itemEditor.newEnchantmentTitle}</DialogTitle>
          <DialogContent>
            <Box component='form' sx={{ display: 'flex', flexWrap: 'wrap' }}>
              <FormControl variant='standard' sx={{ m: 1, minWidth: 120 }}>
                <InputLabel htmlFor='item-editor-enchantment-selector'>{minecraft['container.enchant']}</InputLabel>
                <Select
                  id='item-editor-enchantment-selector'
                  label={minecraft['container.enchant']}
                  value={enchantment || ''}
                  onChange={e => setEnchantment(e.target.value)}
                >{enchantments
                  .filter(e => !(e in enchantmentMap))
                  .map(it => <MenuItem key={it} value={it}>{getEnchantmentName(it)}</MenuItem>)}
                </Select>
              </FormControl>
              <FormControl sx={{ m: 1, minWidth: 120 }}>
                <TextField
                  label={lang.itemEditor.level}
                  type='number'
                  variant='standard'
                  value={level}
                  onChange={e => setLevel(parseInt(e.target.value))}
                />
              </FormControl>
            </Box>
          </DialogContent>
          <DialogActions>
            <Button onClick={() => setEnchantment(undefined)}>{minecraft['gui.cancel']}</Button>
            <Button disabled={!enchantment || isNaN(level)} onClick={() => {
              if (nbt) {
                if (!nbt.tag) nbt.tag = { Damage: new Int(0) }
                ;(nbt.tag.Enchantments || (nbt.tag.Enchantments = [])).push({ id: enchantment!, lvl: new Short(level) })
              }
              setEnchantment(undefined)
              update()
            }}>{minecraft['gui.ok']}</Button>
          </DialogActions>
        </Dialog>
      </Grid>}
    </DialogContent>
    {nbt && tab === 2 && <Box sx={{
      '& .CodeMirror': { width: '100%' },
      '& .CodeMirror-dialog, .CodeMirror-scrollbar-filler': { backgroundColor: theme.palette.background.paper + '!important' }
    }}>
      <UnControlled
        value={nbtText}
        options={{
          mode: 'javascript',
          phrases: lang.codeMirrorPhrases,
          theme: theme.palette.mode === 'dark' ? 'material' : 'one-light'
        }}
        onChange={(_: any, __: any, nbt: string) => {
          const n = parse(nbt) as any as NBT
          const newItem: any = { ...item, nbt }
          if (n.Count?.value != null) newItem.amount = n.Count.value
          setItem(newItem)
        }}
      />
    </Box>}
    <DialogActions>
      <Button onClick={cancel}>{minecraft['gui.cancel']}</Button>
      <Button onClick={() => {
        setItem(undefined)
        if (_resolve) {
          _resolve(!item || item.type === 'AIR' ? null : item)
          _resolve = null
        }
      }}>{minecraft['gui.ok']}</Button>
    </DialogActions>
  </Dialog>
}
Example #12
Source File: EntityView.tsx    From firecms with MIT License 4 votes vote down vote up
export function EntityView<M extends { [Key: string]: any }, UserType>({
                                                                           path,
                                                                           entityId,
                                                                           callbacks,
                                                                           selectedSubpath,
                                                                           copy,
                                                                           permissions,
                                                                           schema: schemaOrResolver,
                                                                           subcollections,
                                                                           onModifiedValues,
                                                                           width
                                                                       }: EntityViewProps<M, UserType>) {

    const resolvedWidth: string | undefined = typeof width === "number" ? `${width}px` : width;
    const classes = useStylesSide({ containerWidth: resolvedWidth ?? CONTAINER_WIDTH });

    const dataSource = useDataSource();
    const sideEntityController = useSideEntityController();
    const snackbarContext = useSnackbarController();
    const context = useFireCMSContext();
    const authController = useAuthController<UserType>();

    const [status, setStatus] = useState<EntityStatus>(copy ? "copy" : (entityId ? "existing" : "new"));
    const [currentEntityId, setCurrentEntityId] = useState<string | undefined>(entityId);
    const [readOnly, setReadOnly] = useState<boolean>(false);
    const [tabsPosition, setTabsPosition] = React.useState(-1);
    const [modifiedValues, setModifiedValues] = useState<EntityValues<any> | undefined>();

    const {
        entity,
        dataLoading,
        // eslint-disable-next-line no-unused-vars
        dataLoadingError
    } = useEntityFetch({
        path,
        entityId: currentEntityId,
        schema: schemaOrResolver,
        useCache: false
    });

    const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);

    const resolvedSchema:ResolvedEntitySchema<M> = useMemo(() => computeSchema({
        schemaOrResolver,
        path,
        entityId,
        values: modifiedValues,
        previousValues: usedEntity?.values
    }), [schemaOrResolver, path, entityId, modifiedValues]);

    useEffect(() => {
        function beforeunload(e: any) {
            if (modifiedValues) {
                e.preventDefault();
                e.returnValue = `You have unsaved changes in this ${resolvedSchema.name}. Are you sure you want to leave this page?`;
            }
        }

        if (typeof window !== "undefined")
            window.addEventListener("beforeunload", beforeunload);

        return () => {
            if (typeof window !== "undefined")
                window.removeEventListener("beforeunload", beforeunload);
        };

    }, [modifiedValues, window]);

    const customViews = resolvedSchema.views;
    const customViewsCount = customViews?.length ?? 0;

    useEffect(() => {
        setUsedEntity(entity);
        if (entity)
            setReadOnly(!canEdit(permissions, entity, authController, path, context));
    }, [entity, permissions]);

    const theme = useTheme();
    const largeLayout = useMediaQuery(theme.breakpoints.up("lg"));

    useEffect(() => {
        if (!selectedSubpath)
            setTabsPosition(-1);

        if (customViews) {
            const index = customViews
                .map((c) => c.path)
                .findIndex((p) => p === selectedSubpath);
            setTabsPosition(index);
        }

        if (subcollections && selectedSubpath) {
            const index = subcollections
                .map((c) => c.path)
                .findIndex((p) => p === selectedSubpath);
            setTabsPosition(index + customViewsCount);
        }
    }, [selectedSubpath]);


    const onPreSaveHookError = useCallback((e: Error) => {
        snackbarContext.open({
            type: "error",
            title: "Error before saving",
            message: e?.message
        });
        console.error(e);
    }, []);

    const onSaveSuccessHookError = useCallback((e: Error) => {
        snackbarContext.open({
            type: "error",
            title: `${resolvedSchema.name}: Error after saving (entity is saved)`,
            message: e?.message
        });
        console.error(e);
    }, []);

    const onSaveSuccess = useCallback((updatedEntity: Entity<M>) => {

        setCurrentEntityId(updatedEntity.id);

        snackbarContext.open({
            type: "success",
            message: `${resolvedSchema.name}: Saved correctly`
        });

        setUsedEntity(updatedEntity);
        setStatus("existing");
        onModifiedValues(false);

        if (tabsPosition === -1)
            sideEntityController.close();

    }, []);

    const onSaveFailure = useCallback((e: Error) => {

        snackbarContext.open({
            type: "error",
            title: `${resolvedSchema.name}: Error saving`,
            message: e?.message
        });

        console.error("Error saving entity", path, entityId);
        console.error(e);
    }, []);

    const onEntitySave = useCallback(async ({
                                                schema,
                                                path,
                                                entityId,
                                                values,
                                                previousValues
                                            }: {
        schema: EntitySchema<M>,
        path: string,
        entityId: string | undefined,
        values: EntityValues<M>,
        previousValues?: EntityValues<M>,
    }): Promise<void> => {

        console.log("onEntitySave", path)
        if (!status)
            return;

        return saveEntityWithCallbacks({
            path,
            entityId,
            callbacks,
            values,
            previousValues,
            schema,
            status,
            dataSource,
            context,
            onSaveSuccess,
            onSaveFailure,
            onPreSaveHookError,
            onSaveSuccessHookError
        });
    }, [status, callbacks, dataSource, context, onSaveSuccess, onSaveFailure, onPreSaveHookError, onSaveSuccessHookError]);

    const onDiscard = useCallback(() => {
        if (tabsPosition === -1)
            sideEntityController.close();
    }, [sideEntityController, tabsPosition]);

    const body = !readOnly
        ? (
            <EntityForm
                key={`form_${path}_${usedEntity?.id ?? "new"}`}
                status={status}
                path={path}
                schemaOrResolver={schemaOrResolver}
                onEntitySave={onEntitySave}
                onDiscard={onDiscard}
                onValuesChanged={setModifiedValues}
                onModified={onModifiedValues}
                entity={usedEntity}/>
        )
        : (usedEntity &&
            <EntityPreview
                entity={usedEntity}
                path={path}
                schema={schemaOrResolver}/>

        )
    ;

    const customViewsView: JSX.Element[] | undefined = customViews && customViews.map(
        (customView, colIndex) => {
            return (
                <Box
                    className={classes.subcollectionPanel}
                    key={`custom_view_${customView.path}_${colIndex}`}
                    role="tabpanel"
                    flexGrow={1}
                    height={"100%"}
                    width={"100%"}
                    hidden={tabsPosition !== colIndex}>
                    <ErrorBoundary>
                        {customView.builder({
                            schema: resolvedSchema,
                            entity: usedEntity,
                            modifiedValues: modifiedValues ?? usedEntity?.values
                        })}
                    </ErrorBoundary>
                </Box>
            );
        }
    );

    const subCollectionsViews = subcollections && subcollections.map(
        (subcollection, colIndex) => {
            const absolutePath = usedEntity ? `${usedEntity?.path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;

            return (
                <Box
                    className={classes.subcollectionPanel}
                    key={`subcol_${subcollection.name}_${colIndex}`}
                    role="tabpanel"
                    flexGrow={1}
                    hidden={tabsPosition !== colIndex + customViewsCount}>
                    {usedEntity && absolutePath
                        ? <EntityCollectionView
                            path={absolutePath}
                            collection={subcollection}/>
                        : <Box m={3}
                               display={"flex"}
                               alignItems={"center"}
                               justifyContent={"center"}>
                            <Box>
                                You need to save your entity before
                                adding additional collections
                            </Box>
                        </Box>
                    }
                </Box>
            );
        }
    );

    const getSelectedSubpath = useCallback((value: number) => {
        if (value === -1) return undefined;

        if (customViews && value < customViewsCount) {
            return customViews[value].path;
        }

        if (subcollections) {
            return subcollections[value - customViewsCount].path;
        }

        throw Error("Something is wrong in getSelectedSubpath");
    }, [customViews]);

    const onSideTabClick = useCallback((value: number) => {
        setTabsPosition(value);
        if (entityId) {
            sideEntityController.open({
                path,
                entityId,
                selectedSubpath: getSelectedSubpath(value),
                overrideSchemaRegistry: false
            });
        }
    }, []);


    const loading = dataLoading || (!usedEntity && status === "existing");

    const header = (
        <Box sx={{
            paddingLeft: 2,
            paddingRight: 2,
            paddingTop: 2,
            display: "flex",
            alignItems: "center",
            backgroundColor: theme.palette.mode === "light" ? theme.palette.background.default : theme.palette.background.paper
        }}
        >

            <IconButton onClick={(e) => sideEntityController.close()}
                        size="large">
                <CloseIcon/>
            </IconButton>

            <Tabs
                value={tabsPosition === -1 ? 0 : false}
                indicatorColor="secondary"
                textColor="inherit"
                scrollButtons="auto"
            >
                <Tab
                    label={resolvedSchema.name}
                    classes={{
                        root: classes.tab
                    }}
                    wrapped={true}
                    onClick={() => {
                        onSideTabClick(-1);
                    }}/>
            </Tabs>

            <Box flexGrow={1}/>

            {loading &&
            <CircularProgress size={16} thickness={8}/>}

            <Tabs
                value={tabsPosition >= 0 ? tabsPosition : false}
                indicatorColor="secondary"
                textColor="inherit"
                onChange={(ev, value) => {
                    onSideTabClick(value);
                }}
                className={classes.tabBar}
                variant="scrollable"
                scrollButtons="auto"
            >

                {customViews && customViews.map(
                    (view) =>
                        <Tab
                            classes={{
                                root: classes.tab
                            }}
                            wrapped={true}
                            key={`entity_detail_custom_tab_${view.name}`}
                            label={view.name}/>
                )}

                {subcollections && subcollections.map(
                    (subcollection) =>
                        <Tab
                            classes={{
                                root: classes.tab
                            }}
                            wrapped={true}
                            key={`entity_detail_collection_tab_${subcollection.name}`}
                            label={subcollection.name}/>
                )}

            </Tabs>
        </Box>

    );

    return <div
        className={clsx(classes.container, { [classes.containerWide]: tabsPosition !== -1 })}>
        {
            loading
                ? <CircularProgressCenter/>
                : <>

                    {header}

                    <Divider/>

                    <div className={classes.tabsContainer}>

                        <Box
                            role="tabpanel"
                            hidden={!largeLayout && tabsPosition !== -1}
                            className={classes.form}>
                            {body}
                        </Box>

                        {customViewsView}

                        {subCollectionsViews}

                    </div>

                </>
        }

    </div>;
}
Example #13
Source File: Library.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function Library() {
    const { setTitle, setAction } = useContext(NavbarContext);
    useEffect(() => {
        setTitle('Library'); setAction(
            <>
                <AppbarSearch />
                <LibraryOptions />
                <UpdateChecker />
            </>,
        );
    }, []);

    const [tabs, setTabs] = useState<IMangaCategory[]>();

    const [tabNum, setTabNum] = useState<number>(0);
    const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam);

    // a hack so MangaGrid doesn't stop working. I won't change it in case
    // if I do manga pagination for library..
    const [lastPageNum, setLastPageNum] = useState<number>(1);

    const handleTabChange = (newTab: number) => {
        setTabNum(newTab);
        setTabSearchParam(newTab);
    };

    useEffect(() => {
        client.get('/api/v1/category')
            .then((response) => response.data)
            .then((categories: ICategory[]) => {
                const categoryTabs = categories.map((category) => ({
                    category,
                    mangas: [] as IManga[],
                    isFetched: false,
                }));
                setTabs(categoryTabs);
                if (categoryTabs.length > 0) {
                    if (
                        tabSearchParam !== undefined
                         && tabSearchParam !== null
                         && !Number.isNaN(tabSearchParam)
                         && categories.some((category) => category.order === Number(tabSearchParam))
                    ) {
                        handleTabChange(Number(tabSearchParam!));
                    } else { handleTabChange(categoryTabs[0].category.order); }
                }
            });
    }, []);

    // fetch the current tab
    useEffect(() => {
        if (tabs !== undefined) {
            tabs.forEach((tab, index) => {
                if (tab.category.order === tabNum && !tab.isFetched) {
                    // eslint-disable-next-line @typescript-eslint/no-shadow
                    client.get(`/api/v1/category/${tab.category.id}`)
                        .then((response) => response.data)
                        .then((data: IManga[]) => {
                            const tabsClone = cloneObject(tabs);
                            tabsClone[index].mangas = data;
                            tabsClone[index].isFetched = true;

                            setTabs(tabsClone);
                        });
                }
            });
        }
    }, [tabs?.length, tabNum]);

    if (tabs === undefined) {
        return <LoadingPlaceholder />;
    }

    if (tabs.length === 0) {
        return <EmptyView message="Your Library is empty" />;
    }

    let toRender;
    if (tabs.length > 1) {
        // eslint-disable-next-line max-len
        const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));

        const tabBodies = tabs.map((tab) => (
            <TabPanel index={tab.category.order} currentIndex={tabNum}>
                <LibraryMangaGrid
                    mangas={tab.mangas}
                    hasNextPage={false}
                    lastPageNum={lastPageNum}
                    setLastPageNum={setLastPageNum}
                    message="Category is Empty"
                    isLoading={!tab.isFetched}
                />
            </TabPanel>
        ));

        // Visual Hack: 160px is min-width for viewport width of >600
        const scrollableTabs = window.innerWidth < tabs.length * 160;
        toRender = (
            <>
                <Tabs
                    key={tabNum}
                    value={tabNum}
                    onChange={(e, newTab) => handleTabChange(newTab)}
                    indicatorColor="primary"
                    textColor="primary"
                    centered={!scrollableTabs}
                    variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
                    scrollButtons
                    allowScrollButtonsMobile
                >
                    {tabDefines}
                </Tabs>
                {tabBodies}
            </>
        );
    } else {
        const mangas = tabs.length === 1 ? tabs[0].mangas : [];
        toRender = (
            <LibraryMangaGrid
                mangas={mangas}
                hasNextPage={false}
                lastPageNum={lastPageNum}
                setLastPageNum={setLastPageNum}
                message="Your Library is empty"
                isLoading={!tabs[0].isFetched}
            />
        );
    }

    return toRender;
}
Example #14
Source File: ChapterOptions.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function ChapterOptions(props: IProps) {
    const { options, optionsDispatch } = props;
    const [filtersOpen, setFiltersOpen] = useState(false);
    const [tabNum, setTabNum] = useState(0);

    const filterOptions = useCallback(
        (value: NullAndUndefined<boolean>, name: string) => {
            optionsDispatch({ type: 'filter', filterType: name.toLowerCase(), filterValue: value });
        }, [],
    );

    return (
        <>
            <IconButton
                onClick={() => setFiltersOpen(!filtersOpen)}
                color={options.active ? 'warning' : 'default'}
            >
                <FilterListIcon />
            </IconButton>

            <Drawer
                anchor="bottom"
                open={filtersOpen}
                onClose={() => setFiltersOpen(false)}
                PaperProps={{
                    style: {
                        maxWidth: 600,
                        padding: '1em',
                        marginLeft: 'auto',
                        marginRight: 'auto',
                        minHeight: '150px',
                    },
                }}
            >
                <Box>
                    <Tabs
                        key={tabNum}
                        value={tabNum}
                        variant="fullWidth"
                        onChange={(e, newTab) => setTabNum(newTab)}
                        indicatorColor="primary"
                        textColor="primary"
                    >
                        <Tab value={0} label="Filter" />
                        <Tab value={1} label="Sort" />
                        <Tab value={2} label="Display" />
                    </Tabs>
                    <TabPanel index={0} currentIndex={tabNum}>
                        <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '150px' }}>
                            <FormControlLabel control={<ThreeStateCheckbox name="Unread" checked={options.unread} onChange={filterOptions} />} label="Unread" />
                            <FormControlLabel control={<ThreeStateCheckbox name="Downloaded" checked={options.downloaded} onChange={filterOptions} />} label="Downloaded" />
                            <FormControlLabel control={<ThreeStateCheckbox name="Bookmarked" checked={options.bookmarked} onChange={filterOptions} />} label="Bookmarked" />
                        </Box>
                    </TabPanel>
                    <TabPanel index={1} currentIndex={tabNum}>
                        <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '150px' }}>
                            {
                                SortTab.map((item) => (
                                    <Stack
                                        direction="row"
                                        alignItems="center"
                                        spacing="2"
                                        sx={{ py: 1, height: 42 }}
                                        onClick={() => (item[0] !== options.sortBy
                                            ? optionsDispatch({ type: 'sortBy', sortBy: item[0] })
                                            : optionsDispatch({ type: 'sortReverse' }))}
                                    >
                                        <Box sx={{ height: 24, width: 24 }}>
                                            {
                                                options.sortBy === item[0]
                                                && (options.reverse
                                                    ? (<ArrowUpward color="primary" />) : (<ArrowDownward color="primary" />))
                                            }
                                        </Box>
                                        <Typography>{item[1]}</Typography>
                                    </Stack>

                                ))
                            }
                        </Box>
                    </TabPanel>
                    <TabPanel index={2} currentIndex={tabNum}>
                        <Stack flexDirection="column" sx={{ minHeight: '150px' }}>
                            <RadioGroup name="chapter-title-display" onChange={() => optionsDispatch({ type: 'showChapterNumber' })} value={options.showChapterNumber}>
                                <FormControlLabel label="By Source Title" value="title" control={<Radio checked={!options.showChapterNumber} />} />
                                <FormControlLabel label="By Chapter Number" value="chapterNumber" control={<Radio checked={options.showChapterNumber} />} />
                            </RadioGroup>
                        </Stack>
                    </TabPanel>
                </Box>
            </Drawer>

        </>
    );
}
Example #15
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 4 votes vote down vote up
WifiDeviceSelect: FunctionComponent<WifiDeviceSelectProps> = (props) => {
  const { wifiDevice, wifiDevices, onChange } = props;

  const options = useMemo(() => {
    const result = wifiDevices.map((target) => {
      return {
        label: `${target.name} - ${
          target.deviceName ? target.deviceName : target.target
        } (${target.ip})`,
        value: target.ip,
      };
    });

    if (result.length === 0) {
      result.push({
        label: `Default (10.0.0.1)`,
        value: `10.0.0.1`,
      });
    }

    return result;
  }, [wifiDevices]);

  const [
    currentlySelectedValue,
    setCurrentlySelectedValue,
  ] = useState<Option | null>(
    wifiDevice
      ? options.find((item) => item.value === wifiDevice) ?? null
      : null
  );

  useEffect(() => {
    setCurrentlySelectedValue(
      options.find((item) => item.value === wifiDevice) ??
        options[0] ??
        currentlySelectedValue
    );
  }, [currentlySelectedValue, options, wifiDevice]);

  const [currentTextBoxValue, setCurrentTextBoxValue] = useState<string | null>(
    wifiDevice
  );

  const onDeviceSelectChange = useCallback(
    (value: string | null) => {
      if (value === null) {
        setCurrentlySelectedValue(null);
      } else {
        setCurrentlySelectedValue(
          options.find((item) => item.value === value) ?? null
        );
        onChange(value);
      }
    },
    [onChange, options]
  );

  const onTextFieldValueChange = useCallback(
    (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
      setCurrentTextBoxValue(event.target.value);
      onChange(event.target.value);
    },
    [onChange]
  );

  const [wifiSource, setWifiSource] = useState<WifiSourceType>(
    WifiSourceType.LIST
  );

  const handleWifiSourceChange = useCallback(
    (_event: React.SyntheticEvent, value: WifiSourceType) => {
      setWifiSource(value);
      if (value === WifiSourceType.LIST) {
        onChange(currentlySelectedValue?.value ?? null);
      } else if (value === WifiSourceType.MANUAL) {
        onChange(currentTextBoxValue);
      }
    },
    [currentTextBoxValue, currentlySelectedValue, onChange]
  );

  return (
    <Box sx={styles.root}>
      <Tabs
        sx={styles.tabs}
        defaultValue={WifiSourceType.LIST}
        value={wifiSource}
        onChange={handleWifiSourceChange}
      >
        <Tab label="WiFi Devices" value={WifiSourceType.LIST} />
        <Tab label="Manual" value={WifiSourceType.MANUAL} />
      </Tabs>
      {wifiSource === WifiSourceType.LIST && (
        <Box sx={styles.inner}>
          <Omnibox
            title="WiFi Device Selection"
            currentValue={
              options.find(
                (item) => item.value === currentlySelectedValue?.value
              ) ?? null
            }
            onChange={onDeviceSelectChange}
            options={options}
          />
        </Box>
      )}

      {wifiSource === WifiSourceType.MANUAL && (
        <div>
          <TextField
            onChange={onTextFieldValueChange}
            fullWidth
            label="IP Address"
            value={currentTextBoxValue}
          />
        </div>
      )}
    </Box>
  );
}
Example #16
Source File: index.tsx    From ExpressLRS-Configurator with GNU General Public License v3.0 4 votes vote down vote up
FirmwareVersionForm: FunctionComponent<FirmwareVersionCardProps> = (
  props
) => {
  const { onChange, data, gitRepository } = props;

  const [firmwareSource, setFirmwareSource] = useState<FirmwareSource>(
    data?.source || FirmwareSource.GitTag
  );
  const handleFirmwareSourceChange = (
    _event: React.SyntheticEvent,
    value: FirmwareSource
  ) => {
    setFirmwareSource(value);
  };

  const [showPreReleases, setShowPreReleases] = useState<boolean>(false);
  useEffect(() => {
    new ApplicationStorage()
      .getShowPreReleases(false)
      .then((value) => {
        setShowPreReleases(value);
      })
      .catch((err: Error) => {
        console.error('failed to get show pre-releases from storage', err);
      });
  }, []);
  const onShowPreReleases = (
    _event: React.ChangeEvent<HTMLInputElement>,
    checked: boolean
  ) => {
    setShowPreReleases(checked);
    new ApplicationStorage().setShowPreReleases(checked).catch((err: Error) => {
      console.error('failed to set show pre-releases in storage', err);
    });
  };

  const [
    queryGitTags,
    { loading: gitTagsLoading, data: gitTagsResponse, error: tagsError },
  ] = useGetReleasesLazyQuery();

  const [
    queryGitBranches,
    {
      loading: gitBranchesLoading,
      data: gitBranchesResponse,
      error: branchesError,
    },
  ] = useGetBranchesLazyQuery();

  const [
    queryGitPullRequests,
    {
      loading: gitPullRequestsLoading,
      data: gitPullRequestsResponse,
      error: pullRequestsError,
    },
  ] = useGetPullRequestsLazyQuery();

  const loading =
    gitTagsLoading || gitBranchesLoading || gitPullRequestsLoading;

  const gitTags = useMemo(() => {
    return (
      gitTagsResponse?.releases.filter(
        ({ tagName }) => !gitRepository.tagExcludes.includes(tagName)
      ) ?? []
    ).sort((a, b) => semver.rcompare(a.tagName, b.tagName));
  }, [gitRepository.tagExcludes, gitTagsResponse?.releases]);

  const gitBranches = useMemo(() => {
    return gitBranchesResponse?.gitBranches ?? [];
  }, [gitBranchesResponse?.gitBranches]);

  const gitPullRequests = gitPullRequestsResponse?.pullRequests;

  const [currentGitTag, setCurrentGitTag] = useState<string>(
    data?.gitTag || ''
  );
  const onGitTag = (name: string | null) => {
    if (name === null) {
      setCurrentGitTag('');
      return;
    }
    setCurrentGitTag(name);
  };

  /*
    We need to make sure that a valid value is selected
   */
  useEffect(() => {
    if (firmwareSource === FirmwareSource.GitTag) {
      if (
        !showPreReleases &&
        gitTags?.length &&
        gitTags?.length > 0 &&
        gitTags
          ?.filter(({ preRelease }) => !preRelease)
          .find((item) => item.tagName === currentGitTag) === undefined
      ) {
        setCurrentGitTag(gitTags[0].tagName);
      }
    }
  }, [showPreReleases, currentGitTag, gitTags, firmwareSource]);

  const [currentGitBranch, setCurrentGitBranch] = useState<string>(
    data?.gitBranch || ''
  );
  const onGitBranch = (name: string | null) => {
    if (name === null) {
      setCurrentGitBranch('');
      return;
    }
    setCurrentGitBranch(name);
  };

  const [currentGitCommit, setCurrentGitCommit] = useState<string>(
    data?.gitCommit || ''
  );
  const [debouncedGitCommit, setDebouncedGitCommit] = useState<string>(
    data?.gitCommit || ''
  );

  const debouncedGitCommitHandler = useMemo(
    () => debounce(setDebouncedGitCommit, 1000),
    [setDebouncedGitCommit]
  );

  // Stop the invocation of the debounced function
  // after unmounting
  useEffect(() => {
    return () => {
      debouncedGitCommitHandler.cancel();
    };
  }, [debouncedGitCommitHandler]);

  const setGitCommit = (value: string) => {
    setCurrentGitCommit(value);
    debouncedGitCommitHandler(value);
  };

  const onGitCommit = (event: React.ChangeEvent<HTMLInputElement>) => {
    setGitCommit(event.target.value);
  };

  const [localPath, setLocalPath] = useState<string>(data?.localPath || '');
  const onLocalPath = (event: React.ChangeEvent<HTMLInputElement>) => {
    setLocalPath(event.target.value);
  };

  const [
    currentGitPullRequest,
    setCurrentGitPullRequest,
  ] = useState<PullRequestInput | null>(data?.gitPullRequest || null);

  /*
    Make sure that a valid pull request is selected
   */
  useEffect(() => {
    if (gitPullRequestsResponse?.pullRequests && currentGitPullRequest) {
      const pullRequest =
        gitPullRequestsResponse.pullRequests.find(
          (item) => item.number === currentGitPullRequest.number
        ) || null;
      // if we have a list of pull requests and the current pull request is not
      // part of that list, then set current pull request to null
      if (!pullRequest) {
        setCurrentGitPullRequest(null);
      }
      // prevent stale head commit hash cache
      if (
        pullRequest &&
        pullRequest.headCommitHash !== currentGitPullRequest.headCommitHash
      ) {
        setCurrentGitPullRequest({
          id: pullRequest.id,
          number: pullRequest.number,
          title: pullRequest.title,
          headCommitHash: pullRequest.headCommitHash,
        });
      }
    }
  }, [gitPullRequestsResponse, currentGitPullRequest]);

  const onGitPullRequest = (value: string | null) => {
    if (value === null) {
      setCurrentGitPullRequest(null);
      return;
    }
    const iValue = parseInt(value, 10);
    const pullRequest = gitPullRequests?.find((item) => item.number === iValue);
    if (pullRequest) {
      setCurrentGitPullRequest({
        id: pullRequest.id,
        number: pullRequest.number,
        title: pullRequest.title,
        headCommitHash: pullRequest.headCommitHash,
      });
    }
  };

  useEffect(() => {
    const storage = new ApplicationStorage();
    storage
      .getFirmwareSource(gitRepository)
      .then((result) => {
        if (result !== null) {
          if (result.source) setFirmwareSource(result.source);
          if (result.gitTag) setCurrentGitTag(result.gitTag);
          if (result.gitCommit) setGitCommit(result.gitCommit);
          if (result.gitBranch) setCurrentGitBranch(result.gitBranch);
          if (result.localPath) setLocalPath(result.localPath);
          if (result.gitPullRequest)
            setCurrentGitPullRequest(result.gitPullRequest);
        }
      })
      .catch((err) => {
        console.error('failed to get firmware source', err);
      });
  }, []);

  const onChooseFolder = () => {
    ipcRenderer
      .invoke(IpcRequest.ChooseFolder)
      .then((result: ChooseFolderResponseBody) => {
        if (result.success) {
          setLocalPath(result.directoryPath);
        }
      })
      .catch((err) => {
        console.error('failed to get local directory path: ', err);
      });
  };

  useEffect(() => {
    switch (firmwareSource) {
      case FirmwareSource.GitTag:
        queryGitTags({
          variables: {
            owner: gitRepository.owner,
            repository: gitRepository.repositoryName,
          },
        });
        break;
      case FirmwareSource.GitBranch:
        queryGitBranches({
          variables: {
            owner: gitRepository.owner,
            repository: gitRepository.repositoryName,
          },
        });
        break;
      case FirmwareSource.GitCommit:
        break;
      case FirmwareSource.Local:
        break;
      case FirmwareSource.GitPullRequest:
        queryGitPullRequests({
          variables: {
            owner: gitRepository.owner,
            repository: gitRepository.repositoryName,
          },
        });
        break;
      default:
        throw new Error(`unknown firmware source: ${firmwareSource}`);
    }
  }, [
    gitRepository,
    firmwareSource,
    queryGitTags,
    queryGitBranches,
    queryGitPullRequests,
  ]);

  useEffect(() => {
    const updatedData = {
      source: firmwareSource,
      gitBranch: currentGitBranch,
      gitTag: currentGitTag,
      gitCommit: debouncedGitCommit,
      localPath,
      gitPullRequest: currentGitPullRequest,
    };
    onChange(updatedData);
    const storage = new ApplicationStorage();
    storage.setFirmwareSource(updatedData, gitRepository).catch((err) => {
      console.error('failed to set firmware source', err);
    });
  }, [
    firmwareSource,
    currentGitBranch,
    currentGitTag,
    debouncedGitCommit,
    localPath,
    currentGitPullRequest,
    onChange,
    gitRepository,
  ]);

  const gitTagOptions = useMemo(() => {
    return gitTags
      .filter((item) => {
        if (!showPreReleases) {
          return item.preRelease === false;
        }
        return true;
      })
      .map((item) => ({
        label: item.tagName,
        value: item.tagName,
      }));
  }, [gitTags, showPreReleases]);

  const gitBranchOptions = useMemo(() => {
    return gitBranches.map((branch) => ({
      label: branch,
      value: branch,
    }));
  }, [gitBranches]);

  const gitPullRequestOptions = useMemo(() => {
    return gitPullRequests?.map((pullRequest) => ({
      label: `${pullRequest.title} #${pullRequest.number}`,
      value: `${pullRequest.number}`,
    }));
  }, [gitPullRequests]);

  const showBetaFpvAlert =
    localPath?.toLocaleLowerCase()?.indexOf('betafpv') > -1;
  return (
    <>
      <Tabs
        sx={styles.tabs}
        defaultValue={FirmwareSource.GitTag}
        value={firmwareSource}
        onChange={handleFirmwareSourceChange}
      >
        <Tab label="Official releases" value={FirmwareSource.GitTag} />
        <Tab label="Git branch" value={FirmwareSource.GitBranch} />
        <Tab label="Git commit" value={FirmwareSource.GitCommit} />
        <Tab label="Local" value={FirmwareSource.Local} />
        <Tab label="Git Pull Request" value={FirmwareSource.GitPullRequest} />
      </Tabs>
      {firmwareSource === FirmwareSource.GitTag && gitTags !== undefined && (
        <>
          <Box sx={styles.tabContents}>
            {!loading && (
              <>
                <FormControlLabel
                  sx={styles.preReleaseCheckbox}
                  control={
                    <Checkbox
                      checked={showPreReleases}
                      onChange={onShowPreReleases}
                    />
                  }
                  label="Show pre-releases"
                />
                <Omnibox
                  title="Releases"
                  options={gitTagOptions}
                  currentValue={
                    gitTagOptions.find(
                      (item) => item.value === currentGitTag
                    ) ?? null
                  }
                  onChange={onGitTag}
                />
                <Button
                  size="small"
                  sx={styles.releaseNotes}
                  target="_blank"
                  rel="noreferrer noreferrer"
                  href={`${gitRepository.url}/releases/tag/${currentGitTag}`}
                >
                  Release notes
                </Button>
                {currentGitTag &&
                  gitTagOptions.length > 0 &&
                  gitTagOptions[0]?.value !== currentGitTag && (
                    <Alert sx={styles.firmwareVersionAlert} severity="info">
                      There is a newer version of the firmware available
                    </Alert>
                  )}
              </>
            )}
          </Box>
        </>
      )}
      {firmwareSource === FirmwareSource.GitBranch &&
        gitBranches !== undefined && (
          <>
            <Alert severity="warning" sx={styles.dangerZone}>
              <AlertTitle>DANGER ZONE</AlertTitle>
              Use these sources only if you know what you are doing or was
              instructed by project developers
            </Alert>
            <Box sx={styles.tabContents}>
              {!loading && (
                <Omnibox
                  title="Git branches"
                  options={gitBranchOptions}
                  currentValue={
                    gitBranchOptions.find(
                      (item) => item.value === currentGitBranch
                    ) ?? null
                  }
                  onChange={onGitBranch}
                />
              )}
            </Box>
          </>
        )}
      {firmwareSource === FirmwareSource.GitCommit && (
        <>
          <Alert severity="warning" sx={styles.dangerZone}>
            <AlertTitle>DANGER ZONE</AlertTitle>
            Use these sources only if you know what you are doing or was
            instructed by project developers
          </Alert>
          <Box sx={styles.tabContents}>
            <TextField
              id="git-commit-hash"
              label="Git commit hash"
              fullWidth
              value={currentGitCommit}
              onChange={onGitCommit}
            />
          </Box>
        </>
      )}
      {firmwareSource === FirmwareSource.Local && (
        <>
          <Alert severity="warning" sx={styles.dangerZone}>
            <AlertTitle>DANGER ZONE</AlertTitle>
            Use these sources only if you know what you are doing or was
            instructed by project developers
          </Alert>
          <Box sx={styles.tabContents}>
            <TextField
              id="local-path"
              label="Local path"
              fullWidth
              value={localPath}
              onChange={onLocalPath}
            />

            {showBetaFpvAlert && (
              <Alert severity="error" sx={styles.betaFpvAlert}>
                <AlertTitle>ATTENTION</AlertTitle>
                You are trying to flash an outdated BetaFPV custom ExpressLRS
                fork. BetaFPV hardware is fully supported in recent official
                ExpressLRS releases. We recommend using official firmware to
                have the best ExpressLRS experience.
              </Alert>
            )}

            <Button
              color="secondary"
              size="small"
              variant="contained"
              sx={styles.chooseFolderButton}
              onClick={onChooseFolder}
            >
              Choose folder
            </Button>
          </Box>
        </>
      )}
      {firmwareSource === FirmwareSource.GitPullRequest &&
        gitPullRequests !== undefined && (
          <>
            <Alert severity="warning" sx={styles.dangerZone}>
              <AlertTitle>DANGER ZONE</AlertTitle>
              Use these sources only if you know what you are doing or was
              instructed by project developers
            </Alert>
            <Box sx={styles.tabContents}>
              {!loading && (
                <Omnibox
                  title="Git pull Requests"
                  options={gitPullRequestOptions ?? []}
                  currentValue={
                    gitPullRequestOptions?.find(
                      (item) =>
                        item.value === `${currentGitPullRequest?.number}`
                    ) ?? null
                  }
                  onChange={onGitPullRequest}
                />
              )}
            </Box>
          </>
        )}
      <Loader loading={loading} />
      <ShowAlerts severity="error" messages={branchesError} />
      <ShowAlerts severity="error" messages={tagsError} />
      <ShowAlerts severity="error" messages={pullRequestsError} />
    </>
  );
}
Example #17
Source File: SignIn.tsx    From Cromwell with MIT License 4 votes vote down vote up
SignInModal = observer(() => {
  const authClient = useAuthClient();
  const formType = appState.signInFormType;
  const activeTab = formType === 'sign-up' ? 1 : 0;

  const handleTabChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
    if (newValue === 1) appState.signInFormType = 'sign-up';
    if (newValue === 0) appState.signInFormType = 'sign-in';
  };

  const onSignUpSuccess = async (user: TUser, password: string) => {
    if (!user.email) return;
    // Automatic sign-in after sign-up 
    const result = await authClient.signIn(user.email, password);
    if (result.success) {
      toast.success('You have been registered')
      handleClose();
    } else {
      toast.error(result.message);
    }
  }

  const handleClose = () => {
    appState.isSignInOpen = false;
    appState.signInFormType = 'sign-in';
  }

  const signInElements: SignInProps['elements'] = {
    TextField: (props) => <TextField fullWidth
      variant="standard"
      size="small"
      className={styles.textField}
      {...props}
    />,
    PasswordField,
    Button: (props: any) => <Button
      color="primary"
      variant="contained"
      className={styles.loginBtn}
      {...props} />
  }

  return (
    <Modal
      className={commonStyles.center}
      open={appState.isSignInOpen}
      onClose={handleClose}
      blurSelector={"#CB_root"}
    >
      <div className={styles.SingIn}>
        <Tabs
          className={styles.tabs}
          value={activeTab}
          indicatorColor="primary"
          textColor="primary"
          onChange={handleTabChange}
        >
          <Tab
            className={styles.tab}
            label="Sign in" />
          <Tab
            className={styles.tab}
            label="Sign up" />
        </Tabs>
        {formType === 'sign-in' && (
          <SignIn
            classes={{
              root: styles.loginForm,
              forgotPassButton: styles.forgotPassText,
              backToSignInButton: styles.forgotPassText,
              resetPassInstructions: styles.resetPassInstructions,
            }}
            elements={signInElements}
            onSignInSuccess={handleClose}
            onSignInError={(result) => toast.error(result.message)}
            onForgotPasswordFailure={(result) => toast.error(result.message)}
            onResetPasswordFailure={(result) => toast.error(result.message)}
            onForgotPasswordEmailSent={() => toast.success('We sent you an email')}
            onResetPasswordSuccess={() => toast.success('Password changed')}
          />
        )}
        {formType === 'sign-up' && (
          <SignUp
            classes={{ root: styles.loginForm }}
            elements={signInElements}
            onSignUpSuccess={onSignUpSuccess}
            onSignUpError={(result) => toast.error(result.message)}
          />
        )}
      </div>
    </Modal >
  )
})
Example #18
Source File: Product.tsx    From Cromwell with MIT License 4 votes vote down vote up
ProductPage = () => {
    const { id: productId } = useParams<{ id: string }>();
    const client = getGraphQLClient();
    const [isLoading, setIsLoading] = useState(false);
    // const [product, setProdData] = useState<TProduct | null>(null);
    const [attributes, setAttributes] = useState<TAttribute[]>([]);

    const [activeTabNum, setActiveTabNum] = React.useState(0);
    const productRef = React.useRef<TProduct | null>(null);
    const [notFound, setNotFound] = useState(false);
    const [canValidate, setCanValidate] = useState(false);
    const forceUpdate = useForceUpdate();
    const history = useHistory();

    const product: TProduct | undefined = productRef.current;

    const setProdData = (data: TProduct) => {
        productRef.current = Object.assign({}, productRef.current, data);
    }

    useEffect(() => {
        return () => {
            resetSelected();
        }
    }, []);

    const getProduct = async () => {
        let prod: TProduct | undefined;
        if (productId && productId !== 'new') {
            try {
                prod = await client?.getProductById(parseInt(productId), gql`
                    fragment AdminPanelProductFragment on Product {
                        id
                        slug
                        createDate
                        updateDate
                        isEnabled
                        pageTitle
                        pageDescription
                        meta {
                            keywords
                        }
                        name
                        price
                        oldPrice
                        sku
                        mainImage
                        images
                        description
                        descriptionDelta
                        views
                        mainCategoryId
                        stockAmount
                        stockStatus
                        manageStock
                        categories(pagedParams: {pageSize: 9999}) {
                            id
                        }
                        customMeta (keys: ${JSON.stringify(getCustomMetaKeysFor(EDBEntity.Product))})
                        attributes {
                            key
                            values {
                                value
                            }
                        }
                        variants {
                            id
                            name
                            price
                            oldPrice
                            sku
                            mainImage
                            images
                            description
                            descriptionDelta
                            stockAmount
                            stockStatus
                            manageStock
                            attributes
                        }
                    }`, 'AdminPanelProductFragment'
                );

            } catch (e) { console.error(e) }

            if (prod?.id) {
                setProdData(prod);
                store.setStateProp({
                    prop: 'selectedItems',
                    payload: Object.assign({}, ...(prod.categories ?? []).map(cat => ({ [cat.id]: true }))),
                });
                store.setStateProp({
                    prop: 'selectedItem',
                    payload: prod?.mainCategoryId,
                });

                forceUpdate();
            }
            else setNotFound(true);

        } else if (productId === 'new') {
            setProdData({} as any);
            forceUpdate();
        }
        return prod;
    }

    const getAttributes = async () => {
        try {
            const attr = await client?.getAttributes();
            if (attr) setAttributes(attr);
        } catch (e) { console.error(e) }
    }

    useEffect(() => {
        (async () => {
            setIsLoading(true);
            await getAttributes();
            await getProduct();
            setIsLoading(false);
        })();
    }, []);

    const refetchMeta = async () => {
        if (!productId) return;
        const data = await getProduct();
        return data?.customMeta;
    };

    const handleSave = async () => {
        const product = productRef.current;
        setCanValidate(true);

        if (!product?.name) return;

        const productAttributes = product.attributes?.map(attr => ({
            key: attr.key,
            values: attr.values ? attr.values.map(val => ({
                value: val.value,
            })) : [],
        }));

        const selectedItems = store.getState().selectedItems;
        const categoryIds = Object.keys(selectedItems)
            .filter(id => selectedItems[id]).map(Number).filter(Boolean);
        let mainCategoryId = store.getState().selectedItem ?? null;
        if (mainCategoryId && !categoryIds.includes(mainCategoryId)) mainCategoryId = null;

        if (product) {
            const input: TProductInput = {
                name: product.name,
                categoryIds,
                mainCategoryId,
                price: typeof product.price === 'string' ? parseFloat(product.price) : product.price,
                oldPrice: typeof product.oldPrice === 'string' ? parseFloat(product.oldPrice) : product.oldPrice,
                sku: product.sku,
                mainImage: product.mainImage,
                images: product.images,
                stockStatus: product.stockStatus ?? 'In stock',
                stockAmount: product.stockAmount,
                manageStock: product.manageStock,
                description: product.description,
                descriptionDelta: product.descriptionDelta,
                slug: product.slug,
                attributes: productAttributes,
                pageTitle: product.pageTitle,
                pageDescription: product.pageDescription,
                meta: product.meta && {
                    keywords: product.meta.keywords
                },
                variants: product.variants?.map(variant => ({
                    id: typeof variant.id === 'number' ? variant.id : undefined,
                    name: variant.name,
                    price: typeof variant.price === 'string' ? parseFloat(variant.price) : variant.price,
                    oldPrice: typeof variant.oldPrice === 'string' ? parseFloat(variant.oldPrice) : variant.oldPrice,
                    sku: variant.sku,
                    mainImage: variant.mainImage,
                    images: variant.images,
                    description: variant.description,
                    descriptionDelta: variant.descriptionDelta,
                    stockAmount: variant.stockAmount,
                    stockStatus: variant.stockStatus,
                    manageStock: variant.manageStock,
                    attributes: variant.attributes,
                })),
                customMeta: Object.assign({}, product.customMeta, await getCustomMetaFor(EDBEntity.Product)),
                isEnabled: product.isEnabled,
            }


            if (input.variants?.length) {
                // Ensure that variants have at least one specified attribute
                input.variants.forEach((variant, i) => {
                    const filteredAttributes: Record<string, string | number> = {};
                    Object.entries((variant.attributes ?? {})).forEach(([key, value]) => {
                        if (value && value !== 'any') filteredAttributes[key] = value;
                    });
                    if (!Object.keys(filteredAttributes).length) delete input.variants[i];
                });
                input.variants = input.variants.filter(Boolean);
            }


            if (productId === 'new') {
                setIsLoading(true);
                try {
                    const prod = await client?.createProduct(input);
                    if (prod?.id) {
                        toast.success('Created product');
                        history.replace(`${productPageInfo.baseRoute}/${prod.slug}`)
                        if (prod) setProdData(prod);
                        forceUpdate();
                    } else {
                        throw new Error('!prod?.id')
                    }
                } catch (e) {
                    toast.error('Failed to create');
                    console.error(e);
                }
                setIsLoading(false);

            } else {
                try {
                    await client?.updateProduct(product.id, input);
                    await getProduct();
                    toast.success('Updated product');
                } catch (e) {
                    toast.error('Failed to update');
                    console.error(e);
                }
            }
        }
        setCanValidate(false);
    }

    const handleTabChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
        setActiveTabNum(newValue);
    }


    if (notFound) {
        return (
            <div className={styles.Product}>
                <div className={styles.notFoundPage}>
                    <p className={styles.notFoundText}>Product not found</p>
                </div>
            </div>
        )
    }

    let pageFullUrl;
    if (product?.slug) {
        pageFullUrl = serviceLocator.getFrontendUrl() + resolvePageRoute('product', { slug: product.slug ?? product.id + '' });
    }

    return (
        <div className={styles.Product}>
            {/* <h2>Edit product</h2> */}
            <div className={styles.header}>
                {/* <p>Product id: {id}</p> */}
                <div className={styles.headerLeft}>
                    <IconButton
                        onClick={() => window.history.back()}
                    >
                        <ArrowBackIcon style={{ fontSize: '18px' }} />
                    </IconButton>
                    <p className={commonStyles.pageTitle}>product</p>
                </div>
                <div >
                    <Tabs
                        value={activeTabNum}
                        indicatorColor="primary"
                        textColor="primary"
                        onChange={handleTabChange}
                    >
                        <Tab label="Main" />
                        <Tab label="Attributes" />
                        <Tab label="Variants" />
                        <Tab label="Categories" />
                    </Tabs>
                </div>
                <div className={styles.headerActions}>
                    {pageFullUrl && (
                        <Tooltip title="Open product in the new tab">
                            <IconButton
                                className={styles.openPageBtn}
                                aria-label="open"
                                onClick={() => { window.open(pageFullUrl, '_blank'); }}
                            >
                                <OpenInNewIcon />
                            </IconButton>
                        </Tooltip>
                    )}
                    <Button variant="contained"
                        color="primary"
                        size="small"
                        className={styles.saveBtn}
                        onClick={handleSave}>
                        Save
                    </Button>
                </div>
            </div>
            {isLoading && <Skeleton width="100%" height="100%" style={{
                transform: 'none',
                margin: '20px 0'
            }} />}
            {!isLoading && product && (
                <>
                    <TabPanel value={activeTabNum} index={0}>
                        <div className={styles.mainTab}>
                            <MainInfoCard
                                product={product}
                                setProdData={setProdData}
                                canValidate={canValidate}
                            />
                            <div style={{ marginBottom: '15px' }}></div>
                            <RenderCustomFields
                                entityType={EDBEntity.Product}
                                entityData={product}
                                refetchMeta={refetchMeta}
                            />
                        </div>
                    </TabPanel>
                    <TabPanel value={activeTabNum} index={1}>
                        <AttributesTab
                            forceUpdate={forceUpdate}
                            product={product}
                            attributes={attributes}
                            setProdData={setProdData}
                        />
                    </TabPanel>
                    <TabPanel value={activeTabNum} index={2}>
                        <VariantsTab
                            forceUpdate={forceUpdate}
                            product={product}
                            attributes={attributes}
                            setProdData={setProdData}
                        />
                    </TabPanel>
                    <TabPanel value={activeTabNum} index={3}>
                        <CategoriesTab />
                    </TabPanel>
                </>
            )}
        </div >
    )
}