feat: Finish off onboarding instance import frontend + multi-select lists

This commit is contained in:
LynithDev 2024-10-28 11:58:54 +01:00
parent 23ff953f64
commit 709d2a0136
5 changed files with 166 additions and 31 deletions

View file

@ -1,21 +1,51 @@
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import { type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, type Signal, splitProps, useContext } from 'solid-js';
import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, type Setter, splitProps, useContext } from 'solid-js';
import styles from './SelectList.module.scss';
type SelectListContextHelpers = Signal<number | undefined>;
interface SelectListContextHelpers {
selected: Accessor<number[]>;
setSelected: Setter<number[]>;
select: (index: number) => void;
}
const SelectListContext = createContext<SelectListContextHelpers>() as Context<SelectListContextHelpers>;
function SelectListContextProvider(props: ParentProps & {
onChanged?: ((index: number | undefined) => void) | undefined;
onChanged?: ((index: number[]) => void) | undefined;
multiple?: boolean | undefined;
selected?: number[] | undefined;
}) {
const [selected, setSelected] = createSignal<number | undefined>();
// eslint-disable-next-line solid/reactivity -- -
const [selected, setSelected] = createSignal<number[]>(props.selected ?? []);
createEffect(() => {
props.onChanged?.(selected());
});
createEffect(() => {
if (props.selected !== undefined)
setSelected(props.selected);
});
const ctx = {
selected,
setSelected,
select(index: number) {
setSelected((prev) => {
if (props.multiple) {
if (prev.includes(index))
return prev.filter(i => i !== index);
return [...prev, index];
}
else {
return [index];
}
});
},
};
return (
<SelectListContext.Provider value={[selected, setSelected]}>
<SelectListContext.Provider value={ctx}>
{props.children}
</SelectListContext.Provider>
);
@ -26,13 +56,20 @@ function useSelectListContext() {
}
export type SelectListProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
onChange?: (index: number | undefined) => void;
onChange?: (index: number[]) => void;
multiple?: boolean | undefined;
selected?: number[] | undefined;
};
function SelectList(props: SelectListProps) {
const [split, rest] = splitProps(props, ['class', 'onChange']);
return (
<SelectListContextProvider onChanged={props.onChange}>
<SelectListContextProvider
multiple={props.multiple}
onChanged={props.onChange}
selected={props.selected}
>
<div {...rest} class={`${styles.select_list} ${split.class || ''}`}>
<OverlayScrollbarsComponent class="max-h-full">
<div>
@ -45,20 +82,33 @@ function SelectList(props: SelectListProps) {
}
export type SelectListRowProps = JSX.HTMLAttributes<HTMLDivElement> & {
index?: number;
index: number;
};
SelectList.Row = (props: SelectListRowProps) => {
const [selected, setSelected] = useSelectListContext();
const { selected, setSelected, select } = useSelectListContext();
const [split, rest] = splitProps(props, ['index', 'class', 'onClick']);
return (
<div
{...rest}
class={`${styles.row} ${props.index !== undefined && selected() === props.index ? styles.selected : ''} ${split.class || ''}`}
class={`${styles.row} ${props.index !== undefined && selected().includes(props.index) ? styles.selected : ''} ${split.class || ''}`}
onClick={(e) => {
if (props.index !== undefined)
setSelected(props.index);
if (e.shiftKey) {
const indexes = selected();
const last = indexes[indexes.length - 1]!;
const start = last !== undefined ? Math.min(last, props.index) : props.index;
const end = Math.max(last, props.index) || start;
const newIndexes = Array.from({ length: end - start + 1 }, (_, i) => i + start);
setSelected(prev => [
...prev.filter(i => !newIndexes.includes(i)),
...newIndexes,
]);
}
else {
select(split.index);
}
if (typeof split.onClick === 'function')
split.onClick(e);

View file

@ -156,7 +156,7 @@ Modal.Simple = function (props: ModalSimpleProps) {
<div class="mt-2 flex flex-row gap-x-4 [&>*]:flex-1">
<For each={split.buttons}>
{button => (
<ModalButton props={button} />
<ModalButton hide={props.hide} props={button} />
)}
</For>
</div>
@ -304,12 +304,12 @@ Modal.Delete = function (props: ModalDeleteProps) {
);
};
function ModalButton(button: { props: ModalButtonProps }) {
function ModalButton(button: { hide: () => void; props: ModalButtonProps }) {
return (
<>
{typeof button.props === 'string'
? (
<Button buttonStyle="secondary">
<Button buttonStyle="secondary" onClick={button.hide}>
{button.props}
</Button>
)

View file

@ -129,7 +129,7 @@ function VersionSelector(props: { setVersion: (version: string) => void }) {
<div class="flex flex-1 flex-row gap-2">
<SelectList
class="max-h-40 min-w-3/5"
onChange={setVersion}
onChange={indexes => setVersion(indexes[0])}
>
<Show when={filteredVersions() !== undefined}>
<Index each={filteredVersions()}>

View file

@ -30,9 +30,19 @@ export const enum OnboardingTaskStage {
Completed = 2,
}
interface ImportInstancesType {
basePath: string;
instances: string[];
}
type InstancesImportMapType = Map<ImportType, ImportInstancesType>;
interface OnboardingContextType {
setLanguage: (language: Language) => void;
setImportTypeInstances: (type: ImportType, instances: string[]) => void;
language: () => Language;
setImportInstances: (type: ImportType, basePath: string, instances: string[]) => void;
importInstances: (type: ImportType) => ImportInstancesType | undefined;
getTasks: () => string[];
@ -51,7 +61,7 @@ function Onboarding(props: ParentProps) {
const [forwardButtonEnabled, setForwardButtonEnabled] = createSignal(true);
const [language, setLanguage] = createSignal<Language>('en');
const [importTypes, setImportTypes] = createSignal<Map<ImportType, string[]>>(new Map());
const [importInstances, setImportInstances] = createSignal<InstancesImportMapType>(new Map());
const [tasksStage, setTasksStage] = createSignal<OnboardingTaskStage>(OnboardingTaskStage.NotStarted);
const [tasksMessage, setTasksMessage] = createSignal<string>('');
@ -131,7 +141,7 @@ function Onboarding(props: ParentProps) {
tasks.push(`Set language to ${LanguagesList[language()][0]}`);
importTypes().forEach((type) => {
importInstances().forEach((type) => {
tasks.push(`Import profiles from ${type}`);
});
@ -140,13 +150,19 @@ function Onboarding(props: ParentProps) {
const ctx: OnboardingContextType = {
setLanguage,
setImportTypeInstances(type, instances) {
setImportTypes((importTypes) => {
language,
setImportInstances(type, basePath, instances) {
setImportInstances((importTypes) => {
const newMap = new Map(importTypes);
newMap.set(type, instances);
newMap.set(type, {
basePath,
instances,
});
return newMap;
});
},
importInstances: type => importInstances().get(type),
getTasks,

View file

@ -1,24 +1,36 @@
import type { ImportType } from '@onelauncher/client/bindings';
import Illustration from '~assets/illustrations/onboarding/import_from_others.svg?component-solid';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import SelectList from '~ui/components/base/SelectList';
import LauncherIcon from '~ui/components/content/LauncherIcon';
import Modal, { createModal, type ModalProps } from '~ui/components/overlay/Modal';
import useCommand from '~ui/hooks/useCommand';
import { LAUNCHER_IMPORT_TYPES } from '~utils';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import { For } from 'solid-js';
import { createEffect, createSignal, For } from 'solid-js';
import Onboarding, { OnboardingStep } from './Onboarding';
import { createModal } from '~ui/components/overlay/Modal';
function OnboardingImport() {
const ctx = Onboarding.useContext();
const [launcher, setLauncher] = createSignal<ImportType>();
const modal = createModal((props) => {
});
const modal = createModal(props => (
<InstancesPickerModal
{...props}
launcher={launcher()!}
onSelected={(basePath, instance) => {
ctx.setImportInstances(launcher()!, basePath, instance);
}}
selected={ctx.importInstances(launcher()!)?.instances}
/>
));
function displayImport(type: ImportType) {
setLauncher(type);
modal.show();
}
return (
<OnboardingStep
illustration={<Illustration />}
@ -30,7 +42,10 @@ function OnboardingImport() {
<div class="grid grid-cols-3">
<For each={LAUNCHER_IMPORT_TYPES}>
{type => (
<button class="flex flex-col items-center justify-center gap-y-4 rounded-md p-4 active:bg-gray-10 hover:bg-gray-05" onClick={() => displayImport(type)}>
<button
class={`flex flex-col items-center justify-center gap-y-4 rounded-md p-4 active:bg-gray-10 hover:bg-gray-05 ${(ctx.importInstances(type)?.instances.length || 0) > 0 ? 'bg-success hover:bg-success/70' : ''}`}
onClick={() => displayImport(type)}
>
<LauncherIcon class="h-16 max-w-22 min-w-16" launcher={type} />
<span class="text-lg font-medium">{type}</span>
</button>
@ -45,6 +60,60 @@ function OnboardingImport() {
export default OnboardingImport;
function InstancesPickerModal() {
type InstancesPickerModalProps = ModalProps & {
launcher: ImportType;
selected: string[] | undefined;
onSelected: (basePath: string, instances: string[]) => void;
};
function InstancesPickerModal(props: InstancesPickerModalProps) {
const [instances] = useCommand(() => bridge.commands.getLauncherInstances(props.launcher, null));
const [selected, setSelected] = createSignal<number[]>([]);
const getSelectedInstances = () => selected().map(i => instances()?.[1][i]).filter(v => v !== undefined);
createEffect(() => {
const selected = props.selected;
const instancesList = instances()?.[1];
if (selected !== undefined && instancesList !== undefined) {
const indexes = selected.map(instance => instancesList.indexOf(instance)).filter(i => i !== undefined);
setSelected(indexes);
}
});
return (
<Modal.Simple
{...props}
buttons={[
'Cancel',
<Button
buttonStyle="primary"
children={`Import (${selected().length || 'None'})`}
disabled={(instances()?.length || 0) <= 0}
onClick={() => {
props.onSelected(instances()![0], getSelectedInstances());
props.hide();
}}
/>,
]}
title="Import Instances"
>
<div class="flex flex-col gap-2">
<SelectList
class="h-52 max-h-52"
multiple
onChange={setSelected}
selected={selected()}
>
<For each={instances()?.[1]}>
{(instance, index) => (
<SelectList.Row index={index()}>
{instance}
</SelectList.Row>
)}
</For>
</SelectList>
</div>
</Modal.Simple>
);
}