Skip to content

Commit

Permalink
feat(modals:install): add instances
Browse files Browse the repository at this point in the history
  • Loading branch information
virtual-designer committed Feb 22, 2024
1 parent 6b8332f commit f316e66
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 33 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"recoil": "^0.7.7",
"semver": "^7.6.0",
"sharp": "^0.33.2",
"tailwind-merge": "^2.2.1",
"uuid": "^9.0.1"
},
"devDependencies": {
Expand All @@ -48,4 +49,4 @@
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
}
1 change: 1 addition & 0 deletions src/app/recoil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function RecoilProvider({ children }: PropsWithChildren) {
] as LocalStorageSystemInstance[],
downloadModalOpen: false,
installModalOpen: false,
selectedInstanceIds: [] as string[],
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/atoms/ExtensionPageAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ExtensionPageState = atom({
url: "https://whatever.org",
},
] as LocalStorageSystemInstance[],
selectedInstanceIds: [] as string[],
},
});

Expand Down
31 changes: 31 additions & 0 deletions src/components/Button/ThinButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ComponentProps, FC, JSX, ReactNode } from "react";
import { twMerge } from "tailwind-merge";

type Props<E extends keyof JSX.IntrinsicElements | FC> = {
as?: E;
children?: ReactNode;
className?: string;
} & ComponentProps<E>;

const ThinButton = <E extends keyof JSX.IntrinsicElements | FC = "button">({
as = "button" as E,
children,
className = "",
...props
}: Props<E>) => {
const Component = as as FC<typeof props>;

return (
<Component
{...props}
className={twMerge(
"px-4 py-1 text-white bg-blue-600 rounded hover:bg-blue-700 focus:outline-blue-700 focus:outline-2 focus:outline-double focus:outline-offset-2 disabled:cursor-not-allowed disabled:bg-blue-800 disabled:hover:bg-blue-800 disabled:focus:outline-blue-800 disabled:focus:outline-2 disabled:focus:outline-double disabled:focus:outline-offset-2 disabled:disabled",
className
)}
>
{children}
</Component>
);
};

export default ThinButton;
59 changes: 51 additions & 8 deletions src/components/Extension/ExtensionInstallChooseInstances.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,67 @@
import ExtensionPageState from "@/atoms/ExtensionPageAtom";
import { Box } from "@mui/material";
import { FC } from "react";
import { FC, useState } from "react";
import { MdError } from "react-icons/md";
import { useRecoilValue } from "recoil";
import InstallAddForm from "../Instance/InstanceAddForm";
import Instances from "../Instance/Instances";
import { StepProps } from "./ExtensionInstallModal";

interface ExtensionInstallationChooseInstancesProps {}
const ExtensionInstallationChooseInstances: FC<StepProps> = ({
setNextValidator,
}) => {
const state = useRecoilValue(ExtensionPageState);
const [isValid, setIsValid] = useState(true);
const [addingInstance, setAddingInstance] = useState(false);

setNextValidator(() => {
const valid = state.selectedInstanceIds.length > 0;

if (valid !== isValid) {
setIsValid(valid);
}

return valid;
});

const ExtensionInstallationChooseInstances: FC<
ExtensionInstallationChooseInstancesProps
> = () => {
return (
<Box
sx={{
pt: 2,
}}
>
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
Choose the instances you want to install the extension on.
</p>
<div className="md:flex justify-between items-center">
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
Choose the instances you want to install the extension on.
You can{" "}
<a
href="#"
className="text-blue-500 hover:underline hover:text-blue-600"
onClick={() => setAddingInstance(true)}
>
add more instances
</a>{" "}
if you want.
</p>
</div>
<br />

{!isValid && (
<p className="text-xs text-red-500 dark:text-red-400 flex items-center gap-1">
<MdError />
{state.instances.length === 0
? "You must add at least one instance."
: "You must select at least one instance."}
</p>
)}

<Instances />

{addingInstance && (
<InstallAddForm onClose={() => setAddingInstance(false)} />
)}

<div className="pb-7"></div>
</Box>
);
};
Expand Down
60 changes: 43 additions & 17 deletions src/components/Extension/ExtensionInstallModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
import { FC, useRef, useState } from "react";
import { MdClose } from "react-icons/md";
import { useRecoilState } from "recoil";
import DeployedCodeUpdate from "../Icons/DeployedCodeUpdate";
Expand All @@ -29,17 +29,32 @@ const components = [
() => <p>Finish</p>,
];

type NextValidator = () => boolean | Promise<boolean>;

export type StepProps = {
setNextValidator: (validator: NextValidator) => void;
};

export default function ExtensionInstallModal({ extension }: Props) {
const [state, setState] = useRecoilState(ExtensionPageState);
const [activeStep, setActiveStep] = useState(0);
const [skipped, setSkipped] = useState(new Set<number>());
const nextValidatorRef = useRef<NextValidator | null>(null);
const onClose = () => {
setState((state) => ({ ...state, installModalOpen: false }));
setState((state) => ({
...state,
installModalOpen: false,
selectedInstanceIds: [],
}));
setActiveStep(0);
setSkipped(new Set<number>());
};

const StepComponent = components[activeStep];
const setNextValidator = (validator: NextValidator) => {
nextValidatorRef.current = validator;
};

const StepComponent = components[activeStep] as FC<StepProps>;

const isStepOptional = (step: number) => {
return false;
Expand All @@ -49,18 +64,29 @@ export default function ExtensionInstallModal({ extension }: Props) {
return skipped.has(step);
};

const handleNext = () => {
const handleNext = async () => {
let newSkipped = skipped;

if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}

if (nextValidatorRef.current) {
const valid = await nextValidatorRef.current();

if (!valid) {
return;
}
}

nextValidatorRef.current = null;
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};

const handleBack = () => {
nextValidatorRef.current = null;
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};

Expand All @@ -69,6 +95,7 @@ export default function ExtensionInstallModal({ extension }: Props) {
throw new Error("You can't skip a step that isn't optional.");
}

nextValidatorRef.current = null;
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
Expand All @@ -77,10 +104,6 @@ export default function ExtensionInstallModal({ extension }: Props) {
});
};

const handleReset = () => {
setActiveStep(0);
};

return (
<Modal
open={state.installModalOpen}
Expand Down Expand Up @@ -142,18 +165,15 @@ export default function ExtensionInstallModal({ extension }: Props) {
</Typography>
</>
) : (
<StepComponent />
<StepComponent
setNextValidator={setNextValidator}
/>
)}
</Box>

<div className="rounded-b-lg flex justify-end gap-4 p-4 absolute bottom-0 left-0 w-[100%] z-[10000] bg-white dark:bg-[#222] [box-shadow:0_-1px_1px_0_rgba(0,0,0,0.1)] dark:[box-shadow:0_0_1px_0_rgba(255,255,255,0.4)]">
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
<Button color="inherit" onClick={onClose}>
Cancel
</Button>
<Box sx={{ flex: "1 1 auto" }} />
{isStepOptional(activeStep) && (
Expand All @@ -165,7 +185,13 @@ export default function ExtensionInstallModal({ extension }: Props) {
Skip
</Button>
)}
<Button onClick={onClose}>Cancel</Button>{" "}
<Button
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>{" "}
<Button onClick={handleNext} variant="outlined">
{activeStep === steps.length - 1
? "Finish"
Expand Down
22 changes: 21 additions & 1 deletion src/components/Instance/Instance.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ExtensionPageState from "@/atoms/ExtensionPageAtom";
import { LocalStorageSystemInstance } from "@/types/SystemInstance";
import { Checkbox } from "@mui/material";
import { useRecoilState } from "recoil";
import semver from "semver";

type Props = {
Expand All @@ -11,6 +13,9 @@ function getVersion(url: string) {
}

export default function Instance({ instance }: Props) {
const [state, setState] = useRecoilState(ExtensionPageState);

// FIXME
const version =
"version" in instance && typeof instance.version === "string"
? instance.version
Expand Down Expand Up @@ -39,7 +44,22 @@ export default function Instance({ instance }: Props) {
)}
</p>
</div>
<Checkbox disabled={!compatible} />
<Checkbox
disabled={!compatible}
onChange={(e) =>
setState((s) => {
return {
...s,
selectedInstanceIds: e.target.checked
? [...s.selectedInstanceIds, instance.id]
: s.selectedInstanceIds.filter(
(id) => id !== instance.id
),
};
})
}
checked={state.selectedInstanceIds.includes(instance.id)}
/>
</div>
);
}
Loading

0 comments on commit f316e66

Please sign in to comment.