Files
TriggerFrontend/src/pages/settings/components/Applications/Applications.tsx
Sean Edmunds d098753ac6 Style changes
2025-04-03 15:42:16 -05:00

369 lines
9.7 KiB
TypeScript

import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import {
Box,
Button,
Card,
Divider,
Grid,
GridCol,
Group,
Popover,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { textInputClasses } from 'styles';
import type { Application } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import SettingsHeader from '../Shared/SettingsHeader';
import AppSettingsItem from './AppSettingsItem';
import DebouncedApplication from './DebouncedApplication';
const ApplicationSettings = () => {
const defaultApplication: Application = {
id: null,
name: '',
url: '',
icon: '',
baseUrl: '',
filterName: '',
index: 0,
enabled: 1,
};
const appsEndpoint = 'api/apps';
const appsOrderEndpoint = 'api/appsOrder';
const { data: apps, isFetching: isAppsFetching } = useQueryAPI({
endpoint: appsEndpoint,
});
const [applicationData, setApplicationData] = useState<Application[]>([]);
const [applicationList, setApplicationList] = useState<Application[]>([]);
const { theme, isMobile } = useCommonHooks();
const [newApplication, setNewApplication] = useState<Application>(defaultApplication);
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [dragging, setDragging] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const reorder = (list: any, startIndex: any, endIndex: any) => {
const result = Array.from(list);
const [removed]: any = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
function onDragEnd(result: any) {
if (!result.destination) {
return;
}
if (result.destination.index === result.source.index) {
return;
}
const items = reorder(applicationData, result.source.index, result.destination.index);
setApplicationList(
items.map((item: any, index: number) => {
return {
...item,
index: index,
};
}),
);
}
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const handleClose = () => {
closeModal();
setNewApplication(defaultApplication);
};
useEffect(() => {
if (apps && !isAppsFetching) {
setApplicationData(
apps
.filter((a: Application) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.sort((a: Application, b: Application) => (a.index > b.index ? 1 : -1)),
);
}
}, [apps, isAppsFetching, debouncedSearch]);
useEffect(() => {
if (applicationList.length > 0 && applicationList !== applicationData) {
mutateAsync({
method: 'PATCH',
endpoint: appsOrderEndpoint,
body: applicationList,
});
}
}, [applicationList]);
return (
<Stack w={'100%'}>
<AddItemModal
modalOpened={modalOpened}
handleClose={handleClose}
title={'Add a new application'}
body={
<Stack gap={0} align='center'>
<Group justify='center'>
<TextInput
label={'Name'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.name}
onChange={(event) => {
setNewApplication({
...newApplication,
name: event.target.value,
});
}}
/>
<TextInput
label={'Base URL'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.baseUrl}
onChange={(event) => {
setNewApplication({
...newApplication,
baseUrl: event.target.value,
});
}}
/>
</Group>
<Group w={isMobile ? undefined : '60%'} justify='center'>
<TextInput
label={'URL'}
className={textInputClasses.settingsItem}
pt={16}
w={isMobile ? undefined : '91%'}
classNames={textInputClasses}
defaultValue={newApplication.url}
onChange={(event) => {
setNewApplication({
...newApplication,
url: event.target.value,
});
}}
/>
</Group>
<Group justify='center'>
<TextInput
label={'Icon'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.icon}
onChange={(event) => {
setNewApplication({
...newApplication,
icon: event.target.value,
});
}}
/>
<TextInput
label={'Filter'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.filterName}
onChange={(event) => {
setNewApplication({
...newApplication,
filterName: event.target.value,
});
}}
/>
</Group>
<Divider color='black' my='lg' />
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newApplication) {
mutateAsync({
method: 'POST',
endpoint: appsEndpoint,
body: {
...newApplication,
index: applicationData.length,
},
}).then(handleClose);
}
}}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
}
/>
<SettingsHeader
setDragging={setDragging}
openModal={openModal}
title={'Applications'}
tooltip={{
title: 'Applications are the apps that will be displayed on the homepage.',
highlightList: [
'Applications',
'apps',
'application',
'name',
'base url',
'url',
'icon',
'filter',
],
description:
'Applications contain a name, base url, url, icon and filter. Currently the icon must be the mdi icon name. URLs can contain either http(s):// or not. Base URLs should start with /, otherwise they will cause errors.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
/>
{dragging ? (
<DragDropContext onDragEnd={onDragEnd}>
<Stack
mt={isMobile ? '9rem' : '6rem'}
mb={isMobile ? '6rem' : '1rem'}
w={'100%'}
align='center'
>
<Droppable droppableId={'groupList'}>
{(provided) => (
<Stack
w={'75%'}
align='center'
justify='center'
ref={provided.innerRef}
{...provided.droppableProps}
>
{applicationData?.map((app: Application) => (
<Draggable
key={app.id}
draggableId={`${app.id}`}
index={Number(app.index)}
>
{(provided) => (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Card
w={'400px'}
style={{
backgroundColor: theme.searchColor,
border: '1px solid var(--color-border)',
borderRadius: '8px',
}}
>
<Group gap={0} align='center' justify='space-between'>
<Stack gap={0}>
<Text c={theme.accentColor} fz={14}>
Name
</Text>
<Text
p={4}
style={{
borderRadius: '4px',
paddingLeft: '8px',
color: theme.primaryColor,
backgroundColor: theme.searchColor,
}}
>
{app.name}
</Text>
</Stack>
<Stack gap={0}>
<Text c={theme.accentColor} fz={14}>
Filter Name
</Text>
<Text
p={4}
style={{
borderRadius: '4px',
paddingLeft: '8px',
color: theme.primaryColor,
backgroundColor: theme.searchColor,
}}
>
{app.filterName}
</Text>
</Stack>
</Group>
</Card>
</Box>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</Stack>
</DragDropContext>
) : (
<Stack
mt={isMobile ? '9rem' : '6rem'}
mb={isMobile ? '6rem' : '1rem'}
w={'100%'}
align='center'
>
<Grid columns={18} w={'75%'}>
{applicationData?.map((application: Application) => (
<GridCol span={6} key={application.id}>
<Popover
withArrow
withOverlay
withinPortal={false}
closeOnClickOutside={!isOpen}
>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<AppSettingsItem
url={application.url}
icon={application.icon}
name={application.name}
enabled={application.enabled}
/>
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedApplication
application={application}
setIsOpen={setIsOpen}
/>
</Popover.Dropdown>
</Popover>
</GridCol>
))}
</Grid>
</Stack>
)}
</Stack>
);
};
export default ApplicationSettings;