This commit is contained in:
violetadev 2024-07-29 22:04:14 +02:00
parent df626db4be
commit 5a849162d8
9 changed files with 308 additions and 81 deletions

View file

@ -0,0 +1,7 @@
export const Accordion = ({ children }: { children: JSX.Element }) => {
return (
<div id="accordion-open" data-accordion="open">
{children}
</div>
);
};

View file

@ -0,0 +1,54 @@
import { useState } from 'react';
type AccordionItemProps = {
title: string;
children: JSX.Element;
};
export const AccordionItem = ({ title, children }: AccordionItemProps) => {
const [isOpen, setIsOpen] = useState(true);
const toggle = () => {
setIsOpen(!isOpen);
};
return (
<div>
<h2>
<button
type="button"
className="flex w-full items-center justify-between gap-3 border border-gray-200 p-5 font-medium text-gray-500 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:ring-gray-800"
onClick={toggle}
aria-expanded={isOpen}
aria-controls={`accordion-body-${title}`}
>
<span className="flex items-center">{title}</span>
<svg
className={`h-3 w-3 ${isOpen ? 'rotate-180' : ''} shrink-0`}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5 5 1 1 5"
/>
</svg>
</button>
</h2>
<div
id={`accordion-body-${title}`}
className={`${isOpen ? 'block' : 'hidden'}`}
aria-labelledby={`accordion-heading-${title}`}
>
<div className="border border-b-0 border-gray-200 p-5 dark:border-gray-700 dark:bg-gray-900">
{children}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,2 @@
export { Accordion } from './Accordion';
export { AccordionItem } from './AccordionItem';

View file

@ -0,0 +1,62 @@
import { useEffect, useState } from 'react';
import Button from '../Button';
import Modal from '../Modal';
export const ConfirmDialog = ({
onClose,
onConfirm,
open,
confirmText,
}: {
onClose?: () => void;
onConfirm?: () => void;
open: boolean;
confirmText?: string;
}) => {
const [isOpen, setIsOpen] = useState(open);
useEffect(() => {
setIsOpen(open);
}, [open]);
const handleClose = () => {
if (onClose) {
onClose();
}
setIsOpen(false);
};
const handleConfirm = () => {
if (onConfirm) {
onConfirm();
}
setIsOpen(false);
};
return (
<Modal isOpen={isOpen} onClose={handleClose}>
<div
data-testid="confirm-dialog"
className="mb-6 flex h-full w-full flex-col flex-wrap items-center justify-center bg-opacity-95"
>
<h2 className="m-4 text-center text-2xl font-bold text-white">
<p>{confirmText || 'Are you sure?'}</p>
</h2>
<div className="m-4 flex justify-center">
<Button
onClick={handleConfirm}
className="me-2 mr-4 mb-2 rounded-lg bg-gray-800 px-5 py-2.5 text-sm font-medium text-white hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
>
Confirm
</Button>
<Button
onClick={handleClose}
className="me-2 mb-2 rounded-lg border border-gray-300 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 "
>
Cancel
</Button>
</div>
</div>
</Modal>
);
};

View file

@ -3,14 +3,20 @@ import Button from 'renderer/components/Button';
import { useState } from 'react'; import { useState } from 'react';
import { FileUploader } from 'renderer/components/FileUploader'; import { FileUploader } from 'renderer/components/FileUploader';
import Modal from 'renderer/components/Modal'; import Modal from 'renderer/components/Modal';
import { addSuites } from 'renderer/store/features/device-manager'; import {
addSuites,
deleteAllSuites,
} from 'renderer/store/features/device-manager';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { ConfirmDialog } from 'renderer/components/ConfirmDialog';
import { transformFile } from './utils'; import { transformFile } from './utils';
import { onFileDownload, setCustomDevices } from './helpers'; import { onFileDownload, setCustomDevices } from './helpers';
import { ManageSuitesToolError } from './ManageSuitesToolError'; import { ManageSuitesToolError } from './ManageSuitesToolError';
export const ManageSuitesTool = ({ setCustomDevicesState }: any) => { export const ManageSuitesTool = ({ setCustomDevicesState }: any) => {
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [resetConfirmation, setResetConfirmation] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -39,32 +45,48 @@ export const ManageSuitesTool = ({ setCustomDevicesState }: any) => {
setOpen(false); setOpen(false);
}; };
const clearCustomDevices = () => {
window.electron.store.set('deviceManager.customDevices', []);
setCustomDevicesState([]);
};
const onReset = () => {
dispatch(deleteAllSuites());
clearCustomDevices();
setResetConfirmation(false);
};
return ( return (
<> <>
<div className="flex flex-row content-end justify-end"> <div className="flex flex-row content-end justify-end">
<Button
data-testid="upload-btn"
className="aspect-square w-12 rounded-full hover:!bg-slate-500"
onClick={() => setOpen(true)}
>
<Icon
icon="mdi:folder-upload"
fontSize={18}
onClick={() => setOpen(true)}
/>
</Button>
<Button <Button
data-testid="download-btn" data-testid="download-btn"
className="aspect-square w-12 rounded-full hover:!bg-slate-500" className="aspect-square w-12 rounded-full hover:!bg-slate-500"
onClick={() => setOpen(true)}
>
<Icon icon="uil:export" fontSize={18} />
</Button>
<Button
data-testid="upload-btn"
className="aspect-square w-12 rounded-full hover:!bg-slate-500"
onClick={onFileDownload} onClick={onFileDownload}
> >
<Icon <Icon icon="uil:import" fontSize={18} />
icon="mdi:folder-download" </Button>
fontSize={18} <Button
onClick={onFileDownload} data-testid="reset-btn"
/> className="aspect-square w-12 rounded-full hover:!bg-slate-500"
onClick={() => setResetConfirmation(true)}
>
<Icon icon="uil:redo" fontSize={18} />
</Button> </Button>
</div> </div>
<ConfirmDialog
onConfirm={onReset}
onClose={() => setResetConfirmation(false)}
open={resetConfirmation}
confirmText="Do you want to reset all settings?"
/>
<Modal <Modal
isOpen={open} isOpen={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
@ -78,7 +100,9 @@ export const ManageSuitesTool = ({ setCustomDevicesState }: any) => {
/> />
<div className="text-align align-items-center flex flex-row flex-nowrap text-orange-500"> <div className="text-align align-items-center flex flex-row flex-nowrap text-orange-500">
<Icon icon="mdi:alert" /> <Icon icon="mdi:alert" />
<p className="pl-2">Importing will replace all current settings.</p> <p className="pl-2">
Duplicated imports will replace existing suites or custom devices.
</p>
</div> </div>
{error && <ManageSuitesToolError onClose={onErrorClose} />} {error && <ManageSuitesToolError onClose={onErrorClose} />}
</> </>

View file

@ -0,0 +1,37 @@
{
"customDevices": [
{
"id": "123",
"name": "a new test",
"width": 400,
"height": 600,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
"typse": "phone",
"dpxxi": 1,
"isTouchCapable": true,
"isMobileCapable": true,
"capabilities": [
"touch",
"mobile"
],
"isCustom": true
}
],
"suites": [
{
"id": "default",
"name": "Default",
"devices": [
"10008"
]
},
{
"id": "a4c142fc-debd-4eaa-beba-aef60093151c",
"name": "my custom suite",
"devices": [
"10008",
"30014"
]
}
]
}

View file

@ -18,12 +18,6 @@ export const PreviewSuites = () => {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className=" mb-6 flex w-full flex-row items-center justify-between gap-2 text-lg">
<div className="align-items-center flex flex-row justify-center">
<Icon icon="heroicons:swatch" />{' '}
<p className="pl-2">Preview Suites</p>
</div>
</div>
<div className="flex w-full items-center gap-4 overflow-x-auto"> <div className="flex w-full items-center gap-4 overflow-x-auto">
<div className="flex flex-shrink-0 gap-4"> <div className="flex flex-shrink-0 gap-4">
{suites.map((suite) => ( {suites.map((suite) => (

View file

@ -17,6 +17,7 @@ import DeviceDetailsModal from './DeviceDetailsModal';
import { PreviewSuites } from './PreviewSuites'; import { PreviewSuites } from './PreviewSuites';
import { ManageSuitesTool } from './PreviewSuites/ManageSuitesTool/ManageSuitesTool'; import { ManageSuitesTool } from './PreviewSuites/ManageSuitesTool/ManageSuitesTool';
import { Divider } from '../Divider'; import { Divider } from '../Divider';
import { AccordionItem, Accordion } from '../Accordion';
const filterDevices = (devices: Device[], filter: string) => { const filterDevices = (devices: Device[], filter: string) => {
const sanitizedFilter = filter.trim().toLowerCase(); const sanitizedFilter = filter.trim().toLowerCase();
@ -35,7 +36,7 @@ const DeviceManager = () => {
); );
const dispatch = useDispatch(); const dispatch = useDispatch();
const activeSuite = useSelector(selectActiveSuite); const activeSuite = useSelector(selectActiveSuite);
const devices = activeSuite.devices.map((id) => getDevicesMap()[id]); const devices = activeSuite.devices?.map((id) => getDevicesMap()[id]);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [filteredDevices, setFilteredDevices] = const [filteredDevices, setFilteredDevices] =
useState<Device[]>(defaultDevices); useState<Device[]>(defaultDevices);
@ -89,18 +90,27 @@ const DeviceManager = () => {
return ( return (
<div className="mx-auto flex w-4/5 flex-col gap-4 rounded-lg p-8"> <div className="mx-auto flex w-4/5 flex-col gap-4 rounded-lg p-8">
<div className="flex w-full text-3xl"> <div className="flex w-full justify-end text-3xl">
<span className="w-full text-left">Device Manager</span>
<Button onClick={() => dispatch(setAppView(APP_VIEWS.BROWSER))}> <Button onClick={() => dispatch(setAppView(APP_VIEWS.BROWSER))}>
<Icon icon="ic:round-close" /> <Icon icon="ic:round-close" fontSize={18} />
</Button> </Button>
</div> </div>
<div className=""> <div>
<div className="flex items-center justify-end justify-between ">
<h2 className="text-2xl font-bold">Device Manager</h2>
<ManageSuitesTool setCustomDevicesState={setCustomDevices} /> <ManageSuitesTool setCustomDevicesState={setCustomDevices} />
</div>
<Divider /> <Divider />
<Accordion>
<AccordionItem title="MANAGE SUITES">
<PreviewSuites /> <PreviewSuites />
</AccordionItem>
</Accordion>
<Divider /> <Divider />
<div className="my-4 flex items-center justify-end "> <div className="my-4 flex items-start justify-end justify-between">
<div className="flex w-fit flex-col items-start px-1">
<h2 className="text-2xl font-bold">Manage Devices</h2>
</div>
<div className="flex w-fit items-center bg-white px-1 dark:bg-slate-900"> <div className="flex w-fit items-center bg-white px-1 dark:bg-slate-900">
<Icon icon="ic:outline-search" height={24} /> <Icon icon="ic:outline-search" height={24} />
<input <input
@ -111,9 +121,9 @@ const DeviceManager = () => {
/> />
</div> </div>
</div> </div>
<div className="mt-8 mb-6 flex justify-between"> <Accordion>
<div className="text-lg ">Predefined Devices</div> <>
</div> <AccordionItem title="DEFAULT DEVICES">
<div className="ml-4 flex flex-row flex-wrap gap-4"> <div className="ml-4 flex flex-row flex-wrap gap-4">
{filteredDevices.map((device) => ( {filteredDevices.map((device) => (
<DeviceLabel <DeviceLabel
@ -133,13 +143,8 @@ const DeviceManager = () => {
</div> </div>
) : null} ) : null}
</div> </div>
<div className="mt-8 mb-6 flex justify-between"> </AccordionItem>
<div className="text-lg ">Custom Devices</div> <AccordionItem title="CUSTOM DEVICES">
<Button onClick={() => setIsDetailsModalOpen(true)} isActive>
<Icon icon="ic:baseline-add" />
Add Custom Device
</Button>
</div>
<div className="ml-4 flex flex-row flex-wrap gap-4"> <div className="ml-4 flex flex-row flex-wrap gap-4">
{filteredCustomDevices.map((device) => ( {filteredCustomDevices.map((device) => (
<DeviceLabel <DeviceLabel
@ -149,17 +154,37 @@ const DeviceManager = () => {
/> />
))} ))}
{customDevices.length === 0 ? ( {customDevices.length === 0 ? (
<div className="m-10 flex w-full items-center justify-center"> <div className="m-10 flex w-full flex-col items-center justify-center">
No custom devices added yet! <span>No custom devices added yet!</span>
<Button
className="m-4 rounded-l"
onClick={() => setIsDetailsModalOpen(true)}
isActive
>
<Icon icon="ic:baseline-add" />
<span className="pr-2 pl-2">Add Custom Device</span>
</Button>
</div> </div>
) : null} ) : null}
{customDevices.length > 0 && filteredCustomDevices.length === 0 ? ( {customDevices.length > 0 &&
filteredCustomDevices.length === 0 ? (
<div className="m-10 flex w-full items-center justify-center"> <div className="m-10 flex w-full items-center justify-center">
Sorry, no matching devices found. Sorry, no matching devices found.
<Icon icon="mdi:emoticon-sad-outline" className="ml-1" /> <Icon icon="mdi:emoticon-sad-outline" className="ml-1" />
</div> </div>
) : null} ) : null}
<Button
className={customDevices.length < 1 ? 'hidden' : 'rounded-l'}
onClick={() => setIsDetailsModalOpen(true)}
isActive
>
<Icon icon="ic:baseline-add" />
<span className="pr-2 pl-2">Add Custom Device</span>
</Button>
</div> </div>
</AccordionItem>
</>
</Accordion>
</div> </div>
<DeviceDetailsModal <DeviceDetailsModal
onSaveDevice={onSaveDevice} onSaveDevice={onSaveDevice}

View file

@ -71,10 +71,24 @@ export const deviceManagerSlice = createSlice({
window.electron.store.set('deviceManager.previewSuites', suites); window.electron.store.set('deviceManager.previewSuites', suites);
}, },
addSuites(state, action: PayloadAction<PreviewSuite[]>) { addSuites(state, action: PayloadAction<PreviewSuite[]>) {
window.electron.store.set('deviceManager.previewSuites', []); const existingSuites = window.electron.store.get(
state.suites = action.payload; 'deviceManager.previewSuites'
);
const suitesMap = new Map();
action.payload.forEach((suite) => suitesMap.set(suite.name, suite));
existingSuites.forEach((suite: PreviewSuite) => {
if (!suitesMap.has(suite.name)) {
suitesMap.set(suite.name, suite);
}
});
const mergedSuites = Array.from(suitesMap.values());
state.suites = mergedSuites;
state.activeSuite = action.payload[0].id; state.activeSuite = action.payload[0].id;
window.electron.store.set('deviceManager.previewSuites', action.payload); window.electron.store.set('deviceManager.previewSuites', mergedSuites);
}, },
deleteSuite(state, action: PayloadAction<string>) { deleteSuite(state, action: PayloadAction<string>) {
const suites: PreviewSuite[] = window.electron.store.get( const suites: PreviewSuite[] = window.electron.store.get(
@ -89,6 +103,13 @@ export const deviceManagerSlice = createSlice({
state.activeSuite = suites[0].id; state.activeSuite = suites[0].id;
window.electron.store.set('deviceManager.previewSuites', suites); window.electron.store.set('deviceManager.previewSuites', suites);
}, },
deleteAllSuites(state) {
const suites: PreviewSuite[] = window.electron.store.get(
'deviceManager.previewSuites'
);
window.electron.store.set('deviceManager.previewSuites', []);
state.suites = [];
},
}, },
}); });
@ -98,8 +119,9 @@ export const {
setSuiteDevices, setSuiteDevices,
setActiveSuite, setActiveSuite,
addSuite, addSuite,
deleteSuite,
addSuites, addSuites,
deleteSuite,
deleteAllSuites,
} = deviceManagerSlice.actions; } = deviceManagerSlice.actions;
export const selectSuites = (state: RootState) => state.deviceManager.suites; export const selectSuites = (state: RootState) => state.deviceManager.suites;