Feature/improve custom extension repos support (#540)
* Move "TextSetting" to new folder * Extract dialog of "TextSetting" * Immediately show dialog when adding a list item * Reuse "TextSettingProps" for "MutableListSetting" * Add "placeholder" prop to "MutableListSetting" * Prevent duplicates by default * Clear dialog values on save * Validate repo url
This commit is contained in:
@@ -15,7 +15,9 @@ import List from '@mui/material/List';
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import { TextSetting, TextSettingProps } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting, TextSettingProps } from '@/components/settings/text/TextSetting.tsx';
|
||||||
|
import { TextSettingDialog } from '@/components/settings/text/TextSettingDialog.tsx';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
|
||||||
const MutableListItem = ({
|
const MutableListItem = ({
|
||||||
handleDelete,
|
handleDelete,
|
||||||
@@ -35,24 +37,34 @@ const MutableListItem = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MutableListSettingProps = Pick<TextSettingProps, 'settingName' | 'placeholder'> & {
|
||||||
|
values?: string[];
|
||||||
|
description?: string;
|
||||||
|
addItemButtonTitle?: string;
|
||||||
|
handleChange: (values: string[]) => void;
|
||||||
|
allowDuplicates?: boolean;
|
||||||
|
validateItem?: (value: string) => boolean;
|
||||||
|
invalidItemError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const MutableListSetting = ({
|
export const MutableListSetting = ({
|
||||||
settingName,
|
settingName,
|
||||||
description,
|
description,
|
||||||
values,
|
values,
|
||||||
handleChange,
|
handleChange,
|
||||||
addItemButtonTitle,
|
addItemButtonTitle,
|
||||||
}: {
|
placeholder,
|
||||||
settingName: string;
|
allowDuplicates = false,
|
||||||
description?: string;
|
validateItem = () => true,
|
||||||
values?: string[];
|
invalidItemError,
|
||||||
handleChange: (values: string[]) => void;
|
}: MutableListSettingProps) => {
|
||||||
addItemButtonTitle?: string;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [dialogValues, setDialogValues] = useState(values ?? []);
|
const [dialogValues, setDialogValues] = useState(values ?? []);
|
||||||
|
|
||||||
|
const [isAddItemDialogOpen, setIsAddItemDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return;
|
return;
|
||||||
@@ -76,11 +88,25 @@ export const MutableListSetting = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDuplicate = !allowDuplicates && dialogValues.includes(newValue);
|
||||||
|
if (isDuplicate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateItem(newValue)) {
|
||||||
|
makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim()));
|
setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveChanges = () => {
|
const saveChanges = () => {
|
||||||
closeDialog();
|
closeDialog(true);
|
||||||
handleChange(dialogValues.filter((dialogValue) => dialogValue !== ''));
|
handleChange(dialogValues.filter((dialogValue) => dialogValue !== ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,8 +131,8 @@ export const MutableListSetting = ({
|
|||||||
<List>
|
<List>
|
||||||
{dialogValues.map((dialogValue, index) => (
|
{dialogValues.map((dialogValue, index) => (
|
||||||
<MutableListItem
|
<MutableListItem
|
||||||
settingName={dialogValue === '' ? t('global.label.placeholder') : ''}
|
settingName=""
|
||||||
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
|
placeholder={placeholder}
|
||||||
handleChange={(newValue: string) => updateSetting(index, newValue)}
|
handleChange={(newValue: string) => updateSetting(index, newValue)}
|
||||||
handleDelete={() => updateSetting(index, undefined)}
|
handleDelete={() => updateSetting(index, undefined)}
|
||||||
value={dialogValue}
|
value={dialogValue}
|
||||||
@@ -116,7 +142,7 @@ export const MutableListSetting = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between">
|
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between">
|
||||||
<Button onClick={() => updateSetting(dialogValues.length, '')}>
|
<Button onClick={() => setIsAddItemDialogOpen(true)}>
|
||||||
{addItemButtonTitle ?? t('global.button.add')}
|
{addItemButtonTitle ?? t('global.button.add')}
|
||||||
</Button>
|
</Button>
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
@@ -126,6 +152,16 @@ export const MutableListSetting = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{isAddItemDialogOpen && (
|
||||||
|
<TextSettingDialog
|
||||||
|
settingName=""
|
||||||
|
placeholder={placeholder}
|
||||||
|
handleChange={(newValue: string) => updateSetting(dialogValues.length, newValue)}
|
||||||
|
isDialogOpen={isAddItemDialogOpen}
|
||||||
|
setIsDialogOpen={setIsAddItemDialogOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Button, Dialog, DialogTitle, InputAdornment, ListItemText } from '@mui/material';
|
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
|
||||||
|
|
||||||
export type TextSettingProps = {
|
|
||||||
settingName: string;
|
|
||||||
dialogTitle?: string;
|
|
||||||
dialogDescription?: string;
|
|
||||||
value?: string;
|
|
||||||
handleChange: (value: string) => void;
|
|
||||||
isPassword?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TextSetting = ({
|
|
||||||
settingName,
|
|
||||||
dialogTitle = settingName,
|
|
||||||
dialogDescription,
|
|
||||||
value,
|
|
||||||
handleChange,
|
|
||||||
isPassword = false,
|
|
||||||
placeholder = '',
|
|
||||||
disabled = false,
|
|
||||||
}: TextSettingProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [dialogValue, setDialogValue] = useState(value ?? '');
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogValue(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const closeDialog = (resetValue: boolean = true) => {
|
|
||||||
if (resetValue) {
|
|
||||||
setDialogValue(value ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowPassword(false);
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSetting = () => {
|
|
||||||
closeDialog(false);
|
|
||||||
handleChange(dialogValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
|
|
||||||
<ListItemText
|
|
||||||
primary={settingName}
|
|
||||||
secondary={isPassword ? value?.replace(/./g, '*') : value ?? t('global.label.loading')}
|
|
||||||
secondaryTypographyProps={{
|
|
||||||
sx: { display: 'flex', flexDirection: 'column', wordWrap: 'break-word' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
|
|
||||||
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle sx={{ paddingLeft: 0 }}>{dialogTitle}</DialogTitle>
|
|
||||||
{!!dialogDescription && (
|
|
||||||
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
|
|
||||||
)}
|
|
||||||
<TextField
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
margin: 'auto',
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={dialogValue}
|
|
||||||
type={isPassword && !showPassword ? 'password' : 'text'}
|
|
||||||
onChange={(e) => setDialogValue(e.target.value)}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: isPassword ? (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label="toggle password visibility"
|
|
||||||
onClick={handleClickShowPassword}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
) : null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => closeDialog()} color="primary">
|
|
||||||
{t('global.button.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => updateSetting()} color="primary">
|
|
||||||
{t('global.button.ok')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
41
src/components/settings/text/TextSetting.tsx
Normal file
41
src/components/settings/text/TextSetting.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ListItemText } from '@mui/material';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TextSettingDialog, TextSettingDialogProps } from '@/components/settings/text/TextSettingDialog.tsx';
|
||||||
|
|
||||||
|
export type TextSettingProps = Omit<TextSettingDialogProps, 'isDialogOpen' | 'setIsDialogOpen'> & {
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextSetting = (props: TextSettingProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const { settingName, value, isPassword = false, disabled = false } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
|
||||||
|
<ListItemText
|
||||||
|
primary={settingName}
|
||||||
|
secondary={isPassword ? value?.replace(/./g, '*') : value ?? t('global.label.loading')}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
sx: { display: 'flex', flexDirection: 'column', wordWrap: 'break-word' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
<TextSettingDialog {...props} isDialogOpen={isDialogOpen} setIsDialogOpen={setIsDialogOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
113
src/components/settings/text/TextSettingDialog.tsx
Normal file
113
src/components/settings/text/TextSettingDialog.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button, Dialog, DialogTitle, InputAdornment } from '@mui/material';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export type TextSettingDialogProps = {
|
||||||
|
settingName: string;
|
||||||
|
dialogTitle?: string;
|
||||||
|
dialogDescription?: string;
|
||||||
|
value?: string;
|
||||||
|
handleChange: (value: string) => void;
|
||||||
|
isPassword?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
isDialogOpen: boolean;
|
||||||
|
setIsDialogOpen: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextSettingDialog = ({
|
||||||
|
settingName,
|
||||||
|
dialogTitle = settingName,
|
||||||
|
dialogDescription,
|
||||||
|
value,
|
||||||
|
handleChange,
|
||||||
|
isPassword = false,
|
||||||
|
placeholder = '',
|
||||||
|
isDialogOpen,
|
||||||
|
setIsDialogOpen,
|
||||||
|
}: TextSettingDialogProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [dialogValue, setDialogValue] = useState(value ?? '');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const closeDialog = (resetValue: boolean = true) => {
|
||||||
|
if (resetValue) {
|
||||||
|
setDialogValue(value ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowPassword(false);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSetting = () => {
|
||||||
|
closeDialog(false);
|
||||||
|
handleChange(dialogValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle sx={{ paddingLeft: 0 }}>{dialogTitle}</DialogTitle>
|
||||||
|
{!!dialogDescription && (
|
||||||
|
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
margin: 'auto',
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={dialogValue}
|
||||||
|
type={isPassword && !showPassword ? 'password' : 'text'}
|
||||||
|
onChange={(e) => setDialogValue(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: isPassword ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
) : null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => closeDialog()} color="primary">
|
||||||
|
{t('global.button.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => updateSetting()} color="primary">
|
||||||
|
{t('global.button.ok')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -243,6 +243,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"label": {
|
||||||
|
"invalid_url": "Invalid repository url"
|
||||||
|
}
|
||||||
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"description": "Add custom repositories from which extensions can be installed",
|
"description": "Add custom repositories from which extensions can be installed",
|
||||||
"title": "Custom repositories"
|
"title": "Custom repositories"
|
||||||
@@ -309,6 +314,7 @@
|
|||||||
"failed_to_save_changes": "Failed to save changes",
|
"failed_to_save_changes": "Failed to save changes",
|
||||||
"invalid_action": "This is not a valid action",
|
"invalid_action": "This is not a valid action",
|
||||||
"invalid_file_type": "Invalid filetype",
|
"invalid_file_type": "Invalid filetype",
|
||||||
|
"invalid_input": "Invalid input",
|
||||||
"update_failed": "Could not check for updates"
|
"update_failed": "Could not check for updates"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { ListItemLink } from '@/components/util/ListItemLink';
|
|||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||||
import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts';
|
import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { Progress } from '@/components/util/Progress.tsx';
|
import { Progress } from '@/components/util/Progress.tsx';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/text/TextSetting.tsx';
|
||||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
import { TimeSetting } from '@/components/settings/TimeSetting.tsx';
|
import { TimeSetting } from '@/components/settings/TimeSetting.tsx';
|
||||||
import { ServerSettings } from '@/typings.ts';
|
import { ServerSettings } from '@/typings.ts';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useContext, useEffect } from 'react';
|
|||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import { ListItem, ListItemText, Switch } from '@mui/material';
|
import { ListItem, ListItemText, Switch } from '@mui/material';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/text/TextSetting.tsx';
|
||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
||||||
import { MetadataServerSettingKeys, MetadataServerSettings, ServerSettings } from '@/typings.ts';
|
import { MetadataServerSettingKeys, MetadataServerSettings, ServerSettings } from '@/typings.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { useLocalStorage } from '@/util/useLocalStorage.tsx';
|
import { useLocalStorage } from '@/util/useLocalStorage.tsx';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/text/TextSetting.tsx';
|
||||||
import { ServerSettings as GqlServerSettings } from '@/typings.ts';
|
import { ServerSettings as GqlServerSettings } from '@/typings.ts';
|
||||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
import { MutableListSetting } from '@/components/settings/MutableListSetting.tsx';
|
import { MutableListSetting } from '@/components/settings/MutableListSetting.tsx';
|
||||||
@@ -136,6 +136,13 @@ export const ServerSettings = () => {
|
|||||||
handleChange={(repos) => updateSetting('extensionRepos', repos)}
|
handleChange={(repos) => updateSetting('extensionRepos', repos)}
|
||||||
values={serverSettings?.extensionRepos}
|
values={serverSettings?.extensionRepos}
|
||||||
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
|
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
|
||||||
|
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
|
||||||
|
validateItem={(repo) =>
|
||||||
|
!!repo.match(
|
||||||
|
/https:\/\/(?:www|raw)?(?:github|githubusercontent)\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/(.*))?\/?/g,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
invalidItemError={t('extension.settings.repositories.custom.error.label.invalid_url')}
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarC
|
|||||||
import { ServerSettings } from '@/typings.ts';
|
import { ServerSettings } from '@/typings.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { WebUIUpdateIntervalSetting } from '@/components/settings/webUI/WebUIUpdateIntervalSetting.tsx';
|
import { WebUIUpdateIntervalSetting } from '@/components/settings/webUI/WebUIUpdateIntervalSetting.tsx';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/text/TextSetting.tsx';
|
||||||
import {
|
import {
|
||||||
SelectSetting,
|
SelectSetting,
|
||||||
SelectSettingValue,
|
SelectSettingValue,
|
||||||
|
|||||||
Reference in New Issue
Block a user