Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/npm_and_yarn/vite-5.4.6
Browse files Browse the repository at this point in the history
  • Loading branch information
DonKoko authored Sep 19, 2024
2 parents 6373069 + 05950dc commit 510e795
Show file tree
Hide file tree
Showing 53 changed files with 2,008 additions and 232 deletions.
2 changes: 2 additions & 0 deletions app/atoms/bulk-update-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const bulkDialogAtom = atom<Record<BulkDialogType, boolean>>({
category: false,
"assign-custody": false,
"release-custody": false,
"tag-add": false,
"tag-remove": false,
trash: false,
activate: false,
deactivate: false,
Expand Down
2 changes: 1 addition & 1 deletion app/atoms/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type NotificationIcon = {
export interface NotificationType {
open: boolean;
title: string;
message: string;
message?: string | null;
icon: NotificationIcon;
time?: number;
senderId: User["id"] | null;
Expand Down
114 changes: 114 additions & 0 deletions app/atoms/qr-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { atom } from "jotai";
import type { AssetWithBooking } from "~/routes/_layout+/bookings.$bookingId.add-assets";
import type { KitForBooking } from "~/routes/_layout+/bookings.$bookingId.add-kits";

export type ScanListItems = {
[key: string]: ScanListItem;
};

export type ScanListItem =
| {
data?: AssetWithBooking | KitForBooking;
error?: string;
type?: "asset" | "kit";
}
| undefined;

/***********************
* Scanned QR Id Atom *
*
* The data is structured in a object where:
* - key: qrId
* - value: asset
*
***********************/

export const scannedItemsAtom = atom<ScanListItems>({});

/** Get an array of the scanned items ids */
export const scannedItemsIdsAtom = atom((get) =>
Object.values(get(scannedItemsAtom)).map((item) => item?.data?.id)
);

// Add item to object with value `undefined` (just receives the key)
export const addScannedItemAtom = atom(
null,
(get, set, qrId: string, error?: string) => {
const currentItems = get(scannedItemsAtom);
if (!currentItems[qrId]) {
/** Set can optionally receive error. If it does, add it to the item.
* This is used for errors that are related to the QR code itself, not the item.
*/
set(scannedItemsAtom, {
[qrId]: error
? {
error: error,
}
: undefined, // Add the new entry at the start
...currentItems, // Spread the rest of the existing items
});
}
}
);

// Update item based on key
export const updateScannedItemAtom = atom(
null,
(get, set, { qrId, item }: { qrId: string; item: ScanListItem }) => {
const currentItems = get(scannedItemsAtom);

// Check if the item already exists; if it does, skip the update
if (!item || currentItems[qrId]) {
return; // Skip the update if the item is already present
}

if ((item && item?.data && item?.type) || item?.error) {
set(scannedItemsAtom, {
...currentItems,
[qrId]: item,
});
}
}
);

// Remove item based on key
export const removeScannedItemAtom = atom(null, (get, set, qrId: string) => {
const currentItems = get(scannedItemsAtom);
const { [qrId]: _, ...rest } = currentItems; // Removes the key
set(scannedItemsAtom, rest);
});

// Remove multiple items based on key array
export const removeMultipleScannedItemsAtom = atom(
null,
(get, set, qrIds: string[]) => {
const currentItems = get(scannedItemsAtom);
const updatedItems = { ...currentItems };
qrIds.forEach((qrId) => {
delete updatedItems[qrId];
});
set(scannedItemsAtom, updatedItems);
}
);

// Remove items based on asset id
export const removeScannedItemsByAssetIdAtom = atom(
null,
(get, set, ids: string[]) => {
const currentItems = get(scannedItemsAtom);
const updatedItems = { ...currentItems };
Object.entries(currentItems).forEach(([qrId, item]) => {
if (item?.data?.id && ids.includes(item?.data?.id)) {
delete updatedItems[qrId];
}
});
set(scannedItemsAtom, updatedItems);
}
);

// Clear all items
export const clearScannedItemsAtom = atom(null, (_get, set) => {
set(scannedItemsAtom, {}); // Resets the atom to an empty object
});

/*******************************/
21 changes: 21 additions & 0 deletions app/components/assets/bulk-actions-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { isFormProcessing } from "~/utils/form";
import { isSelectingAllItems } from "~/utils/list";
import { tw } from "~/utils/tw";
import BulkAssignCustodyDialog from "./bulk-assign-custody-dialog";
import BulkAssignTagsDialog from "./bulk-assign-tags-dialog";
import BulkCategoryUpdateDialog from "./bulk-category-update-dialog";
import BulkDeleteDialog from "./bulk-delete-dialog";
import BulkLocationUpdateDialog from "./bulk-location-update-dialog";
import BulkReleaseCustodyDialog from "./bulk-release-custody-dialog";
import BulkRemoveTagsDialog from "./bulk-remove-tags-dialog";
import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog";
import { ChevronRight } from "../icons/library";
import { Button } from "../shared/button";
Expand Down Expand Up @@ -93,6 +95,8 @@ function ConditionalDropdown() {
/>
)}
<BulkLocationUpdateDialog />
<BulkAssignTagsDialog />
<BulkRemoveTagsDialog />
<BulkCategoryUpdateDialog />
<BulkAssignCustodyDialog />
<BulkReleaseCustodyDialog />
Expand Down Expand Up @@ -184,6 +188,23 @@ function ConditionalDropdown() {
}
/>
</DropdownMenuItem>
<DropdownMenuItem className="py-1 lg:p-0">
<BulkUpdateDialogTrigger
type="tag-add"
onClick={closeMenu}
disabled={isLoading}
label="Assign tags"
/>
</DropdownMenuItem>

<DropdownMenuItem className="py-1 lg:p-0">
<BulkUpdateDialogTrigger
type="tag-remove"
onClick={closeMenu}
disabled={isLoading}
label="Remove tags"
/>
</DropdownMenuItem>

<DropdownMenuItem className="py-1 lg:p-0">
<BulkUpdateDialogTrigger
Expand Down
80 changes: 80 additions & 0 deletions app/components/assets/bulk-assign-tags-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { useFetcher } from "@remix-run/react";
import { useZorm } from "react-zorm";
import { z } from "zod";
import { BulkUpdateDialogContent } from "../bulk-update-dialog/bulk-update-dialog";
import { Button } from "../shared/button";
import { TagsAutocomplete, type TagSuggestion } from "../tag/tags-autocomplete";

export const BulkAssignTagsSchema = z.object({
assetIds: z.array(z.string()).min(1),
tags: z.string(),
});

export default function BulkAssignTagsDialog() {
const zo = useZorm("BulkAssignTags", BulkAssignTagsSchema);

const fetcher = useFetcher();
// @ts-ignore
const suggestions = fetcher.data?.filters.map((tagResponse) => ({
label: tagResponse.name,
value: tagResponse.id,
})) as TagSuggestion[];

useEffect(() => {
fetcher.submit(
{
name: "tag",
queryKey: "name",
queryValue: "",
},
{
method: "GET",
action: "/api/model-filters",
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<BulkUpdateDialogContent
ref={zo.ref}
type="tag-add"
title="Assign tags to assets"
description="Assign tags to selected assets. Assets that already have any of the selected tags, will be skipped."
actionUrl="/api/assets/bulk-assign-tags"
arrayFieldId="assetIds"
>
{({ disabled, handleCloseDialog, fetcherError }) => (
<div className="modal-content-wrapper">
<div className="relative z-50 mb-8">
<TagsAutocomplete existingTags={[]} suggestions={suggestions} />

{zo.errors.tags()?.message ? (
<p className="text-sm text-error-500">
{zo.errors.tags()?.message}
</p>
) : null}
{fetcherError ? (
<p className="text-sm text-error-500">{fetcherError}</p>
) : null}
</div>

<div className="flex gap-3">
<Button
variant="secondary"
width="full"
disabled={disabled}
onClick={handleCloseDialog}
>
Cancel
</Button>
<Button variant="primary" width="full" disabled={disabled}>
Confirm
</Button>
</div>
</div>
)}
</BulkUpdateDialogContent>
);
}
80 changes: 80 additions & 0 deletions app/components/assets/bulk-remove-tags-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { useFetcher } from "@remix-run/react";
import { useZorm } from "react-zorm";
import { z } from "zod";
import { BulkUpdateDialogContent } from "../bulk-update-dialog/bulk-update-dialog";
import { Button } from "../shared/button";
import { TagsAutocomplete, type TagSuggestion } from "../tag/tags-autocomplete";

export const BulkRemoveTagsSchema = z.object({
assetIds: z.array(z.string()).min(1),
tags: z.string(),
});

export default function BulkRemoveTagsDialog() {
const zo = useZorm("BulkRemoveTags", BulkRemoveTagsSchema);

const fetcher = useFetcher();
// @ts-ignore
const suggestions = fetcher.data?.filters.map((tagResponse) => ({
label: tagResponse.name,
value: tagResponse.id,
})) as TagSuggestion[];

useEffect(() => {
fetcher.submit(
{
name: "tag",
queryKey: "name",
queryValue: "",
},
{
method: "GET",
action: "/api/model-filters",
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<BulkUpdateDialogContent
ref={zo.ref}
type="tag-remove"
title="Remove tags from assets"
description="Remove tags to selected assets. Assets that don't have any of the selected tags, will be skipped."
actionUrl="/api/assets/bulk-assign-tags?remove=true"
arrayFieldId="assetIds"
>
{({ disabled, handleCloseDialog, fetcherError }) => (
<div className="modal-content-wrapper">
<div className="relative z-50 mb-8">
<TagsAutocomplete existingTags={[]} suggestions={suggestions} />

{zo.errors.tags()?.message ? (
<p className="text-sm text-error-500">
{zo.errors.tags()?.message}
</p>
) : null}
{fetcherError ? (
<p className="text-sm text-error-500">{fetcherError}</p>
) : null}
</div>

<div className="flex gap-3">
<Button
variant="secondary"
width="full"
disabled={disabled}
onClick={handleCloseDialog}
>
Cancel
</Button>
<Button variant="primary" width="full" disabled={disabled}>
Confirm
</Button>
</div>
</div>
)}
</BulkUpdateDialogContent>
);
}
11 changes: 10 additions & 1 deletion app/components/assets/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export const AssetForm = ({
};
}>();

/** Get the tags from the loader */
const tagsSuggestions = useLoaderData<typeof loader>().tags.map((tag) => ({
label: tag.name,
value: tag.id,
}));

return (
<Card className="w-full lg:w-min">
<Form
Expand Down Expand Up @@ -286,7 +292,10 @@ export const AssetForm = ({
required={zodFieldIsRequired(FormSchema.shape.tags)}
>
<InnerLabel hideLg={true}>Tags</InnerLabel>
<TagsAutocomplete existingTags={tags ?? []} />
<TagsAutocomplete
existingTags={tags ?? []}
suggestions={tagsSuggestions}
/>
</FormRow>

<FormRow
Expand Down
2 changes: 1 addition & 1 deletion app/components/booking/asset-row-actions-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const AssetRowActionsDropdown = ({ asset, fullWidth }: Props) => (
<DropdownMenuTrigger
className={tw("asset-actions", fullWidth ? "w-full" : "")}
>
<span className="flex items-center gap-2">
<span className="flex size-6 items-center justify-center gap-2 text-center">
<VerticalDotsIcon />
</span>
</DropdownMenuTrigger>
Expand Down
Loading

0 comments on commit 510e795

Please sign in to comment.