-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
370 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
221 changes: 221 additions & 0 deletions
221
src/webui/app/routes/workspace/smart-lists/$key/items/details.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import { | ||
Button, | ||
ButtonGroup, | ||
Divider, | ||
FormControl, | ||
InputLabel, | ||
OutlinedInput, | ||
Stack, | ||
} from "@mui/material"; | ||
import type { ActionArgs, LoaderArgs } from "@remix-run/node"; | ||
import { json, redirect } from "@remix-run/node"; | ||
import { | ||
useActionData, | ||
useCatch, | ||
useParams, | ||
useTransition, | ||
} from "@remix-run/react"; | ||
import { ReasonPhrases, StatusCodes } from "http-status-codes"; | ||
import { ApiError, SmartListService } from "jupiter-gen"; | ||
import { useContext } from "react"; | ||
import { z } from "zod"; | ||
import { parseForm, parseParams } from "zodix"; | ||
import { IconSelector } from "~/components/icon-selector"; | ||
import { FieldError, GlobalError } from "~/components/infra/errors"; | ||
import { LeafCard } from "~/components/infra/leaf-card"; | ||
import { GlobalPropertiesContext } from "~/global-properties-client"; | ||
import { validationErrorToUIErrorInfo } from "~/logic/action-result"; | ||
import { isDevelopment } from "~/logic/domain/env"; | ||
import { getIntent } from "~/logic/intent"; | ||
import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; | ||
import { DisplayType } from "~/rendering/use-nested-entities"; | ||
|
||
const ParamsSchema = { | ||
key: z.string(), | ||
}; | ||
|
||
const UpdateFormSchema = { | ||
intent: z.string(), | ||
name: z.string(), | ||
icon: z.string().optional(), | ||
}; | ||
|
||
export const handle = { | ||
displayType: DisplayType.LEAF, | ||
}; | ||
|
||
export async function loader({ params }: LoaderArgs) { | ||
const { key } = parseParams(params, ParamsSchema); | ||
|
||
try { | ||
const response = await SmartListService.loadSmartList({ | ||
key: { the_key: key }, | ||
allow_archived: true, | ||
}); | ||
|
||
return json({ | ||
smartList: response.smart_list, | ||
}); | ||
} catch (error) { | ||
if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { | ||
throw new Response(ReasonPhrases.NOT_FOUND, { | ||
status: StatusCodes.NOT_FOUND, | ||
statusText: ReasonPhrases.NOT_FOUND, | ||
}); | ||
} | ||
|
||
throw error; | ||
} | ||
} | ||
|
||
export async function action({ request, params }: ActionArgs) { | ||
const { key } = parseParams(params, ParamsSchema); | ||
const form = await parseForm(request, UpdateFormSchema); | ||
|
||
const { intent } = getIntent<undefined>(form.intent); | ||
|
||
try { | ||
switch (intent) { | ||
case "update": { | ||
await SmartListService.updateSmartList({ | ||
key: { the_key: key }, | ||
name: { | ||
should_change: true, | ||
value: { the_name: form.name }, | ||
}, | ||
icon: { | ||
should_change: true, | ||
value: form.icon ? { the_icon: form.icon } : undefined, | ||
}, | ||
}); | ||
|
||
return redirect(`/workspace/smart-lists/${key}/items/details`); | ||
} | ||
|
||
case "archive": { | ||
await SmartListService.archiveSmartList({ | ||
key: { the_key: key }, | ||
}); | ||
return redirect("/workspace/smart-lists"); | ||
} | ||
|
||
default: | ||
return new Response("Bad Intent", { status: 500 }); | ||
} | ||
} catch (error) { | ||
if ( | ||
error instanceof ApiError && | ||
error.status === StatusCodes.UNPROCESSABLE_ENTITY | ||
) { | ||
return json(validationErrorToUIErrorInfo(error.body)); | ||
} | ||
|
||
throw error; | ||
} | ||
} | ||
|
||
export default function SmartListDetails() { | ||
const { key } = useParams(); | ||
const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); | ||
const actionData = useActionData<typeof action>(); | ||
const transition = useTransition(); | ||
|
||
const inputsEnabled = | ||
transition.state === "idle" && !loaderData.smartList.archived; | ||
|
||
return ( | ||
<LeafCard | ||
key={loaderData.smartList.key.the_key} | ||
showArchiveButton | ||
enableArchiveButton={inputsEnabled} | ||
returnLocation={`/workspace/smart-lists/${key}/items`} | ||
> | ||
<GlobalError actionResult={actionData} /> | ||
<Stack spacing={2}> | ||
<FormControl fullWidth> | ||
<InputLabel id="key">Key</InputLabel> | ||
<OutlinedInput | ||
label="Key" | ||
name="key" | ||
disabled | ||
defaultValue={loaderData.smartList.key.the_key} | ||
/> | ||
</FormControl> | ||
|
||
<FormControl fullWidth> | ||
<InputLabel id="name">Name</InputLabel> | ||
<OutlinedInput | ||
label="Name" | ||
name="name" | ||
readOnly={!inputsEnabled} | ||
defaultValue={loaderData.smartList.name.the_name} | ||
/> | ||
<FieldError actionResult={actionData} fieldName="/name" /> | ||
</FormControl> | ||
|
||
<FormControl fullWidth> | ||
<InputLabel id="icon">Icon</InputLabel> | ||
<IconSelector | ||
readOnly={!inputsEnabled} | ||
defaultIcon={loaderData.smartList.icon?.the_icon} | ||
/> | ||
<FieldError actionResult={actionData} fieldName="/icon" /> | ||
</FormControl> | ||
</Stack> | ||
|
||
<Divider>Actions</Divider> | ||
|
||
<ButtonGroup> | ||
<Button | ||
variant="contained" | ||
disabled={!inputsEnabled} | ||
type="submit" | ||
name="intent" | ||
value="update" | ||
> | ||
Save | ||
</Button> | ||
</ButtonGroup> | ||
</LeafCard> | ||
); | ||
} | ||
|
||
export function CatchBoundary() { | ||
const caught = useCatch(); | ||
const { key } = useParams(); | ||
|
||
if (caught.status === StatusCodes.NOT_FOUND) { | ||
return ( | ||
<article className="message is-warning"> | ||
<div className="message-header"> | ||
<p>Warning</p> | ||
</div> | ||
<div className="message-body">Could not find smart list #{key}!</div> | ||
</article> | ||
); | ||
} | ||
|
||
throw new Error(`Unhandled error: ${caught.status}`); | ||
} | ||
|
||
export function ErrorBoundary({ error }: { error: Error }) { | ||
const globalProperties = useContext(GlobalPropertiesContext); | ||
|
||
return ( | ||
<article className="message is-danger"> | ||
<div className="message-header"> | ||
<p>Danger</p> | ||
</div> | ||
<div className="message-body"> | ||
There was an error updating the smart list. Please try again! | ||
</div> | ||
|
||
{isDevelopment(globalProperties.env) && ( | ||
<div> | ||
<pre>{error.message}</pre> | ||
<pre>{error.stack}</pre> | ||
</div> | ||
)} | ||
</article> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.