import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { Box, Button, type ComboboxItem, type ComboboxLikeRenderOptionInput, Grid, GridCol, Group, LoadingOverlay, Popover, Select, Stack, Text, TextInput, } from '@mantine/core'; import { isNotEmpty, useForm } from '@mantine/form'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { AddItemModal, AppItem, NoItemsFound, SettingsHeader } from 'components'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useEffect, useState } from 'react'; import * as fa6 from 'react-icons/fa6'; import * as si from 'react-icons/si'; import * as vsc from 'react-icons/vsc'; import { buttonClasses, popoverClasses, selectClasses, textInputClasses } from 'styles'; import type { Application } from 'types'; import DebouncedApplication from './DebouncedApplication'; export function Applications() { const allIcons: any = { ...fa6, ...vsc, ...si }; const { isMobile, reorder, theme, settings, updateSetting } = useCommonHooks(); const defaultApplication: Application = { id: null, name: '', url: '', icon: '', baseUrl: '', filterName: 'Default', index: 0, enabled: 1, }; const [dragging, setDragging] = useState(false); const [searchValue, setSearchValue] = useState(''); const [debouncedSearch] = useDebouncedValue(searchValue, 500); const form = useForm({ initialValues: defaultApplication, validate: { name: isNotEmpty('Name is required'), url: isNotEmpty('URL is required'), icon: isNotEmpty('Icon is required'), filterName: isNotEmpty('Filter name is required'), }, }); const appsEndpoint = 'api/applications'; const appsOrderEndpoint = 'api/applicationOrder'; const { data: applicationData, isFetching: applicationDataIsFetching } = useQueryAPI({ endpoint: appsEndpoint, }); const applications: Application[] = applicationData ?.filter( (a: Application) => a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.baseUrl.toLowerCase().includes((debouncedSearch as string).toLowerCase()), ) .sort((a: Application, b: Application) => { if (!dragging) { switch (settings.appSettingsSort) { case 'A>Z': return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1; case 'Z>A': return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? 1 : -1; case 'Newest': if (a.id && b.id) { return a.id < b.id ? 1 : -1; } return -1; case 'Oldest': if (a.id && b.id) { return a.id > b.id ? 1 : -1; } return -1; case 'Custom': return a.index > b.index ? 1 : -1; default: return 0; } } return a.index > b.index ? 1 : -1; }); const [applicationList, setApplicationList] = useState([]); const { mutateAsync } = useMutationAPI(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); function onDragEnd(result: any) { if (!result.destination) { return; } if (result.destination.index === result.source.index) { return; } const items = reorder(applications, 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(); form.reset(); }; useEffect(() => { if (debouncedSearch === searchValue) { setIsLoading(false); } }, [debouncedSearch]); useEffect(() => { if (applicationList.length > 0 && applicationList !== applications) { mutateAsync({ method: 'PATCH', endpoint: appsOrderEndpoint, body: applicationList, }).catch((error) => { console.error(error); }); } }, [applicationList]); return ( 0 ? setDragging : undefined} openModal={openModal} title={'Applications'} tooltip={{ title: 'Applications are the app buttons that will be displayed on the homepage.', highlightList: [ 'Applications', 'applicationData', 'application', 'app', 'apps', 'name', 'base url', 'url', 'icon', 'filter', ], description: 'Applications contain a name, base url, url, icon and filter. URLs can contain either http(s):// or not. Base URLs should start with /, otherwise they will cause errors.\n\nApplications that have been disabled will display a disabled icon on top of them.', }} searchValue={searchValue} setSearchValue={setSearchValue} searchPlaceholder={'Search Applications...'} isLoading={isLoading} setIsLoading={setIsLoading} sortBy={settings.appSettingsSort} setSortBy={(value) => { updateSetting('appSettingsSort', value); }} customSearchEnabled /> {modalOpened && ( { void mutateAsync({ method: 'POST', endpoint: appsEndpoint, body: { ...values, index: applications?.length, }, }); handleClose(); })} >