mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-18 03:01:09 +01:00
🔀 Merge Mantine v5.0 into Overseerr-integaration
This commit is contained in:
@@ -8,8 +8,9 @@ import {
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
ScrollArea,
|
||||
PasswordInput,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
Tabs,
|
||||
TextInput,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconApps as Apps } from '@tabler/icons';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconApps } from '@tabler/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
@@ -38,18 +40,18 @@ export function AddItemShelfButton(props: any) {
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} />
|
||||
</Modal>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
size="xl"
|
||||
color="blue"
|
||||
style={props.style}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<Tooltip label="Add a service">
|
||||
<Apps />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
<Tooltip withinPortal label="Add a service">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
size="xl"
|
||||
color="blue"
|
||||
style={props.style}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<IconApps />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -85,25 +87,26 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
// Extract all the categories from the services in config
|
||||
const categoryList = config.services.reduce((acc, cur) => {
|
||||
const InitialCategories = config.services.reduce((acc, cur) => {
|
||||
if (cur.category && !acc.includes(cur.category)) {
|
||||
acc.push(cur.category);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
const [categories, setCategories] = useState<string[]>(InitialCategories);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
id: props.id ?? uuidv4(),
|
||||
type: props.type ?? 'Other',
|
||||
category: props.category ?? undefined,
|
||||
category: props.category ?? null,
|
||||
name: props.name ?? '',
|
||||
icon: props.icon ?? DEFAULT_ICON,
|
||||
url: props.url ?? '',
|
||||
apiKey: props.apiKey ?? (undefined as unknown as string),
|
||||
username: props.username ?? (undefined as unknown as string),
|
||||
password: props.password ?? (undefined as unknown as string),
|
||||
openedUrl: props.openedUrl ?? (undefined as unknown as string),
|
||||
apiKey: props.apiKey ?? undefined,
|
||||
username: props.username ?? undefined,
|
||||
password: props.password ?? undefined,
|
||||
openedUrl: props.openedUrl ?? undefined,
|
||||
status: props.status ?? ['200'],
|
||||
newTab: props.newTab ?? true,
|
||||
},
|
||||
@@ -162,21 +165,21 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
</Center>
|
||||
<form
|
||||
onSubmit={form.onSubmit(() => {
|
||||
if (JSON.stringify(form.values.status) === JSON.stringify(['200'])) {
|
||||
form.values.status = undefined;
|
||||
}
|
||||
if (form.values.newTab === true) {
|
||||
form.values.newTab = undefined;
|
||||
const newForm = { ...form.values };
|
||||
if (newForm.newTab === true) newForm.newTab = undefined;
|
||||
if (newForm.category === null) newForm.category = undefined;
|
||||
if (newForm.status.length === 1 && newForm.status[0] === '200') {
|
||||
delete newForm.status;
|
||||
}
|
||||
// If service already exists, update it.
|
||||
if (config.services && config.services.find((s) => s.id === form.values.id)) {
|
||||
if (config.services && config.services.find((s) => s.id === newForm.id)) {
|
||||
setConfig({
|
||||
...config,
|
||||
// replace the found item by matching ID
|
||||
services: config.services.map((s) => {
|
||||
if (s.id === form.values.id) {
|
||||
if (s.id === newForm.id) {
|
||||
return {
|
||||
...form.values,
|
||||
...newForm,
|
||||
};
|
||||
}
|
||||
return s;
|
||||
@@ -185,159 +188,160 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
} else {
|
||||
setConfig({
|
||||
...config,
|
||||
services: [...config.services, form.values],
|
||||
services: [...config.services, newForm],
|
||||
});
|
||||
}
|
||||
setOpened(false);
|
||||
form.reset();
|
||||
})}
|
||||
>
|
||||
<Tabs grow>
|
||||
<Tabs.Tab label="Options">
|
||||
<ScrollArea style={{ height: 500 }} scrollbarSize={4}>
|
||||
<Group direction="column" grow>
|
||||
<TextInput
|
||||
required
|
||||
label="Service name"
|
||||
placeholder="Plex"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
required
|
||||
label="Icon URL"
|
||||
placeholder={DEFAULT_ICON}
|
||||
{...form.getInputProps('icon')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Service URL"
|
||||
placeholder="http://localhost:7575"
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
label="On Click URL"
|
||||
placeholder="http://sonarr.example.com"
|
||||
{...form.getInputProps('openedUrl')}
|
||||
/>
|
||||
<Select
|
||||
label="Service type"
|
||||
defaultValue="Other"
|
||||
placeholder="Pick one"
|
||||
required
|
||||
searchable
|
||||
data={ServiceTypeList}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Select
|
||||
label="Category"
|
||||
data={categoryList}
|
||||
placeholder="Select a category or create a new one"
|
||||
nothingFound="Nothing found"
|
||||
searchable
|
||||
clearable
|
||||
creatable
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
getCreateLabel={(query) => `+ Create "${query}"`}
|
||||
onCreate={(query) => {}}
|
||||
{...form.getInputProps('category')}
|
||||
/>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{(form.values.type === 'Sonarr' ||
|
||||
form.values.type === 'Radarr' ||
|
||||
form.values.type === 'Lidarr' ||
|
||||
form.values.type === 'Overseerr' ||
|
||||
form.values.type === 'Readarr') && (
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="API key"
|
||||
placeholder="Your API key"
|
||||
value={form.values.apiKey}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.apiKey && 'Invalid API key'}
|
||||
/>
|
||||
<Tip>
|
||||
Get your API key{' '}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
href={`${hostname}/settings/general`}
|
||||
>
|
||||
here.
|
||||
</Anchor>
|
||||
</Tip>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'qBittorrent' && (
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Deluge' && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Transmission' && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
/>
|
||||
<TextInput
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab label="Advanced Options">
|
||||
<Group direction="column" grow>
|
||||
<Tabs defaultValue="Options">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Options">Options</Tabs.Tab>
|
||||
<Tabs.Tab value="Advanced Options">Advanced options</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="Options">
|
||||
<Stack>
|
||||
<TextInput
|
||||
required
|
||||
label="Service name"
|
||||
placeholder="Plex"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Icon URL"
|
||||
placeholder={DEFAULT_ICON}
|
||||
{...form.getInputProps('icon')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Service URL"
|
||||
placeholder="http://localhost:7575"
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
label="On Click URL"
|
||||
placeholder="http://sonarr.example.com"
|
||||
{...form.getInputProps('openedUrl')}
|
||||
/>
|
||||
<Select
|
||||
label="Service type"
|
||||
defaultValue="Other"
|
||||
placeholder="Pick one"
|
||||
required
|
||||
searchable
|
||||
data={ServiceTypeList}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Select
|
||||
label="Category"
|
||||
data={categories}
|
||||
placeholder="Select a category or create a new one"
|
||||
nothingFound="Nothing found"
|
||||
searchable
|
||||
clearable
|
||||
creatable
|
||||
onCreate={(query) => {
|
||||
const item = { value: query, label: query };
|
||||
setCategories([...InitialCategories, query]);
|
||||
return item;
|
||||
}}
|
||||
getCreateLabel={(query) => `+ Create "${query}"`}
|
||||
{...form.getInputProps('category')}
|
||||
/>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{(form.values.type === 'Sonarr' ||
|
||||
form.values.type === 'Radarr' ||
|
||||
form.values.type === 'Lidarr' ||
|
||||
form.values.type === 'Readarr') && (
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="API key"
|
||||
placeholder="Your API key"
|
||||
value={form.values.apiKey}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.apiKey && 'Invalid API key'}
|
||||
/>
|
||||
<Tip>
|
||||
Get your API key{' '}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
href={`${hostname}/settings/general`}
|
||||
>
|
||||
here.
|
||||
</Anchor>
|
||||
</Tip>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'qBittorrent' && (
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Deluge' && (
|
||||
<>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Transmission' && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="Advanced Options">
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
required
|
||||
label="HTTP Status Codes"
|
||||
@@ -355,8 +359,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
defaultChecked={form.values.newTab}
|
||||
{...form.getInputProps('newTab')}
|
||||
/>
|
||||
</Group>
|
||||
</Tabs.Tab>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Group grow position="center" mt="xl">
|
||||
<Button type="submit">{props.message ?? 'Add service'}</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Accordion, createStyles, Grid, Group, Paper, useMantineColorScheme } from '@mantine/core';
|
||||
import { Accordion, Grid, Paper, Stack, useMantineColorScheme } from '@mantine/core';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -18,44 +18,22 @@ import { ModuleMenu, ModuleWrapper } from '../../modules/moduleWrapper';
|
||||
import { DownloadsModule } from '../../modules';
|
||||
import DownloadComponent from '../../modules/downloads/DownloadsModule';
|
||||
|
||||
const useStyles = createStyles((theme, _params) => ({
|
||||
item: {
|
||||
overflow: 'hidden',
|
||||
borderLeft: '3px solid transparent',
|
||||
borderRight: '3px solid transparent',
|
||||
borderBottom: '3px solid transparent',
|
||||
borderRadius: '20px',
|
||||
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
marginTop: theme.spacing.md,
|
||||
},
|
||||
|
||||
control: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
borderRadius: theme.spacing.md,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
},
|
||||
},
|
||||
|
||||
content: {
|
||||
margin: theme.spacing.md,
|
||||
},
|
||||
|
||||
label: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
}));
|
||||
|
||||
const AppShelf = (props: any) => {
|
||||
const { classes, cx } = useStyles(props);
|
||||
const [toggledCategories, settoggledCategories] = useLocalStorage({
|
||||
const { config, setConfig } = useConfig();
|
||||
// Extract all the categories from the services in config
|
||||
const categoryList = config.services.reduce((acc, cur) => {
|
||||
if (cur.category && !acc.includes(cur.category)) {
|
||||
acc.push(cur.category);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
const [toggledCategories, setToggledCategories] = useLocalStorage({
|
||||
key: 'app-shelf-toggled',
|
||||
// This is a bit of a hack to get the 5 first categories to be toggled on by default
|
||||
defaultValue: { 0: true, 1: true, 2: true, 3: true, 4: true } as Record<string, boolean>,
|
||||
// This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories
|
||||
defaultValue: categoryList,
|
||||
});
|
||||
const [activeId, setActiveId] = useState(null);
|
||||
const { config, setConfig } = useConfig();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -93,15 +71,8 @@ const AppShelf = (props: any) => {
|
||||
|
||||
setActiveId(null);
|
||||
}
|
||||
// Extract all the categories from the services in config
|
||||
const categoryList = config.services.reduce((acc, cur) => {
|
||||
if (cur.category && !acc.includes(cur.category)) {
|
||||
acc.push(cur.category);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
const item = (filter?: string) => {
|
||||
const getItems = (filter?: string) => {
|
||||
// If filter is not set, return all the services without a category or a null category
|
||||
let filtered = config.services;
|
||||
if (!filter) {
|
||||
@@ -155,54 +126,62 @@ const AppShelf = (props: any) => {
|
||||
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
|
||||
// Create an item with 0: true, 1: true, 2: true... For each category
|
||||
return (
|
||||
// Return one item for each category
|
||||
<Group grow direction="column">
|
||||
// TODO: Style accordion so that the bar is transparent to the user settings
|
||||
<Stack>
|
||||
<Accordion
|
||||
disableIconRotation
|
||||
classNames={classes}
|
||||
variant="separated"
|
||||
radius="lg"
|
||||
order={2}
|
||||
iconPosition="right"
|
||||
multiple
|
||||
initialState={toggledCategories}
|
||||
onChange={(idx) => settoggledCategories(idx)}
|
||||
value={toggledCategories}
|
||||
onChange={(state) => {
|
||||
setToggledCategories(state);
|
||||
}}
|
||||
>
|
||||
{categoryList.map((category, idx) => (
|
||||
<Accordion.Item key={category} label={category}>
|
||||
{item(category)}
|
||||
<Accordion.Item key={category} value={idx.toString()}>
|
||||
<Accordion.Control>{category}</Accordion.Control>
|
||||
<Accordion.Panel>{getItems(category)}</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
{/* Return the item for all services without category */}
|
||||
{noCategory && noCategory.length > 0 ? (
|
||||
<Accordion.Item key="Other" label="Other">
|
||||
{item()}
|
||||
<Accordion.Item key="Other" value="Other">
|
||||
<Accordion.Control>Other</Accordion.Control>
|
||||
<Accordion.Panel>{getItems()}</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
) : null}
|
||||
{downloadEnabled ? (
|
||||
<Accordion.Item key="Downloads" label="Your downloads">
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
|
||||
<Accordion.Item key="Downloads" value="Your downloads">
|
||||
<Accordion.Control>Your downloads</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
|
||||
${(config.settings.appOpacity || 100) / 100}`,
|
||||
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
|
||||
borderColor: `rgba(${
|
||||
colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'
|
||||
} \
|
||||
${(config.settings.appOpacity || 100) / 100}`,
|
||||
}}
|
||||
>
|
||||
<ModuleMenu module={DownloadsModule} />
|
||||
<DownloadComponent />
|
||||
</Paper>
|
||||
}}
|
||||
>
|
||||
<ModuleMenu module={DownloadsModule} />
|
||||
<DownloadComponent />
|
||||
</Paper>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
) : null}
|
||||
</Accordion>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Group grow direction="column">
|
||||
{item()}
|
||||
<Stack>
|
||||
{getItems()}
|
||||
<ModuleWrapper mt="xl" module={DownloadsModule} />
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Card,
|
||||
Anchor,
|
||||
AspectRatio,
|
||||
Image,
|
||||
Center,
|
||||
createStyles,
|
||||
useMantineColorScheme,
|
||||
@@ -12,6 +11,7 @@ import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import Image from 'next/image';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import PingComponent from '../../modules/ping/PingModule';
|
||||
import AppShelfMenu from './AppShelfMenu';
|
||||
@@ -121,11 +121,13 @@ export function AppShelfItem(props: any) {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
styles={{ root: { cursor: 'pointer' } }}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
src={service.icon}
|
||||
fit="contain"
|
||||
src={`/api/imageproxy?url=${service.icon}`}
|
||||
objectFit="contain"
|
||||
onClick={() => {
|
||||
if (service.openedUrl) {
|
||||
window.open(service.openedUrl, service.newTab === false ? '_top' : '_blank');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||
import { ActionIcon, Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useState } from 'react';
|
||||
import { IconCheck as Check, IconEdit as Edit, IconTrash as Trash } from '@tabler/icons';
|
||||
import { IconCheck as Check, IconEdit as Edit, IconMenu, IconTrash as Trash } from '@tabler/icons';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import { AddAppShelfItemForm } from './AddAppShelfItem';
|
||||
@@ -23,49 +23,59 @@ export default function AppShelfMenu(props: any) {
|
||||
<AddAppShelfItemForm setOpened={setOpened} {...service} message="Save service" />
|
||||
</Modal>
|
||||
<Menu
|
||||
position="right"
|
||||
radius="md"
|
||||
withinPortal
|
||||
width={150}
|
||||
shadow="xl"
|
||||
withArrow
|
||||
radius="md"
|
||||
position="right"
|
||||
styles={{
|
||||
body: {
|
||||
dropdown: {
|
||||
// Add shadow and elevation to the body
|
||||
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item
|
||||
color="primary"
|
||||
icon={<Edit />}
|
||||
// TODO: #2 Add the ability to edit the service.
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
<Menu.Label>Danger zone</Menu.Label>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
onClick={(e: any) => {
|
||||
setConfig({
|
||||
...config,
|
||||
services: config.services.filter((s) => s.id !== service.id),
|
||||
});
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
title: (
|
||||
<Text>
|
||||
Service <b>{service.name}</b> removed successfully!
|
||||
</Text>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <Check />,
|
||||
message: undefined,
|
||||
});
|
||||
}}
|
||||
icon={<Trash />}
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
<Menu.Target>
|
||||
<ActionIcon style={{}}>
|
||||
<IconMenu />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item
|
||||
color="primary"
|
||||
icon={<Edit />}
|
||||
// TODO: #2 Add the ability to edit the service.
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
<Menu.Label>Danger zone</Menu.Label>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
onClick={(e: any) => {
|
||||
setConfig({
|
||||
...config,
|
||||
services: config.services.filter((s) => s.id !== service.id),
|
||||
});
|
||||
showNotification({
|
||||
autoClose: 5000,
|
||||
title: (
|
||||
<Text>
|
||||
Service <b>{service.name}</b> removed successfully!
|
||||
</Text>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <Check />,
|
||||
message: undefined,
|
||||
});
|
||||
}}
|
||||
icon={<Trash />}
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user