Skip to content

Commit f316e66

Browse files
feat(modals:install): add instances
1 parent 6b8332f commit f316e66

10 files changed

+309
-33
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"recoil": "^0.7.7",
3030
"semver": "^7.6.0",
3131
"sharp": "^0.33.2",
32+
"tailwind-merge": "^2.2.1",
3233
"uuid": "^9.0.1"
3334
},
3435
"devDependencies": {
@@ -48,4 +49,4 @@
4849
"tailwindcss": "^3.3.0",
4950
"typescript": "^5"
5051
}
51-
}
52+
}

src/app/recoil.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function RecoilProvider({ children }: PropsWithChildren) {
2222
] as LocalStorageSystemInstance[],
2323
downloadModalOpen: false,
2424
installModalOpen: false,
25+
selectedInstanceIds: [] as string[],
2526
});
2627
}
2728
}

src/atoms/ExtensionPageAtom.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const ExtensionPageState = atom({
1717
url: "https://whatever.org",
1818
},
1919
] as LocalStorageSystemInstance[],
20+
selectedInstanceIds: [] as string[],
2021
},
2122
});
2223

src/components/Button/ThinButton.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ComponentProps, FC, JSX, ReactNode } from "react";
2+
import { twMerge } from "tailwind-merge";
3+
4+
type Props<E extends keyof JSX.IntrinsicElements | FC> = {
5+
as?: E;
6+
children?: ReactNode;
7+
className?: string;
8+
} & ComponentProps<E>;
9+
10+
const ThinButton = <E extends keyof JSX.IntrinsicElements | FC = "button">({
11+
as = "button" as E,
12+
children,
13+
className = "",
14+
...props
15+
}: Props<E>) => {
16+
const Component = as as FC<typeof props>;
17+
18+
return (
19+
<Component
20+
{...props}
21+
className={twMerge(
22+
"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",
23+
className
24+
)}
25+
>
26+
{children}
27+
</Component>
28+
);
29+
};
30+
31+
export default ThinButton;

src/components/Extension/ExtensionInstallChooseInstances.tsx

+51-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,67 @@
1+
import ExtensionPageState from "@/atoms/ExtensionPageAtom";
12
import { Box } from "@mui/material";
2-
import { FC } from "react";
3+
import { FC, useState } from "react";
4+
import { MdError } from "react-icons/md";
5+
import { useRecoilValue } from "recoil";
6+
import InstallAddForm from "../Instance/InstanceAddForm";
37
import Instances from "../Instance/Instances";
8+
import { StepProps } from "./ExtensionInstallModal";
49

5-
interface ExtensionInstallationChooseInstancesProps {}
10+
const ExtensionInstallationChooseInstances: FC<StepProps> = ({
11+
setNextValidator,
12+
}) => {
13+
const state = useRecoilValue(ExtensionPageState);
14+
const [isValid, setIsValid] = useState(true);
15+
const [addingInstance, setAddingInstance] = useState(false);
16+
17+
setNextValidator(() => {
18+
const valid = state.selectedInstanceIds.length > 0;
19+
20+
if (valid !== isValid) {
21+
setIsValid(valid);
22+
}
23+
24+
return valid;
25+
});
626

7-
const ExtensionInstallationChooseInstances: FC<
8-
ExtensionInstallationChooseInstancesProps
9-
> = () => {
1027
return (
1128
<Box
1229
sx={{
1330
pt: 2,
1431
}}
1532
>
16-
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
17-
Choose the instances you want to install the extension on.
18-
</p>
33+
<div className="md:flex justify-between items-center">
34+
<p className="text-sm md:text-base text-[#555] dark:text-[#999]">
35+
Choose the instances you want to install the extension on.
36+
You can{" "}
37+
<a
38+
href="#"
39+
className="text-blue-500 hover:underline hover:text-blue-600"
40+
onClick={() => setAddingInstance(true)}
41+
>
42+
add more instances
43+
</a>{" "}
44+
if you want.
45+
</p>
46+
</div>
1947
<br />
2048

49+
{!isValid && (
50+
<p className="text-xs text-red-500 dark:text-red-400 flex items-center gap-1">
51+
<MdError />
52+
{state.instances.length === 0
53+
? "You must add at least one instance."
54+
: "You must select at least one instance."}
55+
</p>
56+
)}
57+
2158
<Instances />
59+
60+
{addingInstance && (
61+
<InstallAddForm onClose={() => setAddingInstance(false)} />
62+
)}
63+
64+
<div className="pb-7"></div>
2265
</Box>
2366
);
2467
};

src/components/Extension/ExtensionInstallModal.tsx

+43-17
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
Stepper,
1212
Typography,
1313
} from "@mui/material";
14-
import { useState } from "react";
14+
import { FC, useRef, useState } from "react";
1515
import { MdClose } from "react-icons/md";
1616
import { useRecoilState } from "recoil";
1717
import DeployedCodeUpdate from "../Icons/DeployedCodeUpdate";
@@ -29,17 +29,32 @@ const components = [
2929
() => <p>Finish</p>,
3030
];
3131

32+
type NextValidator = () => boolean | Promise<boolean>;
33+
34+
export type StepProps = {
35+
setNextValidator: (validator: NextValidator) => void;
36+
};
37+
3238
export default function ExtensionInstallModal({ extension }: Props) {
3339
const [state, setState] = useRecoilState(ExtensionPageState);
3440
const [activeStep, setActiveStep] = useState(0);
3541
const [skipped, setSkipped] = useState(new Set<number>());
42+
const nextValidatorRef = useRef<NextValidator | null>(null);
3643
const onClose = () => {
37-
setState((state) => ({ ...state, installModalOpen: false }));
44+
setState((state) => ({
45+
...state,
46+
installModalOpen: false,
47+
selectedInstanceIds: [],
48+
}));
3849
setActiveStep(0);
3950
setSkipped(new Set<number>());
4051
};
4152

42-
const StepComponent = components[activeStep];
53+
const setNextValidator = (validator: NextValidator) => {
54+
nextValidatorRef.current = validator;
55+
};
56+
57+
const StepComponent = components[activeStep] as FC<StepProps>;
4358

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

52-
const handleNext = () => {
67+
const handleNext = async () => {
5368
let newSkipped = skipped;
69+
5470
if (isStepSkipped(activeStep)) {
5571
newSkipped = new Set(newSkipped.values());
5672
newSkipped.delete(activeStep);
5773
}
5874

75+
if (nextValidatorRef.current) {
76+
const valid = await nextValidatorRef.current();
77+
78+
if (!valid) {
79+
return;
80+
}
81+
}
82+
83+
nextValidatorRef.current = null;
5984
setActiveStep((prevActiveStep) => prevActiveStep + 1);
6085
setSkipped(newSkipped);
6186
};
6287

6388
const handleBack = () => {
89+
nextValidatorRef.current = null;
6490
setActiveStep((prevActiveStep) => prevActiveStep - 1);
6591
};
6692

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

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

80-
const handleReset = () => {
81-
setActiveStep(0);
82-
};
83-
84107
return (
85108
<Modal
86109
open={state.installModalOpen}
@@ -142,18 +165,15 @@ export default function ExtensionInstallModal({ extension }: Props) {
142165
</Typography>
143166
</>
144167
) : (
145-
<StepComponent />
168+
<StepComponent
169+
setNextValidator={setNextValidator}
170+
/>
146171
)}
147172
</Box>
148173

149174
<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)]">
150-
<Button
151-
color="inherit"
152-
disabled={activeStep === 0}
153-
onClick={handleBack}
154-
sx={{ mr: 1 }}
155-
>
156-
Back
175+
<Button color="inherit" onClick={onClose}>
176+
Cancel
157177
</Button>
158178
<Box sx={{ flex: "1 1 auto" }} />
159179
{isStepOptional(activeStep) && (
@@ -165,7 +185,13 @@ export default function ExtensionInstallModal({ extension }: Props) {
165185
Skip
166186
</Button>
167187
)}
168-
<Button onClick={onClose}>Cancel</Button>{" "}
188+
<Button
189+
disabled={activeStep === 0}
190+
onClick={handleBack}
191+
sx={{ mr: 1 }}
192+
>
193+
Back
194+
</Button>{" "}
169195
<Button onClick={handleNext} variant="outlined">
170196
{activeStep === steps.length - 1
171197
? "Finish"

src/components/Instance/Instance.tsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import ExtensionPageState from "@/atoms/ExtensionPageAtom";
12
import { LocalStorageSystemInstance } from "@/types/SystemInstance";
23
import { Checkbox } from "@mui/material";
4+
import { useRecoilState } from "recoil";
35
import semver from "semver";
46

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

1315
export default function Instance({ instance }: Props) {
16+
const [state, setState] = useRecoilState(ExtensionPageState);
17+
18+
// FIXME
1419
const version =
1520
"version" in instance && typeof instance.version === "string"
1621
? instance.version
@@ -39,7 +44,22 @@ export default function Instance({ instance }: Props) {
3944
)}
4045
</p>
4146
</div>
42-
<Checkbox disabled={!compatible} />
47+
<Checkbox
48+
disabled={!compatible}
49+
onChange={(e) =>
50+
setState((s) => {
51+
return {
52+
...s,
53+
selectedInstanceIds: e.target.checked
54+
? [...s.selectedInstanceIds, instance.id]
55+
: s.selectedInstanceIds.filter(
56+
(id) => id !== instance.id
57+
),
58+
};
59+
})
60+
}
61+
checked={state.selectedInstanceIds.includes(instance.id)}
62+
/>
4363
</div>
4464
);
4565
}

0 commit comments

Comments
 (0)