mirror of
https://github.com/responsively-org/responsively-app
synced 2024-11-10 14:54:12 +00:00
Add PreviewSuites, moved device manager to PreviewSuiteSelector
This commit is contained in:
parent
0c2de8f75a
commit
fcf4430bdf
10 changed files with 165 additions and 54 deletions
|
@ -17,6 +17,7 @@ interface Props {
|
|||
enableDnd?: boolean;
|
||||
moveDevice?: (device: Device, atIndex: number) => void;
|
||||
onShowDeviceDetails: (device: Device) => void;
|
||||
hideSelectionControls?: boolean;
|
||||
disableSelectionControls?: boolean;
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ const DeviceLabel = ({
|
|||
moveDevice = () => {},
|
||||
enableDnd = false,
|
||||
onShowDeviceDetails,
|
||||
hideSelectionControls = false,
|
||||
disableSelectionControls = false,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -72,9 +74,10 @@ const DeviceLabel = ({
|
|||
{enableDnd ? <Icon icon="ic:baseline-drag-indicator" /> : null}
|
||||
<input
|
||||
className={cx({
|
||||
'pointer-events-none opacity-0': disableSelectionControls,
|
||||
'pointer-events-none opacity-0': hideSelectionControls,
|
||||
})}
|
||||
type="checkbox"
|
||||
disabled={disableSelectionControls}
|
||||
checked={devices.find((d) => d.name === device.name) != null}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { addSuite } from 'renderer/store/features/device-manager';
|
||||
|
||||
import Button from '../../../Button';
|
||||
import Input from '../../../Input';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CreateSuiteModal = ({ isOpen, onClose }: Props) => {
|
||||
const [name, setName] = useState<string>('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAddSuite = async (): Promise<void> => {
|
||||
if (name === '') {
|
||||
// eslint-disable-next-line no-alert
|
||||
return alert(
|
||||
'Suite name cannot be empty. Please enter a name for the suite.'
|
||||
);
|
||||
}
|
||||
dispatch(addSuite({ id: uuidv4(), name, devices: ['10008'] }));
|
||||
return onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Add Suite">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-[420px] flex-col gap-2">
|
||||
<Input
|
||||
label="Suite Name"
|
||||
type="text"
|
||||
placeholder="My Custom Suite"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
<Button className="px-2" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="px-2" onClick={handleAddSuite} isActive>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from '@iconify/react';
|
||||
import { useState } from 'react';
|
||||
import Button from 'renderer/components/Button';
|
||||
import { CreateSuiteModal } from './CreateSuiteModal';
|
||||
|
||||
export const CreateSuiteButton = () => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div className="relative flex aspect-square h-full min-h-52 flex-shrink-0 flex-col items-center justify-center gap-4 rounded bg-white dark:bg-slate-900">
|
||||
<span className="absolute top-12">Add Suite</span>
|
||||
<Button
|
||||
className="aspect-square w-16 rounded-full"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Icon icon="mdi:plus" fontSize={30} />
|
||||
</Button>
|
||||
<CreateSuiteModal isOpen={open} onClose={() => setOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -28,7 +28,7 @@ export const Suite = ({ suite: { id, name, devices }, isActive }: Props) => {
|
|||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'relative flex-shrink-0 rounded bg-white dark:bg-slate-900',
|
||||
'relative min-w-56 flex-shrink-0 rounded bg-white dark:bg-slate-900',
|
||||
{
|
||||
'border-2 border-slate-500 ': isActive,
|
||||
}
|
||||
|
@ -51,7 +51,8 @@ export const Suite = ({ suite: { id, name, devices }, isActive }: Props) => {
|
|||
<DeviceLabel
|
||||
device={getDevicesMap()[deviceId]}
|
||||
onShowDeviceDetails={() => {}}
|
||||
disableSelectionControls={!isActive}
|
||||
hideSelectionControls={!isActive}
|
||||
disableSelectionControls={devices.length === 1}
|
||||
enableDnd={isActive}
|
||||
key={deviceId}
|
||||
moveDevice={moveDevice}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
selectActiveSuite,
|
||||
selectSuites,
|
||||
} from 'renderer/store/features/device-manager';
|
||||
import { CreateSuiteButton } from './CreateSuiteButton';
|
||||
import { Suite } from './Suite';
|
||||
|
||||
export const PreviewSuites = () => {
|
||||
|
@ -14,20 +15,17 @@ export const PreviewSuites = () => {
|
|||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-6 text-lg">Preview Suites</p>
|
||||
<div className="flex w-full gap-4 overflow-x-auto">
|
||||
{suites.map((suite) => (
|
||||
<Suite
|
||||
suite={suite}
|
||||
isActive={suite.id === activeSuite.id}
|
||||
key={suite.name}
|
||||
/>
|
||||
))}
|
||||
<div className="flex aspect-square h-full min-h-52 flex-shrink-0 flex-col items-center justify-center gap-4 rounded bg-white dark:bg-slate-900">
|
||||
Add Suite
|
||||
<Button className="aspect-square w-16 rounded-full">
|
||||
<Icon icon="mdi:plus" fontSize={30} />
|
||||
</Button>
|
||||
<div className="flex w-full items-center gap-4 overflow-x-auto">
|
||||
<div className="flex gap-4">
|
||||
{suites.map((suite) => (
|
||||
<Suite
|
||||
suite={suite}
|
||||
isActive={suite.id === activeSuite.id}
|
||||
key={suite.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<CreateSuiteButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -106,6 +106,10 @@ const DeviceManager = () => {
|
|||
device={device}
|
||||
key={device.name}
|
||||
onShowDeviceDetails={onShowDeviceDetails}
|
||||
disableSelectionControls={
|
||||
devices.find((d) => d.id === device.id) != null &&
|
||||
devices.length === 1
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{filteredDevices.length === 0 ? (
|
||||
|
|
|
@ -3,14 +3,20 @@ import { Icon } from '@iconify/react';
|
|||
import cx from 'classnames';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Separator {
|
||||
type: 'separator';
|
||||
}
|
||||
interface Option {
|
||||
type?: 'option';
|
||||
label: JSX.Element | string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
type OptionOrSeparator = Option | Separator;
|
||||
|
||||
interface Props {
|
||||
label: JSX.Element | string;
|
||||
options: Option[];
|
||||
options: OptionOrSeparator[];
|
||||
}
|
||||
|
||||
export function DropDown({ label, options }: Props) {
|
||||
|
@ -32,24 +38,32 @@ export function DropDown({ label, options }: Props) {
|
|||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right divide-y divide-slate-100 rounded-md bg-slate-100 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-slate-900">
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-fit origin-top-right divide-y divide-slate-100 rounded-md bg-slate-100 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-slate-900">
|
||||
<div className="px-1 py-1 ">
|
||||
{options.map((option) => (
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={cx(
|
||||
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
|
||||
{ 'bg-slate-200 dark:bg-slate-800': active }
|
||||
)}
|
||||
type="button"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
{options.map((option, idx) => {
|
||||
if (option.type === 'separator') {
|
||||
return (
|
||||
<div className="m-1 border-t-[1px] border-t-slate-500" />
|
||||
);
|
||||
}
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Menu.Item key={idx.toString()}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={cx(
|
||||
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
|
||||
{ 'bg-slate-200 dark:bg-slate-800': active }
|
||||
)}
|
||||
type="button"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
|
|
|
@ -20,15 +20,6 @@ const MenuFlyout = () => {
|
|||
<Devtools />
|
||||
<AllowInSecureSSL />
|
||||
<ClearHistory />
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(setAppView(APP_VIEWS.DEVICE_MANAGER));
|
||||
}}
|
||||
className="w-full !justify-start p-[1px] px-4 "
|
||||
subtle
|
||||
>
|
||||
Device Manager
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Icon } from '@iconify/react';
|
||||
import { active } from 'browser-sync';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { DropDown } from 'renderer/components/DropDown';
|
||||
import {
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
selectSuites,
|
||||
setActiveSuite,
|
||||
} from 'renderer/store/features/device-manager';
|
||||
import { APP_VIEWS, setAppView } from 'renderer/store/features/ui';
|
||||
|
||||
export const PreviewSuiteSelector = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -15,15 +15,30 @@ export const PreviewSuiteSelector = () => {
|
|||
return (
|
||||
<DropDown
|
||||
label={<Icon icon="heroicons:swatch" fontSize={18} />}
|
||||
options={suites.map((suite) => ({
|
||||
label: (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span>{suite.name}</span>
|
||||
{suite.id === activeSuite.id ? <Icon icon="mdi:check" /> : null}
|
||||
</div>
|
||||
),
|
||||
onClick: () => dispatch(setActiveSuite(suite.id)),
|
||||
}))}
|
||||
options={[
|
||||
...suites.map((suite) => ({
|
||||
label: (
|
||||
<div className="flex w-full items-center justify-between gap-12 whitespace-nowrap">
|
||||
<span>{suite.name}</span>
|
||||
{suite.id === activeSuite.id ? <Icon icon="mdi:check" /> : null}
|
||||
</div>
|
||||
),
|
||||
onClick: () => dispatch(setActiveSuite(suite.id)),
|
||||
})),
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex w-full flex-shrink-0 items-center justify-between gap-12 whitespace-nowrap">
|
||||
<span>Manage Suites</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => {
|
||||
dispatch(setAppView(APP_VIEWS.DEVICE_MANAGER));
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -60,11 +60,18 @@ export const deviceManagerSlice = createSlice({
|
|||
setActiveSuite(state, action: PayloadAction<string>) {
|
||||
state.activeSuite = action.payload;
|
||||
},
|
||||
addSuite(state, action: PayloadAction<PreviewSuite>) {
|
||||
const suites = window.electron.store.get('deviceManager.previewSuites');
|
||||
suites.push(action.payload);
|
||||
state.suites = suites;
|
||||
state.activeSuite = action.payload.id;
|
||||
window.electron.store.set('deviceManager.previewSuites', suites);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { setDevices, setSuiteDevices, setActiveSuite } =
|
||||
export const { setDevices, setSuiteDevices, setActiveSuite, addSuite } =
|
||||
deviceManagerSlice.actions;
|
||||
|
||||
export const selectSuites = (state: RootState) => state.deviceManager.suites;
|
||||
|
|
Loading…
Reference in a new issue