From 616bd524151cc059e42afb21a9b4cfc778394ff2 Mon Sep 17 00:00:00 2001 From: Nikolay Bonev Date: Tue, 30 Jul 2024 11:27:32 +0200 Subject: [PATCH 01/27] Update status-filter.tsx --- app/components/booking/status-filter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/booking/status-filter.tsx b/app/components/booking/status-filter.tsx index 4a5d7019e..f02b2a58b 100644 --- a/app/components/booking/status-filter.tsx +++ b/app/components/booking/status-filter.tsx @@ -23,7 +23,6 @@ export function StatusFilter({ setSearchParams((prev) => { /** If the value is "ALL", we just remove the param */ if (value === "ALL") { - //make sure this is added where-ever we are explicitly delteting the searchaprams manually. prev.delete("status"); return prev; } From 9529b0d281301d08b5f75f023fbd74a1c503f3b6 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 30 Jul 2024 18:29:31 +0300 Subject: [PATCH 02/27] ironing out middleware --- app/utils/constants.ts | 3 +++ app/utils/id.server.ts | 3 ++- server/index.ts | 21 +++++++++++----- server/middleware.ts | 54 ++++++++++++++++++++++++------------------ 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 9d2d4d2ad..0c83beae5 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -3,3 +3,6 @@ export const MAX_DUPLICATES_ALLOWED = 10; /** Amount of day for invite token to expire */ export const INVITE_EXPIRY_TTL_DAYS = 5; + +/** Default length of custom cuid2 */ +export const DEFAULT_CUID_LENGTH = 10; diff --git a/app/utils/id.server.ts b/app/utils/id.server.ts index 65e75723d..d624d5cd2 100644 --- a/app/utils/id.server.ts +++ b/app/utils/id.server.ts @@ -1,4 +1,5 @@ import { init } from "@paralleldrive/cuid2"; +import { DEFAULT_CUID_LENGTH } from "./constants"; import { FINGERPRINT } from "./env"; import { ShelfError } from "./error"; import { Logger } from "./logger"; @@ -21,7 +22,7 @@ export function id(length?: number) { ); } return init({ - length: length || 10, + length: length || DEFAULT_CUID_LENGTH, /** FINGERPRINT is not required but it helps with avoiding collision */ ...(FINGERPRINT && { fingerprint: FINGERPRINT }), })(); diff --git a/server/index.ts b/server/index.ts index 55c01d594..d451ea5b8 100644 --- a/server/index.ts +++ b/server/index.ts @@ -36,15 +36,24 @@ const mode = env.NODE_ENV === "test" ? "development" : env.NODE_ENV; const isProductionMode = mode === "production"; -const app = new Hono(); +const app = new Hono({ + getPath: (req) => { + const url = new URL(req.url); + const host = req.headers.get("host"); -/** - * Add url shortner middleware with conditional path exclusion - */ + if (host === process.env.URL_SHORTENER) { + return "/" + host + url.pathname; + } + + return url.pathname; + }, +}); + +// Apply the middleware to all routes app.use( - "*", + `/${process.env.URL_SHORTENER}/:path*`, urlShortener({ - excludePaths: ["/file-assets/:path*", "/healthcheck", "/static/:path*"], + excludePaths: ["/file-assets", "/healthcheck", "/static"], }) ); diff --git a/server/middleware.ts b/server/middleware.ts index d0fa46a8d..f3f2ce1d0 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -7,6 +7,7 @@ import { refreshAccessToken, validateSession, } from "~/modules/auth/service.server"; +import { DEFAULT_CUID_LENGTH } from "~/utils/constants"; import { ShelfError } from "~/utils/error"; import { safeRedirect } from "~/utils/http.server"; import { Logger } from "~/utils/logger"; @@ -141,38 +142,45 @@ export function cache(seconds: number) { export function urlShortener({ excludePaths }: { excludePaths: string[] }) { return createMiddleware(async (c, next) => { - console.log("c.req", c.req); - const { hostname, pathname } = new URL(c.req.url); - console.log("hostname", hostname); - console.log("pathname", pathname); + const fullPath = c.req.path; + // Remove the URL_SHORTENER part from the beginning of the path + const pathWithoutShortener = fullPath.replace( + `/${process.env.URL_SHORTENER}`, + "" + ); + const pathParts = pathWithoutShortener.split("/").filter(Boolean); + const pathname = "/" + pathParts.join("/"); - // Check if the current request path matches any of the excluded paths - const isExcluded = pathMatch(excludePaths, pathname); - console.log("isExcluded", isExcluded); + console.log(`urlShortener middleware: Processing ${pathname}`); + // Check if the current request path matches any of the excluded paths + const isExcluded = excludePaths.some((path) => pathname.startsWith(path)); if (isExcluded) { - // Skip processing for excluded paths + console.log( + `urlShortener middleware: Skipping excluded path ${pathname}` + ); return next(); } - const urlShortener = process.env.URL_SHORTENER; + const path = pathParts.join("/"); const serverUrl = process.env.SERVER_URL; - if (!urlShortener) return next(); - - console.log("urlShortener", urlShortener); - - if (hostname.startsWith(urlShortener)) { - // Remove leading slash - const path = pathname.slice(1); - - // Check if the path looks like a QR tag (alphanumeric, certain length) - if (isCuid(path)) - return c.redirect(safeRedirect(`https://${serverUrl}/qr/${path}`)); - - return c.redirect(safeRedirect(`https://${serverUrl}/${path}`)); + // Check if the path is a single segment and a valid CUID + if ( + pathParts.length === 1 && + isCuid(path, { + minLength: DEFAULT_CUID_LENGTH, + maxLength: DEFAULT_CUID_LENGTH, + }) + ) { + const redirectUrl = `${serverUrl}/qr/${path}`; + console.log(`urlShortener middleware: Redirecting QR to ${redirectUrl}`); + return c.redirect(safeRedirect(redirectUrl)); } - return next(); + // Handle all other cases + const redirectUrl = `${serverUrl}${pathname}`; + console.log(`urlShortener middleware: Redirecting to ${redirectUrl}`); + return c.redirect(safeRedirect(redirectUrl)); }); } From fd3d22f0d84d3e976fcf2728f65897adc9a3957e Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 11:20:32 +0300 Subject: [PATCH 03/27] refactor search params hooks, change file locations to prevent dependency cycle --- app/components/assets/actions-dropdown.tsx | 2 +- .../assets/bulk-actions-dropdown.tsx | 2 +- .../booking/availability-select.tsx | 2 +- .../booking/bulk-actions-dropdown.tsx | 2 +- app/components/booking/status-filter.tsx | 2 +- .../bulk-update-dialog/bulk-update-dialog.tsx | 2 +- .../category/bulk-actions-dropdown.tsx | 2 +- .../custom-fields/bulk-actions-dropdown.tsx | 2 +- app/components/kits/actions-dropdown.tsx | 2 +- app/components/kits/bulk-actions-dropdown.tsx | 2 +- .../category/category-checkbox-dropdown.tsx | 2 +- app/components/list/filters/search-form.tsx | 2 +- app/components/list/filters/sort-by.tsx | 2 +- .../filters/tag/tag-checkbox-dropdown.tsx | 2 +- .../list/pagination/page-number.tsx | 2 +- .../list/pagination/per-page-items-select.tsx | 2 +- .../location/bulk-actions-dropdown.tsx | 2 +- app/components/nrm/bulk-actions-dropdown.tsx | 2 +- .../successful-subscription-modal.tsx | 2 +- app/components/tag/bulk-actions-dropdown.tsx | 2 +- app/components/welcome/carousel.tsx | 2 +- .../workspace/nrm-actions-dropdown.tsx | 2 +- .../workspace/users-actions-dropdown.tsx | 2 +- app/hooks/search-params/use-search-params.ts | 62 --------- app/hooks/search-params/utils.ts | 130 +++++++++++++++--- .../use-controlled-dropdown-menu.ts | 2 +- app/hooks/use-current-organization-id.ts | 10 +- app/hooks/use-model-filters.ts | 2 +- app/hooks/use-pagination.ts | 2 +- app/hooks/use-position.ts | 2 +- app/routes/_auth+/join.tsx | 2 +- app/routes/_auth+/login.tsx | 2 +- app/routes/_auth+/oauth.callback.tsx | 2 +- app/routes/_auth+/otp.tsx | 2 +- app/routes/_layout+/admin-dashboard+/qrs.tsx | 2 +- app/routes/_layout+/assets.new.tsx | 2 +- app/routes/_layout+/kits.new.tsx | 2 +- .../settings.team.users.invite-user.tsx | 2 +- app/routes/_welcome+/onboarding.tsx | 2 +- app/routes/qr+/$qrId_.link.tsx | 2 +- app/routes/qr+/$qrId_.not-logged-in.tsx | 2 +- 41 files changed, 149 insertions(+), 129 deletions(-) delete mode 100644 app/hooks/search-params/use-search-params.ts rename app/{utils => hooks}/use-controlled-dropdown-menu.ts (95%) diff --git a/app/components/assets/actions-dropdown.tsx b/app/components/assets/actions-dropdown.tsx index 3feba8772..0c9e8a63f 100644 --- a/app/components/assets/actions-dropdown.tsx +++ b/app/components/assets/actions-dropdown.tsx @@ -7,9 +7,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "~/components/shared/dropdown"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import type { loader } from "~/routes/_layout+/assets.$assetId"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; import { DeleteAsset } from "./delete-asset"; import { UpdateGpsCoordinatesForm } from "./update-gps-coordinates-form"; import Icon from "../icons/icon"; diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index 67d0a0bd2..1d954d623 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -5,7 +5,7 @@ import { selectedBulkItemsAtom } from "~/atoms/list"; import { isFormProcessing } from "~/utils/form"; import { isSelectingAllItems } from "~/utils/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkAssignCustodyDialog from "./bulk-assign-custody-dialog"; import BulkCategoryUpdateDialog from "./bulk-category-update-dialog"; import BulkDeleteDialog from "./bulk-delete-dialog"; diff --git a/app/components/booking/availability-select.tsx b/app/components/booking/availability-select.tsx index 5890d2ff5..cc32dd98f 100644 --- a/app/components/booking/availability-select.tsx +++ b/app/components/booking/availability-select.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { Select, diff --git a/app/components/booking/bulk-actions-dropdown.tsx b/app/components/booking/bulk-actions-dropdown.tsx index dd6b0eafd..de5e73b92 100644 --- a/app/components/booking/bulk-actions-dropdown.tsx +++ b/app/components/booking/bulk-actions-dropdown.tsx @@ -3,6 +3,7 @@ import { useNavigation } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { useUserRoleHelper } from "~/hooks/user-user-role-helper"; import { isFormProcessing } from "~/utils/form"; import { @@ -11,7 +12,6 @@ import { } from "~/utils/permissions/permission.data"; import { userHasPermission } from "~/utils/permissions/permission.validator.client"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; import BulkArchiveDialog from "./bulk-archive-dialog"; import BulkCancelDialog from "./bulk-cancel-dialog"; import BulkDeleteDialog from "./bulk-delete-dialog"; diff --git a/app/components/booking/status-filter.tsx b/app/components/booking/status-filter.tsx index f02b2a58b..16e97984a 100644 --- a/app/components/booking/status-filter.tsx +++ b/app/components/booking/status-filter.tsx @@ -1,5 +1,5 @@ import { useNavigation } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { isFormProcessing } from "~/utils/form"; import { Select, diff --git a/app/components/bulk-update-dialog/bulk-update-dialog.tsx b/app/components/bulk-update-dialog/bulk-update-dialog.tsx index b034e4f6d..69132f477 100644 --- a/app/components/bulk-update-dialog/bulk-update-dialog.tsx +++ b/app/components/bulk-update-dialog/bulk-update-dialog.tsx @@ -11,7 +11,7 @@ import { selectedBulkItemsAtom, selectedBulkItemsCountAtom, } from "~/atoms/list"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { action } from "~/routes/api+/assets.bulk-update-location"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/category/bulk-actions-dropdown.tsx b/app/components/category/bulk-actions-dropdown.tsx index eba0c1186..d4d699c87 100644 --- a/app/components/category/bulk-actions-dropdown.tsx +++ b/app/components/category/bulk-actions-dropdown.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/custom-fields/bulk-actions-dropdown.tsx b/app/components/custom-fields/bulk-actions-dropdown.tsx index 5122dda79..54311ef5d 100644 --- a/app/components/custom-fields/bulk-actions-dropdown.tsx +++ b/app/components/custom-fields/bulk-actions-dropdown.tsx @@ -4,7 +4,7 @@ import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkActivateDialog from "./bulk-activate-dialog"; import BulkDeactivateDialog from "./bulk-deactivate-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; diff --git a/app/components/kits/actions-dropdown.tsx b/app/components/kits/actions-dropdown.tsx index e377c2d77..512469fa4 100644 --- a/app/components/kits/actions-dropdown.tsx +++ b/app/components/kits/actions-dropdown.tsx @@ -2,7 +2,7 @@ import { useLoaderData } from "@remix-run/react"; import { useHydrated } from "remix-utils/use-hydrated"; import type { loader } from "~/routes/_layout+/kits.$kitId"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import DeleteKit from "./delete-kit"; import Icon from "../icons/icon"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/kits/bulk-actions-dropdown.tsx b/app/components/kits/bulk-actions-dropdown.tsx index 5528e9c12..cce9fa125 100644 --- a/app/components/kits/bulk-actions-dropdown.tsx +++ b/app/components/kits/bulk-actions-dropdown.tsx @@ -6,7 +6,7 @@ import { selectedBulkItemsAtom } from "~/atoms/list"; import { isFormProcessing } from "~/utils/form"; import { isSelectingAllItems } from "~/utils/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkAssignCustodyDialog from "./bulk-assign-custody-dialog"; import BulkDeleteDialog from "./bulk-delete-dialog"; import BulkReleaseCustodyDialog from "./bulk-release-custody-dialog"; diff --git a/app/components/list/filters/category/category-checkbox-dropdown.tsx b/app/components/list/filters/category/category-checkbox-dropdown.tsx index 9677541b9..0eb64d05b 100644 --- a/app/components/list/filters/category/category-checkbox-dropdown.tsx +++ b/app/components/list/filters/category/category-checkbox-dropdown.tsx @@ -7,7 +7,7 @@ import { CategorySelectNoCategories } from "~/components/category/category-selec import { Badge } from "~/components/shared/badge"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { WithDateFields } from "~/modules/types"; import { useCategorySearch } from "../../../category/useCategorySearch"; import Input from "../../../forms/input"; diff --git a/app/components/list/filters/search-form.tsx b/app/components/list/filters/search-form.tsx index e95fd53ee..d7c26504a 100644 --- a/app/components/list/filters/search-form.tsx +++ b/app/components/list/filters/search-form.tsx @@ -3,7 +3,7 @@ import { useLoaderData, useNavigation } from "@remix-run/react"; import Input from "~/components/forms/input"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { SearchableIndexResponse } from "~/modules/types"; import { isSearching } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/list/filters/sort-by.tsx b/app/components/list/filters/sort-by.tsx index d42c16bd3..e53bc7ebc 100644 --- a/app/components/list/filters/sort-by.tsx +++ b/app/components/list/filters/sort-by.tsx @@ -6,7 +6,7 @@ import { PopoverTrigger, } from "@radix-ui/react-popover"; import { useNavigation } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/list/filters/tag/tag-checkbox-dropdown.tsx b/app/components/list/filters/tag/tag-checkbox-dropdown.tsx index 9e4c76799..4eca54c0f 100644 --- a/app/components/list/filters/tag/tag-checkbox-dropdown.tsx +++ b/app/components/list/filters/tag/tag-checkbox-dropdown.tsx @@ -6,7 +6,7 @@ import { useAtom, useAtomValue } from "jotai"; import { useTagSearch } from "~/components/category/useTagSearch"; import { Badge } from "~/components/shared/badge"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { WithDateFields } from "~/modules/types"; import Input from "../../../forms/input"; import { CheckIcon, ChevronRight } from "../../../icons/library"; diff --git a/app/components/list/pagination/page-number.tsx b/app/components/list/pagination/page-number.tsx index 6892d1537..27a4fbc88 100644 --- a/app/components/list/pagination/page-number.tsx +++ b/app/components/list/pagination/page-number.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { NavLink } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { getParamsValues } from "~/utils/list"; import { mergeSearchParams } from "~/utils/merge-search-params"; diff --git a/app/components/list/pagination/per-page-items-select.tsx b/app/components/list/pagination/per-page-items-select.tsx index 728180cef..bf40d7fc6 100644 --- a/app/components/list/pagination/per-page-items-select.tsx +++ b/app/components/list/pagination/per-page-items-select.tsx @@ -6,7 +6,7 @@ import { SelectContent, SelectItem, } from "~/components/forms/select"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { loader } from "~/routes/_layout+/assets._index"; diff --git a/app/components/location/bulk-actions-dropdown.tsx b/app/components/location/bulk-actions-dropdown.tsx index 6a1e4a7d7..7f1461484 100644 --- a/app/components/location/bulk-actions-dropdown.tsx +++ b/app/components/location/bulk-actions-dropdown.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/nrm/bulk-actions-dropdown.tsx b/app/components/nrm/bulk-actions-dropdown.tsx index f32d28430..42c950337 100644 --- a/app/components/nrm/bulk-actions-dropdown.tsx +++ b/app/components/nrm/bulk-actions-dropdown.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/subscription/successful-subscription-modal.tsx b/app/components/subscription/successful-subscription-modal.tsx index e74d853f9..26cb3173c 100644 --- a/app/components/subscription/successful-subscription-modal.tsx +++ b/app/components/subscription/successful-subscription-modal.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { useLoaderData } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import type { loader } from "~/routes/_layout+/account-details.subscription"; import { Button } from "../shared/button"; diff --git a/app/components/tag/bulk-actions-dropdown.tsx b/app/components/tag/bulk-actions-dropdown.tsx index 9e472aa04..99a9c23ff 100644 --- a/app/components/tag/bulk-actions-dropdown.tsx +++ b/app/components/tag/bulk-actions-dropdown.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/welcome/carousel.tsx b/app/components/welcome/carousel.tsx index e45d5d33f..f4a088040 100644 --- a/app/components/welcome/carousel.tsx +++ b/app/components/welcome/carousel.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { ClientOnly } from "remix-utils/client-only"; import { Pagination, Navigation } from "swiper/modules"; import { Swiper, SwiperSlide } from "swiper/react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { Button } from "../shared/button"; export default function WelcomeCarousel() { diff --git a/app/components/workspace/nrm-actions-dropdown.tsx b/app/components/workspace/nrm-actions-dropdown.tsx index d325bf6fa..08f29a990 100644 --- a/app/components/workspace/nrm-actions-dropdown.tsx +++ b/app/components/workspace/nrm-actions-dropdown.tsx @@ -10,7 +10,7 @@ import { import type { loader } from "~/routes/_layout+/settings.team.nrm"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { DeleteMember } from "./delete-member"; import { Button } from "../shared/button"; diff --git a/app/components/workspace/users-actions-dropdown.tsx b/app/components/workspace/users-actions-dropdown.tsx index ca893fb69..7d1e71229 100644 --- a/app/components/workspace/users-actions-dropdown.tsx +++ b/app/components/workspace/users-actions-dropdown.tsx @@ -13,7 +13,7 @@ import { } from "~/components/shared/dropdown"; import { isFormProcessing } from "~/utils/form"; -import { useControlledDropdownMenu } from "~/utils/use-controlled-dropdown-menu"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { Button } from "../shared/button"; import { Spinner } from "../shared/spinner"; diff --git a/app/hooks/search-params/use-search-params.ts b/app/hooks/search-params/use-search-params.ts deleted file mode 100644 index cf198c608..000000000 --- a/app/hooks/search-params/use-search-params.ts +++ /dev/null @@ -1,62 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { useSearchParams as remixUseSearchParams } from "@remix-run/react"; -// eslint-disable-next-line import/no-cycle -import { useCookieDestroy } from "./utils"; - -/** - * Get the types from the ReturnType of the original useSearchParams hook - */ -type SearchParamsType = ReturnType[0]; // URLSearchParams -type SetSearchParamsType = ReturnType[1]; - -export const useSearchParams = (): [ - SearchParamsType, - ( - nextInit: Parameters[0], - navigateOptions?: Parameters[1] - ) => void, -] => { - const [searchParams, setSearchParams] = remixUseSearchParams(); - const { destroyCookieValues } = useCookieDestroy(); - - const customSetSearchParams: ( - nextInit: Parameters[0], - navigateOptions?: Parameters[1] - ) => void = (nextInit, navigateOptions) => { - const prevParams = new URLSearchParams(searchParams.toString()); - - const checkAndDestroyCookies = (newParams: URLSearchParams) => { - const removedKeys: string[] = []; - prevParams.forEach((_value, key) => { - if (!newParams.has(key)) { - removedKeys.push(key); - } - }); - if (removedKeys.length > 0) { - destroyCookieValues(removedKeys); - } - }; - - if (typeof nextInit === "function") { - setSearchParams((prev) => { - let newParams = nextInit(prev); - // Ensure newParams is an instance of URLSearchParams - if (!(newParams instanceof URLSearchParams)) { - newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types - } - checkAndDestroyCookies(newParams); - return newParams; - }, navigateOptions); - } else { - let newParams = nextInit; - // Ensure newParams is an instance of URLSearchParams - if (!(newParams instanceof URLSearchParams)) { - newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types - } - checkAndDestroyCookies(newParams); - setSearchParams(newParams, navigateOptions); - } - }; - - return [searchParams, customSetSearchParams]; -}; diff --git a/app/hooks/search-params/utils.ts b/app/hooks/search-params/utils.ts index d869054c7..9c5b993d4 100644 --- a/app/hooks/search-params/utils.ts +++ b/app/hooks/search-params/utils.ts @@ -1,10 +1,78 @@ import { useMemo } from "react"; -import { useLoaderData, useLocation } from "@remix-run/react"; +import { + useLoaderData, + useLocation, + // eslint-disable-next-line no-restricted-imports + useSearchParams as remixUseSearchParams, +} from "@remix-run/react"; import Cookies from "js-cookie"; import type { loader } from "~/routes/_layout+/assets._index"; -// eslint-disable-next-line import/no-cycle -import { useSearchParams } from "./use-search-params"; +import { useCurrentOrganization } from "../use-current-organization-id"; + +/** + * Get the types from the ReturnType of the original useSearchParams hook + */ +type SearchParamsType = ReturnType[0]; // URLSearchParams +type SetSearchParamsType = ReturnType[1]; + +export const useSearchParams = (): [ + SearchParamsType, + ( + nextInit: Parameters[0], + navigateOptions?: Parameters[1] + ) => void, +] => { + const [searchParams, setSearchParams] = remixUseSearchParams(); + const { destroyCookieValues } = useCookieDestroy(); + const isAssetIndexPage = useIsAssetIndexPage(); + const currentOrganization = useCurrentOrganization(); + + /** In those cases, we return the default searchParams and setSearchParams as we dont need to handle cookies */ + if (!isAssetIndexPage || !currentOrganization) { + return [searchParams, setSearchParams]; + } + const customSetSearchParams: ( + nextInit: Parameters[0], + navigateOptions?: Parameters[1] + ) => void = (nextInit, navigateOptions) => { + const prevParams = new URLSearchParams(searchParams.toString()); + + const checkAndDestroyCookies = (newParams: URLSearchParams) => { + const removedKeys: string[] = []; + prevParams.forEach((_value, key) => { + if (!newParams.has(key)) { + removedKeys.push(key); + } + }); + if (removedKeys.length > 0) { + destroyCookieValues(removedKeys); + } + }; + + if (typeof nextInit === "function") { + setSearchParams((prev) => { + let newParams = nextInit(prev); + // Ensure newParams is an instance of URLSearchParams + if (!(newParams instanceof URLSearchParams)) { + newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types + } + checkAndDestroyCookies(newParams); + return newParams; + }, navigateOptions); + } else { + let newParams = nextInit; + // Ensure newParams is an instance of URLSearchParams + if (!(newParams instanceof URLSearchParams)) { + newParams = new URLSearchParams(newParams as any); // Safely cast to any to handle URLSearchParamsInit types + } + checkAndDestroyCookies(newParams); + setSearchParams(newParams, navigateOptions); + } + }; + + return [searchParams, customSetSearchParams]; +}; type SetSearchParams = ( setter: (prev: URLSearchParams) => URLSearchParams @@ -13,18 +81,34 @@ type SetSearchParams = ( /** * Custom hook to gather and return metadata related to the asset index page. * - * @returns {Object} - An object containing the filters, a boolean indicating if it's the asset index page, + * @returns - An object containing the filters, a boolean indicating if it's the asset index page, * a URLSearchParams object constructed from the filters, and the organization ID. */ -export function useAssetIndexMeta() { - const location = useLocation(); - const { filters, organizationId } = useLoaderData(); - const isAssetIndexPage = location.pathname === "/assets"; - const cookieSearchParams = new URLSearchParams(filters); +export function useAssetIndexCookieSearchParams() { + const assetIndexData = useLoaderData(); + const isAssetIndexPage = useIsAssetIndexPage(); + + if (!assetIndexData || !isAssetIndexPage) { + return new URLSearchParams(); + } + + const { filters } = assetIndexData; + const cookieSearchParams = new URLSearchParams( + isAssetIndexPage && filters && filters !== "" ? filters : "" + ); - return { filters, isAssetIndexPage, cookieSearchParams, organizationId }; + return cookieSearchParams; } +/** + * Checks if the current page is the asset index page. + * @returns {boolean} - True if the current page is the asset index page, otherwise false. + */ +export const useIsAssetIndexPage = (): boolean => { + const location = useLocation(); + return location.pathname === "/assets"; +}; + /** * Returns a boolean indicating whether any of the specified keys have values * in the provided cookie search parameters. @@ -46,9 +130,10 @@ export function checkValueInCookie( * @param {string[]} keys - Array of keys (strings) to check in the URL search parameters and cookies. * @returns {boolean} - True if any of the keys have values in the search parameters or in the cookies, otherwise false. */ -export function useSearchParamHasValue(...keys: string[]) { +export function useSearchParamHasValue(...keys: string[]): boolean { const [searchParams] = useSearchParams(); - const { isAssetIndexPage, cookieSearchParams } = useAssetIndexMeta(); + const cookieSearchParams = useAssetIndexCookieSearchParams(); + const isAssetIndexPage = useIsAssetIndexPage(); const hasValue = useMemo( () => keys.map((key) => searchParams.has(key)).some(Boolean), [keys, searchParams] @@ -107,14 +192,15 @@ export function destroyCookieValues( * @param {string[]} keys - Array of keys (strings) to be cleared from the URL search parameters and cookies. * @returns {Function} - A function that, when called, clears the specified keys from the URL search parameters and, if on the asset index page, also from the cookies. */ -export function useClearValueFromParams(...keys: string[]) { +export function useClearValueFromParams(...keys: string[]): Function { const [, setSearchParams] = useSearchParams(); - const { isAssetIndexPage, organizationId, cookieSearchParams } = - useAssetIndexMeta(); + const cookieSearchParams = useAssetIndexCookieSearchParams(); + const currentOrganization = useCurrentOrganization(); + const isAssetIndexPage = useIsAssetIndexPage(); function clearValuesFromParams() { - if (isAssetIndexPage) { - destroyCookieValues(organizationId, keys, cookieSearchParams); + if (isAssetIndexPage && currentOrganization && currentOrganization?.id) { + destroyCookieValues(currentOrganization.id, keys, cookieSearchParams); deleteKeysInSearchParams(keys, setSearchParams); return; } @@ -129,8 +215,10 @@ export function useClearValueFromParams(...keys: string[]) { * @returns {Object} - An object containing the `destroyCookieValues` function that clears specific keys from cookies. */ export function useCookieDestroy() { - const { isAssetIndexPage, cookieSearchParams, organizationId } = - useAssetIndexMeta(); + const cookieSearchParams = useAssetIndexCookieSearchParams(); + const currentOrganization = useCurrentOrganization(); + // const isAssetIndexPage = useIsAssetIndexPage(); + const isAssetIndexPage = false; /** * Function to destroy specific keys from cookies if on the asset index page. @@ -139,9 +227,9 @@ export function useCookieDestroy() { */ function _destroyCookieValues(keys: string[]) { // Check if the current page is the asset index page - if (isAssetIndexPage) { + if (isAssetIndexPage && currentOrganization && currentOrganization?.id) { // Call the destroyCookieValues utility function to delete keys from cookies and update the cookie - destroyCookieValues(organizationId, keys, cookieSearchParams); + destroyCookieValues(currentOrganization.id, keys, cookieSearchParams); } } diff --git a/app/utils/use-controlled-dropdown-menu.ts b/app/hooks/use-controlled-dropdown-menu.ts similarity index 95% rename from app/utils/use-controlled-dropdown-menu.ts rename to app/hooks/use-controlled-dropdown-menu.ts index e1a18fad9..7d59322f9 100644 --- a/app/utils/use-controlled-dropdown-menu.ts +++ b/app/hooks/use-controlled-dropdown-menu.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; type UseControlledDropdownMenuReturn = { ref: React.RefObject; diff --git a/app/hooks/use-current-organization-id.ts b/app/hooks/use-current-organization-id.ts index bd6eeb4a9..01bd8e55f 100644 --- a/app/hooks/use-current-organization-id.ts +++ b/app/hooks/use-current-organization-id.ts @@ -1,6 +1,5 @@ import { useRouteLoaderData } from "@remix-run/react"; import type { loader } from "~/routes/_layout+/_layout"; -import { ShelfError } from "~/utils/error"; /** * This base hook is used to access the organization from within the _layout route @@ -11,13 +10,8 @@ export function useCurrentOrganization() { "routes/_layout+/_layout" ); - if (!layoutData) { - throw new ShelfError({ - cause: null, - message: - "Something went wrong with fetching your organization details. If the issue persists, please contact support", - label: "Organization", - }); + if (!layoutData || !layoutData.organizations) { + return undefined; } return layoutData.organizations.find( diff --git a/app/hooks/use-model-filters.ts b/app/hooks/use-model-filters.ts index d5d716ab9..bc7d8e8d1 100644 --- a/app/hooks/use-model-filters.ts +++ b/app/hooks/use-model-filters.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { User } from "@prisma/client"; import type { SerializeFrom } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { type loader, type ModelFilters } from "~/routes/api+/model-filters"; import { transformItemUsingTransformer } from "~/utils/model-filters"; import useFetcherWithReset from "./use-fetcher-with-reset"; diff --git a/app/hooks/use-pagination.ts b/app/hooks/use-pagination.ts index e61b6caf6..9f8db8747 100644 --- a/app/hooks/use-pagination.ts +++ b/app/hooks/use-pagination.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useLoaderData } from "@remix-run/react"; import type { IndexResponse } from "~/components/list"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; /** * This base hook is used to get all the pagination data and actions diff --git a/app/hooks/use-position.ts b/app/hooks/use-position.ts index 9155f2f1b..f049fb40b 100644 --- a/app/hooks/use-position.ts +++ b/app/hooks/use-position.ts @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useFetcher, useParams } from "@remix-run/react"; import { atom, useAtom } from "jotai"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; const positionAtom = atom(null); diff --git a/app/routes/_auth+/join.tsx b/app/routes/_auth+/join.tsx index e990b2116..1e6c077c0 100644 --- a/app/routes/_auth+/join.tsx +++ b/app/routes/_auth+/join.tsx @@ -14,7 +14,7 @@ import Input from "~/components/forms/input"; import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { ContinueWithEmailForm } from "~/modules/auth/components/continue-with-email-form"; import { signUpWithEmailPass } from "~/modules/auth/service.server"; import { findUserByEmail } from "~/modules/user/service.server"; diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index 53b59784a..6a5639352 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -14,7 +14,7 @@ import Input from "~/components/forms/input"; import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { ContinueWithEmailForm } from "~/modules/auth/components/continue-with-email-form"; import { signInWithEmail } from "~/modules/auth/service.server"; diff --git a/app/routes/_auth+/oauth.callback.tsx b/app/routes/_auth+/oauth.callback.tsx index fd4863d03..473f5a2a1 100644 --- a/app/routes/_auth+/oauth.callback.tsx +++ b/app/routes/_auth+/oauth.callback.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { Button } from "~/components/shared/button"; import { Spinner } from "~/components/shared/spinner"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { supabaseClient } from "~/integrations/supabase/client"; import { refreshAccessToken } from "~/modules/auth/service.server"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; diff --git a/app/routes/_auth+/otp.tsx b/app/routes/_auth+/otp.tsx index f857017d4..aecb2d974 100644 --- a/app/routes/_auth+/otp.tsx +++ b/app/routes/_auth+/otp.tsx @@ -11,7 +11,7 @@ import { z } from "zod"; import { Form } from "~/components/custom-form"; import Input from "~/components/forms/input"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { verifyOtpAndSignin } from "~/modules/auth/service.server"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { getOrganizationByUserId } from "~/modules/organization/service.server"; diff --git a/app/routes/_layout+/admin-dashboard+/qrs.tsx b/app/routes/_layout+/admin-dashboard+/qrs.tsx index 7f9c4fd9c..ec04ee3bc 100644 --- a/app/routes/_layout+/admin-dashboard+/qrs.tsx +++ b/app/routes/_layout+/admin-dashboard+/qrs.tsx @@ -18,7 +18,7 @@ import { List } from "~/components/list"; import { Filters } from "~/components/list/filters"; import { Td, Th } from "~/components/table"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { getPaginatedAndFilterableQrCodes, markBatchAsPrinted, diff --git a/app/routes/_layout+/assets.new.tsx b/app/routes/_layout+/assets.new.tsx index 8e694d50b..f16d749c1 100644 --- a/app/routes/_layout+/assets.new.tsx +++ b/app/routes/_layout+/assets.new.tsx @@ -4,7 +4,7 @@ import { useAtomValue } from "jotai"; import { dynamicTitleAtom } from "~/atoms/dynamic-title-atom"; import { AssetForm, NewAssetFormSchema } from "~/components/assets/form"; import Header from "~/components/layout/header"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { createAsset, getAllEntriesForCreateAndEdit, diff --git a/app/routes/_layout+/kits.new.tsx b/app/routes/_layout+/kits.new.tsx index 8b3c41767..71256df33 100644 --- a/app/routes/_layout+/kits.new.tsx +++ b/app/routes/_layout+/kits.new.tsx @@ -4,7 +4,7 @@ import { useAtomValue } from "jotai"; import { dynamicTitleAtom } from "~/atoms/dynamic-title-atom"; import KitsForm, { NewKitFormSchema } from "~/components/kits/form"; import Header from "~/components/layout/header"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { createKit, updateKitImage } from "~/modules/kit/service.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { sendNotification } from "~/utils/emitter/send-notification.server"; diff --git a/app/routes/_layout+/settings.team.users.invite-user.tsx b/app/routes/_layout+/settings.team.users.invite-user.tsx index 1d03b5dbf..a0f2f0af3 100644 --- a/app/routes/_layout+/settings.team.users.invite-user.tsx +++ b/app/routes/_layout+/settings.team.users.invite-user.tsx @@ -19,7 +19,7 @@ import { UserIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { Image } from "~/components/shared/image"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { useCurrentOrganization } from "~/hooks/use-current-organization-id"; import { createInvite } from "~/modules/invite/service.server"; import styles from "~/styles/layout/custom-modal.css?url"; diff --git a/app/routes/_welcome+/onboarding.tsx b/app/routes/_welcome+/onboarding.tsx index 50e67b696..7fb3f49bd 100644 --- a/app/routes/_welcome+/onboarding.tsx +++ b/app/routes/_welcome+/onboarding.tsx @@ -13,7 +13,7 @@ import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; import { onboardingEmailText } from "~/emails/onboarding-email"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { getAuthUserById, signInWithEmail, diff --git a/app/routes/qr+/$qrId_.link.tsx b/app/routes/qr+/$qrId_.link.tsx index f8a1f4ba4..c41e856b8 100644 --- a/app/routes/qr+/$qrId_.link.tsx +++ b/app/routes/qr+/$qrId_.link.tsx @@ -12,7 +12,7 @@ import HorizontalTabs from "~/components/layout/horizontal-tabs"; import { Button } from "~/components/shared/button"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { claimQrCode } from "~/modules/qr/service.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; diff --git a/app/routes/qr+/$qrId_.not-logged-in.tsx b/app/routes/qr+/$qrId_.not-logged-in.tsx index 2abe0deaa..dbafe2e92 100644 --- a/app/routes/qr+/$qrId_.not-logged-in.tsx +++ b/app/routes/qr+/$qrId_.not-logged-in.tsx @@ -4,7 +4,7 @@ import { useLoaderData } from "@remix-run/react"; import { z } from "zod"; import { CuboidIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/use-search-params"; +import { useSearchParams } from "~/hooks/search-params/utils"; import { usePosition } from "~/hooks/use-position"; import { data, getParams } from "~/utils/http.server"; From 2e0a4752e68a58d0ec34550d52d32f91e8bafbe0 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 11:21:38 +0300 Subject: [PATCH 04/27] automatic fix of import order --- app/components/assets/bulk-actions-dropdown.tsx | 2 +- app/components/category/bulk-actions-dropdown.tsx | 2 +- app/components/custom-fields/bulk-actions-dropdown.tsx | 2 +- app/components/kits/actions-dropdown.tsx | 2 +- app/components/kits/bulk-actions-dropdown.tsx | 2 +- app/components/location/bulk-actions-dropdown.tsx | 2 +- app/components/nrm/bulk-actions-dropdown.tsx | 2 +- app/components/tag/bulk-actions-dropdown.tsx | 2 +- app/components/workspace/nrm-actions-dropdown.tsx | 2 +- app/components/workspace/users-actions-dropdown.tsx | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index 1d954d623..d3660c340 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -2,10 +2,10 @@ import { useNavigation } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { isFormProcessing } from "~/utils/form"; import { isSelectingAllItems } from "~/utils/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkAssignCustodyDialog from "./bulk-assign-custody-dialog"; import BulkCategoryUpdateDialog from "./bulk-category-update-dialog"; import BulkDeleteDialog from "./bulk-delete-dialog"; diff --git a/app/components/category/bulk-actions-dropdown.tsx b/app/components/category/bulk-actions-dropdown.tsx index d4d699c87..197510c8e 100644 --- a/app/components/category/bulk-actions-dropdown.tsx +++ b/app/components/category/bulk-actions-dropdown.tsx @@ -1,8 +1,8 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; -import { tw } from "~/utils/tw"; import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; +import { tw } from "~/utils/tw"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/custom-fields/bulk-actions-dropdown.tsx b/app/components/custom-fields/bulk-actions-dropdown.tsx index 54311ef5d..8b8326167 100644 --- a/app/components/custom-fields/bulk-actions-dropdown.tsx +++ b/app/components/custom-fields/bulk-actions-dropdown.tsx @@ -2,9 +2,9 @@ import { useNavigation } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkActivateDialog from "./bulk-activate-dialog"; import BulkDeactivateDialog from "./bulk-deactivate-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; diff --git a/app/components/kits/actions-dropdown.tsx b/app/components/kits/actions-dropdown.tsx index 512469fa4..9f8e86845 100644 --- a/app/components/kits/actions-dropdown.tsx +++ b/app/components/kits/actions-dropdown.tsx @@ -1,8 +1,8 @@ import { useLoaderData } from "@remix-run/react"; import { useHydrated } from "remix-utils/use-hydrated"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import type { loader } from "~/routes/_layout+/kits.$kitId"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import DeleteKit from "./delete-kit"; import Icon from "../icons/icon"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/kits/bulk-actions-dropdown.tsx b/app/components/kits/bulk-actions-dropdown.tsx index cce9fa125..559f0e260 100644 --- a/app/components/kits/bulk-actions-dropdown.tsx +++ b/app/components/kits/bulk-actions-dropdown.tsx @@ -3,10 +3,10 @@ import { useNavigation } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { isFormProcessing } from "~/utils/form"; import { isSelectingAllItems } from "~/utils/list"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import BulkAssignCustodyDialog from "./bulk-assign-custody-dialog"; import BulkDeleteDialog from "./bulk-delete-dialog"; import BulkReleaseCustodyDialog from "./bulk-release-custody-dialog"; diff --git a/app/components/location/bulk-actions-dropdown.tsx b/app/components/location/bulk-actions-dropdown.tsx index 7f1461484..f87a4ddd7 100644 --- a/app/components/location/bulk-actions-dropdown.tsx +++ b/app/components/location/bulk-actions-dropdown.tsx @@ -1,8 +1,8 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; -import { tw } from "~/utils/tw"; import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; +import { tw } from "~/utils/tw"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/nrm/bulk-actions-dropdown.tsx b/app/components/nrm/bulk-actions-dropdown.tsx index 42c950337..c7f885568 100644 --- a/app/components/nrm/bulk-actions-dropdown.tsx +++ b/app/components/nrm/bulk-actions-dropdown.tsx @@ -1,8 +1,8 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsAtom } from "~/atoms/list"; -import { tw } from "~/utils/tw"; import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; +import { tw } from "~/utils/tw"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/tag/bulk-actions-dropdown.tsx b/app/components/tag/bulk-actions-dropdown.tsx index 99a9c23ff..b676ae573 100644 --- a/app/components/tag/bulk-actions-dropdown.tsx +++ b/app/components/tag/bulk-actions-dropdown.tsx @@ -1,8 +1,8 @@ import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; import { selectedBulkItemsCountAtom } from "~/atoms/list"; -import { tw } from "~/utils/tw"; import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; +import { tw } from "~/utils/tw"; import BulkDeleteDialog from "./bulk-delete-dialog"; import { BulkUpdateDialogTrigger } from "../bulk-update-dialog/bulk-update-dialog"; import { ChevronRight } from "../icons/library"; diff --git a/app/components/workspace/nrm-actions-dropdown.tsx b/app/components/workspace/nrm-actions-dropdown.tsx index 08f29a990..d9655d4cc 100644 --- a/app/components/workspace/nrm-actions-dropdown.tsx +++ b/app/components/workspace/nrm-actions-dropdown.tsx @@ -8,9 +8,9 @@ import { DropdownMenuTrigger, } from "~/components/shared/dropdown"; +import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import type { loader } from "~/routes/_layout+/settings.team.nrm"; import { tw } from "~/utils/tw"; -import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; import { DeleteMember } from "./delete-member"; import { Button } from "../shared/button"; diff --git a/app/components/workspace/users-actions-dropdown.tsx b/app/components/workspace/users-actions-dropdown.tsx index 7d1e71229..08dd3ad22 100644 --- a/app/components/workspace/users-actions-dropdown.tsx +++ b/app/components/workspace/users-actions-dropdown.tsx @@ -12,8 +12,8 @@ import { DropdownMenuTrigger, } from "~/components/shared/dropdown"; -import { isFormProcessing } from "~/utils/form"; import { useControlledDropdownMenu } from "~/hooks/use-controlled-dropdown-menu"; +import { isFormProcessing } from "~/utils/form"; import { Button } from "../shared/button"; import { Spinner } from "../shared/spinner"; From 145c940176761a1e2ac2907591edaafc48f5c4fa Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 11:32:23 +0300 Subject: [PATCH 05/27] small text adjustment --- app/components/subscription/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/subscription/helpers.ts b/app/components/subscription/helpers.ts index b09a16cc1..ce88b22fa 100644 --- a/app/components/subscription/helpers.ts +++ b/app/components/subscription/helpers.ts @@ -12,7 +12,7 @@ export const FREE_PLAN = { Automatic Upgrades, Server Maintenance `, - slogan: "Free forever. No credit card required.", + slogan: "For personal use or hobby use.", }, }, unit_amount: 0, From afe3a79595112be6f7b303927291d7181820d8d5 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 11:35:33 +0300 Subject: [PATCH 06/27] another file rename --- app/components/booking/availability-select.tsx | 2 +- app/components/booking/status-filter.tsx | 2 +- app/components/bulk-update-dialog/bulk-update-dialog.tsx | 2 +- .../list/filters/category/category-checkbox-dropdown.tsx | 2 +- app/components/list/filters/search-form.tsx | 2 +- app/components/list/filters/sort-by.tsx | 2 +- app/components/list/filters/tag/tag-checkbox-dropdown.tsx | 2 +- app/components/list/pagination/page-number.tsx | 2 +- app/components/list/pagination/per-page-items-select.tsx | 2 +- app/components/subscription/successful-subscription-modal.tsx | 2 +- app/components/welcome/carousel.tsx | 3 +-- app/hooks/search-params/{utils.ts => index.ts} | 0 app/hooks/use-controlled-dropdown-menu.ts | 2 +- app/hooks/use-model-filters.ts | 2 +- app/hooks/use-pagination.ts | 2 +- app/hooks/use-position.ts | 2 +- app/routes/_auth+/join.tsx | 2 +- app/routes/_auth+/login.tsx | 2 +- app/routes/_auth+/oauth.callback.tsx | 2 +- app/routes/_auth+/otp.tsx | 2 +- app/routes/_layout+/admin-dashboard+/qrs.tsx | 2 +- app/routes/_layout+/assets._index.tsx | 2 +- app/routes/_layout+/assets.new.tsx | 2 +- app/routes/_layout+/kits.new.tsx | 2 +- app/routes/_layout+/settings.team.users.invite-user.tsx | 2 +- app/routes/_welcome+/onboarding.tsx | 2 +- app/routes/qr+/$qrId_.link.asset.tsx | 2 +- app/routes/qr+/$qrId_.link.tsx | 2 +- app/routes/qr+/$qrId_.not-logged-in.tsx | 2 +- 29 files changed, 28 insertions(+), 29 deletions(-) rename app/hooks/search-params/{utils.ts => index.ts} (100%) diff --git a/app/components/booking/availability-select.tsx b/app/components/booking/availability-select.tsx index cc32dd98f..849f2909f 100644 --- a/app/components/booking/availability-select.tsx +++ b/app/components/booking/availability-select.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { Select, diff --git a/app/components/booking/status-filter.tsx b/app/components/booking/status-filter.tsx index 16e97984a..f72ede7b5 100644 --- a/app/components/booking/status-filter.tsx +++ b/app/components/booking/status-filter.tsx @@ -1,5 +1,5 @@ import { useNavigation } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { isFormProcessing } from "~/utils/form"; import { Select, diff --git a/app/components/bulk-update-dialog/bulk-update-dialog.tsx b/app/components/bulk-update-dialog/bulk-update-dialog.tsx index 69132f477..d0a71b649 100644 --- a/app/components/bulk-update-dialog/bulk-update-dialog.tsx +++ b/app/components/bulk-update-dialog/bulk-update-dialog.tsx @@ -11,7 +11,7 @@ import { selectedBulkItemsAtom, selectedBulkItemsCountAtom, } from "~/atoms/list"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { action } from "~/routes/api+/assets.bulk-update-location"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/list/filters/category/category-checkbox-dropdown.tsx b/app/components/list/filters/category/category-checkbox-dropdown.tsx index 0eb64d05b..d21cbd450 100644 --- a/app/components/list/filters/category/category-checkbox-dropdown.tsx +++ b/app/components/list/filters/category/category-checkbox-dropdown.tsx @@ -7,7 +7,7 @@ import { CategorySelectNoCategories } from "~/components/category/category-selec import { Badge } from "~/components/shared/badge"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { WithDateFields } from "~/modules/types"; import { useCategorySearch } from "../../../category/useCategorySearch"; import Input from "../../../forms/input"; diff --git a/app/components/list/filters/search-form.tsx b/app/components/list/filters/search-form.tsx index d7c26504a..a0a0a3c79 100644 --- a/app/components/list/filters/search-form.tsx +++ b/app/components/list/filters/search-form.tsx @@ -3,7 +3,7 @@ import { useLoaderData, useNavigation } from "@remix-run/react"; import Input from "~/components/forms/input"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { SearchableIndexResponse } from "~/modules/types"; import { isSearching } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/list/filters/sort-by.tsx b/app/components/list/filters/sort-by.tsx index e53bc7ebc..b0194e10b 100644 --- a/app/components/list/filters/sort-by.tsx +++ b/app/components/list/filters/sort-by.tsx @@ -6,7 +6,7 @@ import { PopoverTrigger, } from "@radix-ui/react-popover"; import { useNavigation } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { isFormProcessing } from "~/utils/form"; import { tw } from "~/utils/tw"; diff --git a/app/components/list/filters/tag/tag-checkbox-dropdown.tsx b/app/components/list/filters/tag/tag-checkbox-dropdown.tsx index 4eca54c0f..9ab1d6e30 100644 --- a/app/components/list/filters/tag/tag-checkbox-dropdown.tsx +++ b/app/components/list/filters/tag/tag-checkbox-dropdown.tsx @@ -6,7 +6,7 @@ import { useAtom, useAtomValue } from "jotai"; import { useTagSearch } from "~/components/category/useTagSearch"; import { Badge } from "~/components/shared/badge"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { WithDateFields } from "~/modules/types"; import Input from "../../../forms/input"; import { CheckIcon, ChevronRight } from "../../../icons/library"; diff --git a/app/components/list/pagination/page-number.tsx b/app/components/list/pagination/page-number.tsx index 27a4fbc88..c07d2b700 100644 --- a/app/components/list/pagination/page-number.tsx +++ b/app/components/list/pagination/page-number.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { NavLink } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { getParamsValues } from "~/utils/list"; import { mergeSearchParams } from "~/utils/merge-search-params"; diff --git a/app/components/list/pagination/per-page-items-select.tsx b/app/components/list/pagination/per-page-items-select.tsx index bf40d7fc6..26cd3a0e7 100644 --- a/app/components/list/pagination/per-page-items-select.tsx +++ b/app/components/list/pagination/per-page-items-select.tsx @@ -6,7 +6,7 @@ import { SelectContent, SelectItem, } from "~/components/forms/select"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { loader } from "~/routes/_layout+/assets._index"; diff --git a/app/components/subscription/successful-subscription-modal.tsx b/app/components/subscription/successful-subscription-modal.tsx index 26cb3173c..2e72ffd64 100644 --- a/app/components/subscription/successful-subscription-modal.tsx +++ b/app/components/subscription/successful-subscription-modal.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { useLoaderData } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import type { loader } from "~/routes/_layout+/account-details.subscription"; import { Button } from "../shared/button"; diff --git a/app/components/welcome/carousel.tsx b/app/components/welcome/carousel.tsx index f4a088040..604fb4106 100644 --- a/app/components/welcome/carousel.tsx +++ b/app/components/welcome/carousel.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; - import { ClientOnly } from "remix-utils/client-only"; import { Pagination, Navigation } from "swiper/modules"; import { Swiper, SwiperSlide } from "swiper/react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { Button } from "../shared/button"; export default function WelcomeCarousel() { diff --git a/app/hooks/search-params/utils.ts b/app/hooks/search-params/index.ts similarity index 100% rename from app/hooks/search-params/utils.ts rename to app/hooks/search-params/index.ts diff --git a/app/hooks/use-controlled-dropdown-menu.ts b/app/hooks/use-controlled-dropdown-menu.ts index 7d59322f9..a6146eaa7 100644 --- a/app/hooks/use-controlled-dropdown-menu.ts +++ b/app/hooks/use-controlled-dropdown-menu.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; type UseControlledDropdownMenuReturn = { ref: React.RefObject; diff --git a/app/hooks/use-model-filters.ts b/app/hooks/use-model-filters.ts index bc7d8e8d1..b508d0509 100644 --- a/app/hooks/use-model-filters.ts +++ b/app/hooks/use-model-filters.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { User } from "@prisma/client"; import type { SerializeFrom } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { type loader, type ModelFilters } from "~/routes/api+/model-filters"; import { transformItemUsingTransformer } from "~/utils/model-filters"; import useFetcherWithReset from "./use-fetcher-with-reset"; diff --git a/app/hooks/use-pagination.ts b/app/hooks/use-pagination.ts index 9f8db8747..30becafca 100644 --- a/app/hooks/use-pagination.ts +++ b/app/hooks/use-pagination.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useLoaderData } from "@remix-run/react"; import type { IndexResponse } from "~/components/list"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; /** * This base hook is used to get all the pagination data and actions diff --git a/app/hooks/use-position.ts b/app/hooks/use-position.ts index f049fb40b..32d76fe89 100644 --- a/app/hooks/use-position.ts +++ b/app/hooks/use-position.ts @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useFetcher, useParams } from "@remix-run/react"; import { atom, useAtom } from "jotai"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; const positionAtom = atom(null); diff --git a/app/routes/_auth+/join.tsx b/app/routes/_auth+/join.tsx index 1e6c077c0..70d5e0683 100644 --- a/app/routes/_auth+/join.tsx +++ b/app/routes/_auth+/join.tsx @@ -14,7 +14,7 @@ import Input from "~/components/forms/input"; import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { ContinueWithEmailForm } from "~/modules/auth/components/continue-with-email-form"; import { signUpWithEmailPass } from "~/modules/auth/service.server"; import { findUserByEmail } from "~/modules/user/service.server"; diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index 6a5639352..6a19a350a 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -14,7 +14,7 @@ import Input from "~/components/forms/input"; import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { ContinueWithEmailForm } from "~/modules/auth/components/continue-with-email-form"; import { signInWithEmail } from "~/modules/auth/service.server"; diff --git a/app/routes/_auth+/oauth.callback.tsx b/app/routes/_auth+/oauth.callback.tsx index 473f5a2a1..26cc56111 100644 --- a/app/routes/_auth+/oauth.callback.tsx +++ b/app/routes/_auth+/oauth.callback.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { Button } from "~/components/shared/button"; import { Spinner } from "~/components/shared/spinner"; import { config } from "~/config/shelf.config"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { supabaseClient } from "~/integrations/supabase/client"; import { refreshAccessToken } from "~/modules/auth/service.server"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; diff --git a/app/routes/_auth+/otp.tsx b/app/routes/_auth+/otp.tsx index aecb2d974..2c03bd8af 100644 --- a/app/routes/_auth+/otp.tsx +++ b/app/routes/_auth+/otp.tsx @@ -11,7 +11,7 @@ import { z } from "zod"; import { Form } from "~/components/custom-form"; import Input from "~/components/forms/input"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { verifyOtpAndSignin } from "~/modules/auth/service.server"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { getOrganizationByUserId } from "~/modules/organization/service.server"; diff --git a/app/routes/_layout+/admin-dashboard+/qrs.tsx b/app/routes/_layout+/admin-dashboard+/qrs.tsx index ec04ee3bc..f925511d1 100644 --- a/app/routes/_layout+/admin-dashboard+/qrs.tsx +++ b/app/routes/_layout+/admin-dashboard+/qrs.tsx @@ -18,7 +18,7 @@ import { List } from "~/components/list"; import { Filters } from "~/components/list/filters"; import { Td, Th } from "~/components/table"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { getPaginatedAndFilterableQrCodes, markBatchAsPrinted, diff --git a/app/routes/_layout+/assets._index.tsx b/app/routes/_layout+/assets._index.tsx index aef5b88a0..866425d8e 100644 --- a/app/routes/_layout+/assets._index.tsx +++ b/app/routes/_layout+/assets._index.tsx @@ -40,7 +40,7 @@ import { db } from "~/database/db.server"; import { useClearValueFromParams, useSearchParamHasValue, -} from "~/hooks/search-params/utils"; +} from "~/hooks/search-params"; import { useUserRoleHelper } from "~/hooks/user-user-role-helper"; import { bulkDeleteAssets, diff --git a/app/routes/_layout+/assets.new.tsx b/app/routes/_layout+/assets.new.tsx index f16d749c1..3b21e6e41 100644 --- a/app/routes/_layout+/assets.new.tsx +++ b/app/routes/_layout+/assets.new.tsx @@ -4,7 +4,7 @@ import { useAtomValue } from "jotai"; import { dynamicTitleAtom } from "~/atoms/dynamic-title-atom"; import { AssetForm, NewAssetFormSchema } from "~/components/assets/form"; import Header from "~/components/layout/header"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { createAsset, getAllEntriesForCreateAndEdit, diff --git a/app/routes/_layout+/kits.new.tsx b/app/routes/_layout+/kits.new.tsx index 71256df33..15dc75390 100644 --- a/app/routes/_layout+/kits.new.tsx +++ b/app/routes/_layout+/kits.new.tsx @@ -4,7 +4,7 @@ import { useAtomValue } from "jotai"; import { dynamicTitleAtom } from "~/atoms/dynamic-title-atom"; import KitsForm, { NewKitFormSchema } from "~/components/kits/form"; import Header from "~/components/layout/header"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { createKit, updateKitImage } from "~/modules/kit/service.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { sendNotification } from "~/utils/emitter/send-notification.server"; diff --git a/app/routes/_layout+/settings.team.users.invite-user.tsx b/app/routes/_layout+/settings.team.users.invite-user.tsx index a0f2f0af3..16332a0ff 100644 --- a/app/routes/_layout+/settings.team.users.invite-user.tsx +++ b/app/routes/_layout+/settings.team.users.invite-user.tsx @@ -19,7 +19,7 @@ import { UserIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { Image } from "~/components/shared/image"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { useCurrentOrganization } from "~/hooks/use-current-organization-id"; import { createInvite } from "~/modules/invite/service.server"; import styles from "~/styles/layout/custom-modal.css?url"; diff --git a/app/routes/_welcome+/onboarding.tsx b/app/routes/_welcome+/onboarding.tsx index 7fb3f49bd..238d174d0 100644 --- a/app/routes/_welcome+/onboarding.tsx +++ b/app/routes/_welcome+/onboarding.tsx @@ -13,7 +13,7 @@ import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; import { onboardingEmailText } from "~/emails/onboarding-email"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { getAuthUserById, signInWithEmail, diff --git a/app/routes/qr+/$qrId_.link.asset.tsx b/app/routes/qr+/$qrId_.link.asset.tsx index 4638aea98..1c7e3c185 100644 --- a/app/routes/qr+/$qrId_.link.asset.tsx +++ b/app/routes/qr+/$qrId_.link.asset.tsx @@ -32,7 +32,7 @@ import When from "~/components/when/when"; import { useClearValueFromParams, useSearchParamHasValue, -} from "~/hooks/search-params/utils"; +} from "~/hooks/search-params"; import { useViewportHeight } from "~/hooks/use-viewport-height"; import { getPaginatedAndFilterableAssets, diff --git a/app/routes/qr+/$qrId_.link.tsx b/app/routes/qr+/$qrId_.link.tsx index c41e856b8..66c7b9bad 100644 --- a/app/routes/qr+/$qrId_.link.tsx +++ b/app/routes/qr+/$qrId_.link.tsx @@ -12,7 +12,7 @@ import HorizontalTabs from "~/components/layout/horizontal-tabs"; import { Button } from "~/components/shared/button"; import { db } from "~/database/db.server"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { claimQrCode } from "~/modules/qr/service.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; diff --git a/app/routes/qr+/$qrId_.not-logged-in.tsx b/app/routes/qr+/$qrId_.not-logged-in.tsx index dbafe2e92..83de322a3 100644 --- a/app/routes/qr+/$qrId_.not-logged-in.tsx +++ b/app/routes/qr+/$qrId_.not-logged-in.tsx @@ -4,7 +4,7 @@ import { useLoaderData } from "@remix-run/react"; import { z } from "zod"; import { CuboidIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; -import { useSearchParams } from "~/hooks/search-params/utils"; +import { useSearchParams } from "~/hooks/search-params"; import { usePosition } from "~/hooks/use-position"; import { data, getParams } from "~/utils/http.server"; From 5698e642029d27038252dfbdc94b143171d7af95 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 15:10:46 +0300 Subject: [PATCH 07/27] implement isQrId validation function --- app/modules/asset/service.server.ts | 2 +- app/modules/kit/service.server.ts | 2 +- app/modules/qr/service.server.ts | 2 +- app/utils/constants.ts | 1 + app/utils/{ => id}/id.server.ts | 8 ++++---- app/utils/id/utils.ts | 31 +++++++++++++++++++++++++++++ server/middleware.ts | 18 ++--------------- 7 files changed, 41 insertions(+), 23 deletions(-) rename app/utils/{ => id}/id.server.ts (83%) create mode 100644 app/utils/id/utils.ts diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index 8248518e3..ad71ddfee 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -50,7 +50,7 @@ import { maybeUniqueConstraintViolation, } from "~/utils/error"; import { getCurrentSearchParams } from "~/utils/http.server"; -import { id } from "~/utils/id.server"; +import { id } from "~/utils/id/id.server"; import { ALL_SELECTED_KEY, getParamsValues } from "~/utils/list"; import { Logger } from "~/utils/logger"; import { oneDayFromNow } from "~/utils/one-week-from-now"; diff --git a/app/modules/kit/service.server.ts b/app/modules/kit/service.server.ts index a75bd30a2..bb6c70674 100644 --- a/app/modules/kit/service.server.ts +++ b/app/modules/kit/service.server.ts @@ -24,7 +24,7 @@ import type { ErrorLabel } from "~/utils/error"; import { maybeUniqueConstraintViolation, ShelfError } from "~/utils/error"; import { extractImageNameFromSupabaseUrl } from "~/utils/extract-image-name-from-supabase-url"; import { getCurrentSearchParams } from "~/utils/http.server"; -import { id } from "~/utils/id.server"; +import { id } from "~/utils/id/id.server"; import { ALL_SELECTED_KEY, getParamsValues } from "~/utils/list"; import { Logger } from "~/utils/logger"; import { oneDayFromNow } from "~/utils/one-week-from-now"; diff --git a/app/modules/qr/service.server.ts b/app/modules/qr/service.server.ts index 3ce307a83..face61035 100644 --- a/app/modules/qr/service.server.ts +++ b/app/modules/qr/service.server.ts @@ -13,7 +13,7 @@ import { updateCookieWithPerPage } from "~/utils/cookies.server"; import type { ErrorLabel } from "~/utils/error"; import { isLikeShelfError, ShelfError } from "~/utils/error"; import { getCurrentSearchParams } from "~/utils/http.server"; -import { id } from "~/utils/id.server"; +import { id } from "~/utils/id/id.server"; import { getParamsValues } from "~/utils/list"; // eslint-disable-next-line import/no-cycle import { generateCode } from "./utils.server"; diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 0c83beae5..b068d9550 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -6,3 +6,4 @@ export const INVITE_EXPIRY_TTL_DAYS = 5; /** Default length of custom cuid2 */ export const DEFAULT_CUID_LENGTH = 10; +export const LEGACY_CUID_LENGTH = 25; diff --git a/app/utils/id.server.ts b/app/utils/id/id.server.ts similarity index 83% rename from app/utils/id.server.ts rename to app/utils/id/id.server.ts index d624d5cd2..3f74f777d 100644 --- a/app/utils/id.server.ts +++ b/app/utils/id/id.server.ts @@ -1,8 +1,8 @@ import { init } from "@paralleldrive/cuid2"; -import { DEFAULT_CUID_LENGTH } from "./constants"; -import { FINGERPRINT } from "./env"; -import { ShelfError } from "./error"; -import { Logger } from "./logger"; +import { DEFAULT_CUID_LENGTH } from "../constants"; +import { FINGERPRINT } from "../env"; +import { ShelfError } from "../error"; +import { Logger } from "../logger"; /** * Generate a unique id using cuid2 diff --git a/app/utils/id/utils.ts b/app/utils/id/utils.ts new file mode 100644 index 000000000..916f51620 --- /dev/null +++ b/app/utils/id/utils.ts @@ -0,0 +1,31 @@ +import { DEFAULT_CUID_LENGTH, LEGACY_CUID_LENGTH } from "../constants"; + +/** + * Checks if a string is a valid QR id + * QR id is a 10 character string + * Legacy QR id is a + */ +export function isQrId(id: string): boolean { + const possibleLengths = [DEFAULT_CUID_LENGTH, LEGACY_CUID_LENGTH]; + const length = id.length; + + /** + * 1. The string must contain only lowercase letters and digits. + * 2. The string must start with a lowercase letter. + * 3. The string must contain at least one digit. + */ + const regex = /^(?=.*\d)[a-z][0-9a-z]*$/; + + try { + if ( + typeof id === "string" && + possibleLengths.includes(length) && + regex.test(id) + ) + return true; + } finally { + /* empty */ + } + + return false; +} diff --git a/server/middleware.ts b/server/middleware.ts index f3f2ce1d0..b6ad61bcc 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -1,4 +1,3 @@ -import { isCuid } from "@paralleldrive/cuid2"; import { createMiddleware } from "hono/factory"; import { pathToRegexp } from "path-to-regexp"; import { getSession } from "remix-hono/session"; @@ -7,9 +6,9 @@ import { refreshAccessToken, validateSession, } from "~/modules/auth/service.server"; -import { DEFAULT_CUID_LENGTH } from "~/utils/constants"; import { ShelfError } from "~/utils/error"; import { safeRedirect } from "~/utils/http.server"; +import { isQrId } from "~/utils/id/utils"; import { Logger } from "~/utils/logger"; import type { FlashData } from "./session"; import { authSessionKey } from "./session"; @@ -151,14 +150,9 @@ export function urlShortener({ excludePaths }: { excludePaths: string[] }) { const pathParts = pathWithoutShortener.split("/").filter(Boolean); const pathname = "/" + pathParts.join("/"); - console.log(`urlShortener middleware: Processing ${pathname}`); - // Check if the current request path matches any of the excluded paths const isExcluded = excludePaths.some((path) => pathname.startsWith(path)); if (isExcluded) { - console.log( - `urlShortener middleware: Skipping excluded path ${pathname}` - ); return next(); } @@ -166,21 +160,13 @@ export function urlShortener({ excludePaths }: { excludePaths: string[] }) { const serverUrl = process.env.SERVER_URL; // Check if the path is a single segment and a valid CUID - if ( - pathParts.length === 1 && - isCuid(path, { - minLength: DEFAULT_CUID_LENGTH, - maxLength: DEFAULT_CUID_LENGTH, - }) - ) { + if (pathParts.length === 1 && isQrId(path)) { const redirectUrl = `${serverUrl}/qr/${path}`; - console.log(`urlShortener middleware: Redirecting QR to ${redirectUrl}`); return c.redirect(safeRedirect(redirectUrl)); } // Handle all other cases const redirectUrl = `${serverUrl}${pathname}`; - console.log(`urlShortener middleware: Redirecting to ${redirectUrl}`); return c.redirect(safeRedirect(redirectUrl)); }); } From 895cddb1c106df59ae4683d09580d93822a21927 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 15:28:05 +0300 Subject: [PATCH 08/27] further improvements --- app/utils/id/{utils.ts => index.ts} | 23 +++++++++++------------ server/middleware.ts | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) rename app/utils/id/{utils.ts => index.ts} (65%) diff --git a/app/utils/id/utils.ts b/app/utils/id/index.ts similarity index 65% rename from app/utils/id/utils.ts rename to app/utils/id/index.ts index 916f51620..f1bcd3222 100644 --- a/app/utils/id/utils.ts +++ b/app/utils/id/index.ts @@ -1,9 +1,10 @@ import { DEFAULT_CUID_LENGTH, LEGACY_CUID_LENGTH } from "../constants"; /** - * Checks if a string is a valid QR id + * Checks if a string is a valid QR id. + * * QR id is a 10 character string - * Legacy QR id is a + * Legacy QR id is a 25 character string */ export function isQrId(id: string): boolean { const possibleLengths = [DEFAULT_CUID_LENGTH, LEGACY_CUID_LENGTH]; @@ -16,16 +17,14 @@ export function isQrId(id: string): boolean { */ const regex = /^(?=.*\d)[a-z][0-9a-z]*$/; - try { - if ( - typeof id === "string" && - possibleLengths.includes(length) && - regex.test(id) - ) - return true; - } finally { - /* empty */ + // Validate the ID against the criteria + if ( + typeof id !== "string" || + !possibleLengths.includes(length) || + !regex.test(id) + ) { + return false; } - return false; + return true; } diff --git a/server/middleware.ts b/server/middleware.ts index b6ad61bcc..d5d5ed885 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -8,7 +8,7 @@ import { } from "~/modules/auth/service.server"; import { ShelfError } from "~/utils/error"; import { safeRedirect } from "~/utils/http.server"; -import { isQrId } from "~/utils/id/utils"; +import { isQrId } from "~/utils/id"; import { Logger } from "~/utils/logger"; import type { FlashData } from "./session"; import { authSessionKey } from "./session"; From bf00b4591d8e1c006b17042c15e3e1a00c4deba9 Mon Sep 17 00:00:00 2001 From: hunar Date: Wed, 31 Jul 2024 17:58:06 +0530 Subject: [PATCH 09/27] added a createScanNote function which creates a note whenever a scan is created --- app/modules/scan/service.server.ts | 69 +++++++++++++++++++++++++++++- app/routes/api+/asset.scan.ts | 1 + app/routes/qr+/$qrId.tsx | 1 + 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/app/modules/scan/service.server.ts b/app/modules/scan/service.server.ts index 08275bf86..7d67bb90e 100644 --- a/app/modules/scan/service.server.ts +++ b/app/modules/scan/service.server.ts @@ -2,6 +2,9 @@ import type { Scan } from "@prisma/client"; import { db } from "~/database/db.server"; import { ShelfError } from "~/utils/error"; import type { ErrorLabel } from "~/utils/error"; +import { createNote } from "../note/service.server"; +import { getOrganizationById } from "../organization/service.server"; +import { getQr } from "../qr/service.server"; const label: ErrorLabel = "Scan"; @@ -34,7 +37,7 @@ export async function createScan(params: { }; /** If user id is passed, connect to that user */ - if (userId) { + if (userId && userId != "anonymous") { Object.assign(data, { user: { connect: { @@ -59,9 +62,13 @@ export async function createScan(params: { }); } - return await db.scan.create({ + const scan = await db.scan.create({ data, }); + + await createScanNote({ userId, qrId, longitude, latitude, manuallyGenerated }); + + return scan; } catch (cause) { throw new ShelfError({ cause, @@ -138,3 +145,61 @@ export async function getScanByQrId({ qrId }: { qrId: string }) { }); } } + +export async function createScanNote({ userId, qrId, latitude, longitude, manuallyGenerated }: { + userId?: string | null; + qrId: string; + latitude?: Scan["latitude"]; + longitude?: Scan["longitude"]; + manuallyGenerated?: boolean; +}) { + try { + let message = ""; + const { assetId, organizationId } = await getQr(qrId) + if (assetId) { + if (userId && userId != "anonymous") { + const user = await db.user.findFirst({ + where: { + id: userId, + }, + select: { + firstName: true, + lastName: true, + }, + }); + + const userName = user?.firstName + " " + user?.lastName + if (manuallyGenerated) { + message = `**${userName}** manually updated the GPS coordinates to **${latitude} ${longitude}**` + } else { + message = `**${userName}** performed a scan of the asset QR code` + } + return await createNote({ + content: message, + type: "UPDATE", + userId, + assetId + }); + } else { + if (organizationId) { + const { userId: ownerId } = await getOrganizationById(organizationId) + message = "An unknown user has performed a scan of the asset QR code" + return await createNote({ + content: message, + type: "UPDATE", + userId: ownerId, + assetId + }); + } + } + } + } + catch (cause) { + throw new ShelfError({ + cause, + message: "Something went wrong while creating a scan note", + additionalData: { userId, qrId, latitude, longitude, manuallyGenerated }, + label, + }); + } +} \ No newline at end of file diff --git a/app/routes/api+/asset.scan.ts b/app/routes/api+/asset.scan.ts index 78ba3912d..dbbd7b868 100644 --- a/app/routes/api+/asset.scan.ts +++ b/app/routes/api+/asset.scan.ts @@ -49,6 +49,7 @@ export async function action({ context, request }: ActionFunctionArgs) { await createScan({ userAgent: request.headers.get("user-agent") as string, + userId: userId, qrId: qr.id, deleted: false, latitude: latitude, diff --git a/app/routes/qr+/$qrId.tsx b/app/routes/qr+/$qrId.tsx index 92d69c0ca..b2cef97be 100644 --- a/app/routes/qr+/$qrId.tsx +++ b/app/routes/qr+/$qrId.tsx @@ -39,6 +39,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { */ const scan = await createScan({ userAgent: request.headers.get("user-agent") as string, + userId, qrId: id, deleted: !qr, }); From c7dfb06c95d8ecf51dca85d45f5fa6aba4764fcb Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 16:34:34 +0300 Subject: [PATCH 10/27] adjusting safeRedirect to handle safelist of domains --- app/utils/http.server.ts | 18 ++++++++++++------ server/middleware.ts | 11 +++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/utils/http.server.ts b/app/utils/http.server.ts index 817606d8f..1d0593878 100644 --- a/app/utils/http.server.ts +++ b/app/utils/http.server.ts @@ -3,6 +3,7 @@ import { json } from "react-router"; import { parseFormAny } from "react-zorm"; import type { ZodType } from "zod"; import { sendNotification } from "./emitter/send-notification.server"; +import { SERVER_URL, URL_SHORTENER } from "./env"; import type { Options } from "./error"; import { ShelfError, @@ -155,12 +156,17 @@ export function safeRedirect( to: FormDataEntryValue | string | null | undefined, defaultRedirect = "/" ) { - if ( - !to || - typeof to !== "string" || - !to.startsWith("/") || - to.startsWith("//") - ) { + /** List of domains we allow to redirect to + */ + const safeList = [SERVER_URL, `https://${URL_SHORTENER}`]; + + if (!to || typeof to !== "string" || to.startsWith("//")) { + return defaultRedirect; + } + + // Check if the URL starts with any of the safe domains + const isSafeDomain = safeList.some((safeUrl) => to.startsWith(safeUrl)); + if (!to.startsWith("/") && !isSafeDomain) { return defaultRedirect; } diff --git a/server/middleware.ts b/server/middleware.ts index d5d5ed885..5a6a18e72 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -150,9 +150,14 @@ export function urlShortener({ excludePaths }: { excludePaths: string[] }) { const pathParts = pathWithoutShortener.split("/").filter(Boolean); const pathname = "/" + pathParts.join("/"); + // console.log(`urlShortener middleware: Processing ${pathname}`); + // Check if the current request path matches any of the excluded paths const isExcluded = excludePaths.some((path) => pathname.startsWith(path)); if (isExcluded) { + // console.log( + // `urlShortener middleware: Skipping excluded path ${pathname}` + // ); return next(); } @@ -162,11 +167,13 @@ export function urlShortener({ excludePaths }: { excludePaths: string[] }) { // Check if the path is a single segment and a valid CUID if (pathParts.length === 1 && isQrId(path)) { const redirectUrl = `${serverUrl}/qr/${path}`; - return c.redirect(safeRedirect(redirectUrl)); + // console.log(`urlShortener middleware: Redirecting QR to ${redirectUrl}`); + return c.redirect(safeRedirect(redirectUrl), 301); } // Handle all other cases const redirectUrl = `${serverUrl}${pathname}`; - return c.redirect(safeRedirect(redirectUrl)); + // console.log(`urlShortener middleware: Redirecting to ${redirectUrl}`); + return c.redirect(safeRedirect(redirectUrl), 301); }); } From de7e273962b6bd87ee3df266d255aa4f8abbe042 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 31 Jul 2024 16:37:03 +0300 Subject: [PATCH 11/27] 1 more safety check --- server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index d451ea5b8..e2633521b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -41,7 +41,7 @@ const app = new Hono({ const url = new URL(req.url); const host = req.headers.get("host"); - if (host === process.env.URL_SHORTENER) { + if (process.env.URL_SHORTENER && host === process.env.URL_SHORTENER) { return "/" + host + url.pathname; } From 914938562c391616ed5a53cc8cb12caf48aa0c32 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 13:11:48 +0300 Subject: [PATCH 12/27] fixing .env.example to show correct shortner url --- .env.example | 3 ++- docs/url-shortener.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 43ba4ab37..7f80df44d 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,8 @@ SERVER_URL="http://localhost:3000" # Used for shortening URLs of QR codes. Optional # If present, the QR code will be generated with a shortened URL -# URL_SHORTENER="https://hey.lo/" +# Should not include the protocol (http/https) or a trailing slash +# URL_SHORTENER="eam.sh" APP_NAME="Shelf" diff --git a/docs/url-shortener.md b/docs/url-shortener.md index bb755fe6d..d03221f1e 100644 --- a/docs/url-shortener.md +++ b/docs/url-shortener.md @@ -8,6 +8,6 @@ The redirection is handled via the `urlShortener` middleware. Using the shortener is super easy. -1. Add an env variable with your shortner domain: `URL_SHORTENER="https://hey.lo/"`. You can refer to `.env.example` for further example +1. Add an env variable with your shortner domain: `URL_SHORTENER="hey.lo"`. You can refer to `.env.example` for further example. The domain should not include the protocol (http/https) or a trailing slash 2. Make sure you point your short domain to your application server 3. Enjoy From 73a8e960a76543f417d70ae6390a8c69397e0e10 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 13:12:33 +0300 Subject: [PATCH 13/27] mend --- docs/url-shortener.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/url-shortener.md b/docs/url-shortener.md index d03221f1e..6082a7f42 100644 --- a/docs/url-shortener.md +++ b/docs/url-shortener.md @@ -8,6 +8,6 @@ The redirection is handled via the `urlShortener` middleware. Using the shortener is super easy. -1. Add an env variable with your shortner domain: `URL_SHORTENER="hey.lo"`. You can refer to `.env.example` for further example. The domain should not include the protocol (http/https) or a trailing slash +1. Add an env variable with your shortner domain: `URL_SHORTENER="eam.sh"`. You can refer to `.env.example` for further example. The domain should not include the protocol (http/https) or a trailing slash 2. Make sure you point your short domain to your application server 3. Enjoy From e98e110ffb5b46130f8bebee64cc454e6bf16332 Mon Sep 17 00:00:00 2001 From: hunar Date: Thu, 1 Aug 2024 16:24:29 +0530 Subject: [PATCH 14/27] resolved feedbacks --- app/modules/scan/service.server.ts | 55 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/app/modules/scan/service.server.ts b/app/modules/scan/service.server.ts index 7d67bb90e..4561aeda9 100644 --- a/app/modules/scan/service.server.ts +++ b/app/modules/scan/service.server.ts @@ -5,6 +5,7 @@ import type { ErrorLabel } from "~/utils/error"; import { createNote } from "../note/service.server"; import { getOrganizationById } from "../organization/service.server"; import { getQr } from "../qr/service.server"; +import { getUserByID } from "../user/service.server"; const label: ErrorLabel = "Scan"; @@ -66,7 +67,13 @@ export async function createScan(params: { data, }); - await createScanNote({ userId, qrId, longitude, latitude, manuallyGenerated }); + await createScanNote({ + userId, + qrId, + longitude, + latitude, + manuallyGenerated, + }); return scan; } catch (cause) { @@ -146,7 +153,13 @@ export async function getScanByQrId({ qrId }: { qrId: string }) { } } -export async function createScanNote({ userId, qrId, latitude, longitude, manuallyGenerated }: { +export async function createScanNote({ + userId, + qrId, + latitude, + longitude, + manuallyGenerated, +}: { userId?: string | null; qrId: string; latitude?: Scan["latitude"]; @@ -155,46 +168,42 @@ export async function createScanNote({ userId, qrId, latitude, longitude, manual }) { try { let message = ""; - const { assetId, organizationId } = await getQr(qrId) + const { assetId, organizationId } = await getQr(qrId); if (assetId) { if (userId && userId != "anonymous") { - const user = await db.user.findFirst({ - where: { - id: userId, - }, - select: { - firstName: true, - lastName: true, - }, - }); - - const userName = user?.firstName + " " + user?.lastName + const { firstName, lastName } = await getUserByID(userId) + const userName = (firstName ? firstName.trim() : "") + " " + (lastName ? lastName.trim() : "") if (manuallyGenerated) { - message = `**${userName}** manually updated the GPS coordinates to **${latitude} ${longitude}**` + message = `*${userName}* manually updated the GPS coordinates to *${latitude}, ${longitude}*`; } else { - message = `**${userName}** performed a scan of the asset QR code` + message = `*${userName}* performed a scan of the asset QR code`; } return await createNote({ content: message, type: "UPDATE", userId, - assetId + assetId, }); } else { if (organizationId) { - const { userId: ownerId } = await getOrganizationById(organizationId) - message = "An unknown user has performed a scan of the asset QR code" + // If there is an assetId there will always be organization id. This is an extra check for organizationId. + + const { userId: ownerId } = await getOrganizationById(organizationId); + message = "An unknown user has performed a scan of the asset QR code"; + + /* to create a note we are using user id to track which user created the note + but in this case where scanner is anonymous, we are using the user id of the owner + of the organization to which the scanner QR belongs */ return await createNote({ content: message, type: "UPDATE", userId: ownerId, - assetId + assetId, }); } } } - } - catch (cause) { + } catch (cause) { throw new ShelfError({ cause, message: "Something went wrong while creating a scan note", @@ -202,4 +211,4 @@ export async function createScanNote({ userId, qrId, latitude, longitude, manual label, }); } -} \ No newline at end of file +} From ed7f2f533e2a138081398d1eaeab8ca35bfe9237 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 14:29:19 +0300 Subject: [PATCH 15/27] fixing bug with preview of code --- app/modules/qr/utils.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/qr/utils.server.ts b/app/modules/qr/utils.server.ts index 9357b7ca0..3a7d38c5d 100644 --- a/app/modules/qr/utils.server.ts +++ b/app/modules/qr/utils.server.ts @@ -10,7 +10,7 @@ import { createQr, getQrByAssetId, getQrByKitId } from "./service.server"; const label: ErrorLabel = "QR"; export function getQrBaseUrl() { - return URL_SHORTENER ? `${URL_SHORTENER}/qr` : `${SERVER_URL}/qr`; + return URL_SHORTENER ? `https://${URL_SHORTENER}` : `${SERVER_URL}/qr`; } export async function generateCode({ From ff84cc08e2aec7d4065dc725076b4013e2d11ef8 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 16:02:12 +0300 Subject: [PATCH 16/27] updating regex of scanner to also handle shortener links --- app/components/zxing-scanner.tsx | 27 +++++++++++++++++++++++---- app/utils/env.ts | 2 ++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/components/zxing-scanner.tsx b/app/components/zxing-scanner.tsx index 0c40bbf8e..b5647c753 100644 --- a/app/components/zxing-scanner.tsx +++ b/app/components/zxing-scanner.tsx @@ -7,9 +7,11 @@ import { import { useZxing } from "react-zxing"; import { useClientNotification } from "~/hooks/use-client-notification"; import type { loader } from "~/routes/_layout+/scanner"; +import { SERVER_URL, URL_SHORTENER } from "~/utils/env"; import { ShelfError } from "~/utils/error"; import { isFormProcessing } from "~/utils/form"; import { Spinner } from "./shared/spinner"; +import { isQrId } from "~/utils/id"; export const ZXingScanner = ({ videoMediaDevices, @@ -27,11 +29,18 @@ export const ZXingScanner = ({ // Function to decode the QR code const decodeQRCodes = (result: string) => { if (result != null && !isRedirecting) { - const regex = /^(https?:\/\/)([^/:]+)(:\d+)?\/qr\/([a-zA-Z0-9]+)$/; - /** We make sure the value of the QR code matches the structure of Shelf qr codes */ + /** + * - ^(https?:\/\/[^\/]+\/ matches the protocol, domain, and the initial slash. + * - (?:qr\/)? optionally matches the /qr/ part. + * - ([a-zA-Z0-9]+))$ matches the QR ID which is the last segment of the URL. + */ + // Regex to match both old and new QR code structures + const regex = /^(https?:\/\/[^\/]+\/(?:qr\/)?([a-zA-Z0-9]+))$/; + + /** We make sure the value of the QR code matches the structure of Shelf QR codes */ const match = result.match(regex); if (!match) { - /** If the QR code does not match the structure of Shelf qr codes, we show an error message */ + /** If the QR code does not match the structure of Shelf QR codes, we show an error message */ sendNotification({ title: "QR Code Not Valid", message: "Please Scan valid asset QR", @@ -40,12 +49,22 @@ export const ZXingScanner = ({ return; } + const qrId = match[2]; // Get the QR id from the URL + if (!isQrId(qrId)) { + sendNotification({ + title: "QR ID Not Valid", + message: "Please Scan valid asset QR", + icon: { name: "trash", variant: "error" }, + }); + return; + } + sendNotification({ title: "Shelf's QR Code detected", message: "Redirecting to mapped asset", icon: { name: "success", variant: "success" }, }); - const qrId = match[4]; // Get the last segment of the URL as the QR id + navigate(`/qr/${qrId}`); } }; diff --git a/app/utils/env.ts b/app/utils/env.ts index 76beda650..94c9e84ee 100644 --- a/app/utils/env.ts +++ b/app/utils/env.ts @@ -14,6 +14,7 @@ declare global { ENABLE_PREMIUM_FEATURES: string; MAINTENANCE_MODE: string; CHROME_EXECUTABLE_PATH: string; + URL_SHORTENER: string; }; } } @@ -223,5 +224,6 @@ export function getBrowserEnv() { ENABLE_PREMIUM_FEATURES, MAINTENANCE_MODE, CHROME_EXECUTABLE_PATH, + URL_SHORTENER, }; } From 2d4d800bd517db0c6d159a8b4d070c748734ea00 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 16:02:41 +0300 Subject: [PATCH 17/27] cleanup --- app/components/zxing-scanner.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/zxing-scanner.tsx b/app/components/zxing-scanner.tsx index b5647c753..57b9f938b 100644 --- a/app/components/zxing-scanner.tsx +++ b/app/components/zxing-scanner.tsx @@ -7,11 +7,10 @@ import { import { useZxing } from "react-zxing"; import { useClientNotification } from "~/hooks/use-client-notification"; import type { loader } from "~/routes/_layout+/scanner"; -import { SERVER_URL, URL_SHORTENER } from "~/utils/env"; import { ShelfError } from "~/utils/error"; import { isFormProcessing } from "~/utils/form"; -import { Spinner } from "./shared/spinner"; import { isQrId } from "~/utils/id"; +import { Spinner } from "./shared/spinner"; export const ZXingScanner = ({ videoMediaDevices, From 2fadb4fbcc6b1a893d325250e1bdf00c8a6fafb1 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Thu, 1 Aug 2024 16:04:34 +0300 Subject: [PATCH 18/27] more polish --- app/components/zxing-scanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/zxing-scanner.tsx b/app/components/zxing-scanner.tsx index 57b9f938b..b20c65c91 100644 --- a/app/components/zxing-scanner.tsx +++ b/app/components/zxing-scanner.tsx @@ -34,7 +34,7 @@ export const ZXingScanner = ({ * - ([a-zA-Z0-9]+))$ matches the QR ID which is the last segment of the URL. */ // Regex to match both old and new QR code structures - const regex = /^(https?:\/\/[^\/]+\/(?:qr\/)?([a-zA-Z0-9]+))$/; + const regex = /^(https?:\/\/[^/]+\/(?:qr\/)?([a-zA-Z0-9]+))$/; /** We make sure the value of the QR code matches the structure of Shelf QR codes */ const match = result.match(regex); From 62c952d3dfc956e0a4b9093ac6e2e8ff2c68a443 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Fri, 2 Aug 2024 12:42:44 +0300 Subject: [PATCH 19/27] very small style adjustments --- app/modules/scan/service.server.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/modules/scan/service.server.ts b/app/modules/scan/service.server.ts index 4561aeda9..0351ba7c9 100644 --- a/app/modules/scan/service.server.ts +++ b/app/modules/scan/service.server.ts @@ -171,12 +171,15 @@ export async function createScanNote({ const { assetId, organizationId } = await getQr(qrId); if (assetId) { if (userId && userId != "anonymous") { - const { firstName, lastName } = await getUserByID(userId) - const userName = (firstName ? firstName.trim() : "") + " " + (lastName ? lastName.trim() : "") + const { firstName, lastName } = await getUserByID(userId); + const userName = + (firstName ? firstName.trim() : "") + + " " + + (lastName ? lastName.trim() : ""); if (manuallyGenerated) { - message = `*${userName}* manually updated the GPS coordinates to *${latitude}, ${longitude}*`; + message = `**${userName}** manually updated the GPS coordinates to *${latitude}, ${longitude}*.`; } else { - message = `*${userName}* performed a scan of the asset QR code`; + message = `**${userName}** performed a scan of the asset QR code.`; } return await createNote({ content: message, From c733c5dc1a807573c21e870faa9bbc17728c1aeb Mon Sep 17 00:00:00 2001 From: Donkoko Date: Fri, 2 Aug 2024 13:54:04 +0300 Subject: [PATCH 20/27] allowing user to request account deletion --- .env.example | 5 +- app/components/layout/header/types.ts | 7 +- app/components/shared/button.tsx | 4 + app/components/user/delete-user.tsx | 134 +++++++++++------- app/hooks/use-disabled.ts | 13 ++ .../_layout+/account-details.general.tsx | 39 ++++- .../_layout+/admin-dashboard+/$userId.tsx | 4 - docs/hooks.md | 44 ++++++ 8 files changed, 187 insertions(+), 63 deletions(-) create mode 100644 app/hooks/use-disabled.ts diff --git a/.env.example b/.env.example index 7f80df44d..94cdc84a2 100644 --- a/.env.example +++ b/.env.example @@ -60,4 +60,7 @@ GEOCODE_API_KEY="geocode-api-key" SENTRY_ORG="sentry-org" SENTRY_PROJECT="sentry-project" SENTRY_DSN="sentry-dsn" -# CHROME_EXECUTABLE_PATH="/usr/bin/chromium" \ No newline at end of file +# CHROME_EXECUTABLE_PATH="/usr/bin/chromium" + +# Used for sending emails to admins for stuff like Request user delete. Optional. Defaults to support@shelf.nu +ADMIN_EMAIL="support@shelf.nu" \ No newline at end of file diff --git a/app/components/layout/header/types.ts b/app/components/layout/header/types.ts index 56d436bb5..58de9e234 100644 --- a/app/components/layout/header/types.ts +++ b/app/components/layout/header/types.ts @@ -45,7 +45,12 @@ export type Action = { }; /** The button variant. Default is primary */ -export type ButtonVariant = "primary" | "secondary" | "tertiary" | "link"; +export type ButtonVariant = + | "primary" + | "secondary" + | "tertiary" + | "link" + | "danger"; /** Width of the button. Default is auto */ export type ButtonWidth = "auto" | "full"; diff --git a/app/components/shared/button.tsx b/app/components/shared/button.tsx index e685239ce..f5981cddc 100644 --- a/app/components/shared/button.tsx +++ b/app/components/shared/button.tsx @@ -64,6 +64,10 @@ export const Button = React.forwardRef( link: tw( `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800` ), + danger: tw( + `border-error-600 bg-error-600 text-white focus:ring-2`, + disabled ? "border-error-300 bg-error-300" : "hover:bg-error-800" + ), }; const sizes = { diff --git a/app/components/user/delete-user.tsx b/app/components/user/delete-user.tsx index 68e9f3077..a632a04c4 100644 --- a/app/components/user/delete-user.tsx +++ b/app/components/user/delete-user.tsx @@ -1,4 +1,5 @@ -import type { User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useActionData } from "@remix-run/react"; import { Button } from "~/components/shared/button"; import { @@ -11,62 +12,85 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "~/components/shared/modal"; +import { useDisabled } from "~/hooks/use-disabled"; +import { useUserData } from "~/hooks/use-user-data"; +import type { action } from "~/routes/_layout+/account-details.general"; import { Form } from "../custom-form"; +import Input from "../forms/input"; import { TrashIcon } from "../icons/library"; -export const DeleteUser = ({ - user, -}: { - user: { - id: User["id"]; - firstName: User["firstName"]; - lastName: User["lastName"]; - }; -}) => ( - - - - +export const DeleteUser = () => { + const disabled = useDisabled(); + const user = useUserData(); + const actionData = useActionData(); + const [open, setOpen] = useState(false); - - -
- - - -
- - Delete {user.firstName} {user.lastName} - - - Are you sure you want to delete this user? This action cannot be - undone. - -
- -
- - - + useEffect(() => { + if (actionData && !actionData?.error && actionData.success) { + setOpen(false); + } + }, [actionData]); -
- -
-
-
-
-
-); + return ( + + + + + + +
+ +
+ + + +
+ + Are you sure you want to delete your account? + + + In order to delete your account you need to send a request that + will be fulfilled within the next 72 hours. Account deletion is + final and cannot be undone. + + + +
+ +
+ + + + + + + +
+
+
+
+
+ ); +}; diff --git a/app/hooks/use-disabled.ts b/app/hooks/use-disabled.ts new file mode 100644 index 000000000..49668b845 --- /dev/null +++ b/app/hooks/use-disabled.ts @@ -0,0 +1,13 @@ +import { useNavigation, type Fetcher } from "@remix-run/react"; +import { isFormProcessing } from "~/utils/form"; + +/** + * Used to know if a button should be disabled on navigation. + * By default it works with navigation state + * Optionally it can receive a fetcher to use as state + */ +export function useDisabled(fetcher?: Fetcher) { + const navigation = useNavigation(); + const state = fetcher ? fetcher.state : navigation.state; + return isFormProcessing(state); +} diff --git a/app/routes/_layout+/account-details.general.tsx b/app/routes/_layout+/account-details.general.tsx index 6c1822e9c..29fe4dad6 100644 --- a/app/routes/_layout+/account-details.general.tsx +++ b/app/routes/_layout+/account-details.general.tsx @@ -1,3 +1,4 @@ +import type { User } from "@prisma/client"; import type { ActionFunctionArgs, MetaFunction } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; @@ -10,6 +11,7 @@ import { Form } from "~/components/custom-form"; import FormRow from "~/components/forms/form-row"; import Input from "~/components/forms/input"; import { Button } from "~/components/shared/button"; +import { DeleteUser } from "~/components/user/delete-user"; import PasswordResetForm from "~/components/user/password-reset-form"; import ProfilePicture from "~/components/user/profile-picture"; @@ -24,10 +26,12 @@ import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { checkExhaustiveSwitch } from "~/utils/check-exhaustive-switch"; import { delay } from "~/utils/delay"; import { sendNotification } from "~/utils/emitter/send-notification.server"; +import { ADMIN_EMAIL } from "~/utils/env"; import { makeShelfError } from "~/utils/error"; import { isFormProcessing } from "~/utils/form"; import { getValidationErrors } from "~/utils/http"; import { data, error, parseData } from "~/utils/http.server"; +import { sendEmail } from "~/utils/mail.server"; import { zodFieldIsRequired } from "~/utils/zod"; export const UpdateFormSchema = z.object({ @@ -44,8 +48,9 @@ export const UpdateFormSchema = z.object({ const Actions = z.discriminatedUnion("intent", [ z.object({ - intent: z.literal("resetPassword"), + intent: z.enum(["resetPassword", "deleteUser"]), email: z.string(), + reason: z.string(), }), UpdateFormSchema.extend({ intent: z.literal("updateUser"), @@ -102,6 +107,28 @@ export async function action({ context, request }: ActionFunctionArgs) { return json(data({ success: true })); } + case "deleteUser": { + let reason = "No reason provided"; + if ("reason" in payload && payload.reason) { + reason = payload?.reason; + } + + await sendEmail({ + to: ADMIN_EMAIL || "support@shelf.nu", + subject: "Delete account request", + text: `User with id ${userId} and email ${payload.email} has requested to delete their account. \n\n Reason: ${reason}`, + }); + + sendNotification({ + title: "Account deletion request", + message: + "Your request has been sent to the admin and will be processed within 24 hours.", + icon: { name: "success", variant: "success" }, + senderId: authSession.userId, + }); + + return json(data({ success: true })); + } default: { checkExhaustiveSwitch(intent); return json(data(null)); @@ -132,7 +159,7 @@ export default function UserPage() { const transition = useNavigation(); const disabled = isFormProcessing(transition.state); const data = useActionData(); - const user = useUserData(); + const user = useUserData() as unknown as User; const usernameError = getValidationErrors(data?.error)?.username ?.message || zo.errors.username()?.message; @@ -268,6 +295,14 @@ export default function UserPage() {

Update your password here

+ +
+

Delete account

+

+ Send a request to delete your account. +

+ +
); } diff --git a/app/routes/_layout+/admin-dashboard+/$userId.tsx b/app/routes/_layout+/admin-dashboard+/$userId.tsx index 6dde5c03b..f930d045f 100644 --- a/app/routes/_layout+/admin-dashboard+/$userId.tsx +++ b/app/routes/_layout+/admin-dashboard+/$userId.tsx @@ -18,7 +18,6 @@ import { Button } from "~/components/shared/button"; import { DateS } from "~/components/shared/date"; import { Spinner } from "~/components/shared/spinner"; import { Table, Td, Tr } from "~/components/table"; -import { DeleteUser } from "~/components/user/delete-user"; import { db } from "~/database/db.server"; import { updateUserTierId } from "~/modules/tier/service.server"; import { deleteUser, getUserByID } from "~/modules/user/service.server"; @@ -207,9 +206,6 @@ export default function Area51UserPage() {

User: {user?.email}

-
- -
diff --git a/docs/hooks.md b/docs/hooks.md index 9d0c22dcb..e92bbec33 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -134,3 +134,47 @@ const QRScannerComponent = () => { ); }; ``` + +### `useDisabled` + +The `useDisabled` hook is used to determine if a button should be disabled during navigation. By default, it operates with the navigation state, but it can optionally accept a fetcher to use as the state. + +**Usage:** + +```typescript +/** Without fetcher, using default navigation */ +const isDisabled = useDisabled(); + +/** Without fetcher */ +const isDisabled = useDisabled(fetcher); +``` + +**Parameters:** + +- `fetcher` (optional): An object that contains the state to be used. If not provided, the navigation state will be used. + +**Returns:** + +- `boolean`: Returns `true` if the form is processing and the button should be disabled, otherwise `false`. + +**Example:** + +```typescript +import { useDisabled } from './path/to/hooks'; + +const MyComponent = () => { + const fetcher = useFetcher(); + const isDisabled = useDisabled(fetcher); + + return ( + + ); +}; +``` + +**Dependencies:** + +- `useNavigation`: A hook that provides the current navigation state. +- `isFormProcessing`: A function that checks if the form is currently processing based on the state. From 90556f02d9417c07ae9f47bb4f4c0338d25f0c16 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Fri, 2 Aug 2024 13:59:42 +0300 Subject: [PATCH 21/27] async email sending + more improvements to content --- app/routes/_layout+/account-details.general.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/routes/_layout+/account-details.general.tsx b/app/routes/_layout+/account-details.general.tsx index 29fe4dad6..f2a2a75bd 100644 --- a/app/routes/_layout+/account-details.general.tsx +++ b/app/routes/_layout+/account-details.general.tsx @@ -113,16 +113,22 @@ export async function action({ context, request }: ActionFunctionArgs) { reason = payload?.reason; } - await sendEmail({ + void sendEmail({ to: ADMIN_EMAIL || "support@shelf.nu", subject: "Delete account request", - text: `User with id ${userId} and email ${payload.email} has requested to delete their account. \n\n Reason: ${reason}`, + text: `User with id ${userId} and email ${payload.email} has requested to delete their account. \n\n Reason: ${reason}\n\n`, + }); + + void sendEmail({ + to: payload.email, + subject: "Delete account request received", + text: `We have received your request to delete your account. It will be processed within 72 hours.\n\n Kind regards,\nthe Shelf team \n\n`, }); sendNotification({ title: "Account deletion request", message: - "Your request has been sent to the admin and will be processed within 24 hours.", + "Your request has been sent to the admin and will be processed within 24 hours. You will receive an email confirmation.", icon: { name: "success", variant: "success" }, senderId: authSession.userId, }); From 194dc07f40a66978eaf1f6c69df9ddd0769c1aa0 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Mon, 5 Aug 2024 11:22:47 +0300 Subject: [PATCH 22/27] make an extra check to make sure all ids are generated with a number. Write a test for it --- app/utils/id/id.server.test.ts | 13 ++++++++++ app/utils/id/id.server.ts | 46 ++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 app/utils/id/id.server.test.ts diff --git a/app/utils/id/id.server.test.ts b/app/utils/id/id.server.test.ts new file mode 100644 index 000000000..04060e758 --- /dev/null +++ b/app/utils/id/id.server.test.ts @@ -0,0 +1,13 @@ +import { id } from "./id.server"; + +// Utility function to check if a string contains at least one digit +const hasNumber = (str: string) => /\d/.test(str); + +describe("id function", () => { + it("should generate 5000 IDs and ensure each has at least one number", () => { + const ids = Array.from({ length: 5_000 }, () => id(10)); + ids.forEach((generatedId) => { + expect(hasNumber(generatedId)).toBe(true); + }); + }); +}); diff --git a/app/utils/id/id.server.ts b/app/utils/id/id.server.ts index 3f74f777d..70d2e52ad 100644 --- a/app/utils/id/id.server.ts +++ b/app/utils/id/id.server.ts @@ -1,4 +1,5 @@ import { init } from "@paralleldrive/cuid2"; +import { isQrId } from "."; import { DEFAULT_CUID_LENGTH } from "../constants"; import { FINGERPRINT } from "../env"; import { ShelfError } from "../error"; @@ -21,18 +22,47 @@ export function id(length?: number) { }) ); } - return init({ + let generatedId = init({ length: length || DEFAULT_CUID_LENGTH, /** FINGERPRINT is not required but it helps with avoiding collision */ ...(FINGERPRINT && { fingerprint: FINGERPRINT }), })(); + + /** + * Because of the custom length we are passing, + * there are situations where the generated id does not have a number, + * which is a requirement for a QR id. + * In that case we generate a random number between 0 and 9 and replace the charactear at its own index. + * We have to make sure we never replace the first character because it must be a letter. + * */ + const hasNumber = (str: string) => /\d/.test(str); + + if (!hasNumber(generatedId)) { + const randomNumber = Math.floor(Math.random() * 10); + /** + * 1. Math.random() generates a random floating-point number between 0 (inclusive) and 1 (exclusive). + * 2. Multiplying by 9 scales this to a range of 0 (inclusive) to 9 (exclusive). + * 3. Math.floor() rounds down to the nearest whole number, resulting in an integer between 0 and 8 (inclusive). + * 4. Adding 1 shifts the range to between 1 and 9 (inclusive). + */ + const randomIndex = Math.floor(Math.random() * 9) + 1; + + // Convert generatedId to an array of characters + let generatedIdArray = generatedId.split(""); + // Replace the character at randomIndex with randomNumber + generatedIdArray[randomIndex] = randomNumber.toString(); + + // Join the array back into a string + generatedId = generatedIdArray.join(""); + } + return generatedId; } catch (cause) { - Logger.error( - new ShelfError({ - cause: null, - message: "Id generation failed", - label: "DB", - }) - ); + const e = new ShelfError({ + cause, + message: "Id generation failed", + label: "DB", + }); + Logger.error(e); + throw e; } } From 88498bbc686d0ebe4147f68916db8e3e3413da7f Mon Sep 17 00:00:00 2001 From: Donkoko Date: Mon, 5 Aug 2024 12:23:57 +0300 Subject: [PATCH 23/27] removing number validation for isQrId to prevent issue caused by corrupted qrIds --- app/utils/id/id.server.test.ts | 4 +--- app/utils/id/id.server.ts | 3 +-- app/utils/id/index.ts | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/utils/id/id.server.test.ts b/app/utils/id/id.server.test.ts index 04060e758..b3111fa29 100644 --- a/app/utils/id/id.server.test.ts +++ b/app/utils/id/id.server.test.ts @@ -1,8 +1,6 @@ +import { hasNumber } from "."; import { id } from "./id.server"; -// Utility function to check if a string contains at least one digit -const hasNumber = (str: string) => /\d/.test(str); - describe("id function", () => { it("should generate 5000 IDs and ensure each has at least one number", () => { const ids = Array.from({ length: 5_000 }, () => id(10)); diff --git a/app/utils/id/id.server.ts b/app/utils/id/id.server.ts index 70d2e52ad..916114e18 100644 --- a/app/utils/id/id.server.ts +++ b/app/utils/id/id.server.ts @@ -1,5 +1,5 @@ import { init } from "@paralleldrive/cuid2"; -import { isQrId } from "."; +import { hasNumber } from "."; import { DEFAULT_CUID_LENGTH } from "../constants"; import { FINGERPRINT } from "../env"; import { ShelfError } from "../error"; @@ -35,7 +35,6 @@ export function id(length?: number) { * In that case we generate a random number between 0 and 9 and replace the charactear at its own index. * We have to make sure we never replace the first character because it must be a letter. * */ - const hasNumber = (str: string) => /\d/.test(str); if (!hasNumber(generatedId)) { const randomNumber = Math.floor(Math.random() * 10); diff --git a/app/utils/id/index.ts b/app/utils/id/index.ts index f1bcd3222..6f6b4cc36 100644 --- a/app/utils/id/index.ts +++ b/app/utils/id/index.ts @@ -10,12 +10,20 @@ export function isQrId(id: string): boolean { const possibleLengths = [DEFAULT_CUID_LENGTH, LEGACY_CUID_LENGTH]; const length = id.length; + // @TODO - temporary disabled number check due to bug and corrupted ids + // /** + // * 1. The string must contain only lowercase letters and digits. + // * 2. The string must start with a lowercase letter. + // * 3. The string must contain at least one digit. + // */ + // const regex = /^(?=.*\d)[a-z][0-9a-z]*$/; + /** - * 1. The string must contain only lowercase letters and digits. + * Adjusted criteria: + * 1. The string must contain only lowercase letters. * 2. The string must start with a lowercase letter. - * 3. The string must contain at least one digit. */ - const regex = /^(?=.*\d)[a-z][0-9a-z]*$/; + const regex = /^[a-z][0-9a-z]*$/; // Validate the ID against the criteria if ( @@ -28,3 +36,7 @@ export function isQrId(id: string): boolean { return true; } + +export function hasNumber(str: string) { + return /\d/.test(str); +} From e57f56339ed2ab13c8f433754e1956d988843801 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 6 Aug 2024 13:04:53 +0300 Subject: [PATCH 24/27] move mail file --- app/{utils => emails}/mail.server.ts | 4 ++-- app/modules/booking/email-helpers.ts | 2 +- app/modules/booking/service.server.ts | 2 +- app/modules/booking/worker.server.ts | 2 +- app/modules/invite/service.server.ts | 2 +- app/modules/report-found/service.server.ts | 2 +- app/routes/_layout+/account-details.general.tsx | 2 +- app/routes/_layout+/settings.team.users.tsx | 2 +- app/routes/_welcome+/onboarding.tsx | 2 +- app/routes/api+/stripe-webhook.ts | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) rename app/{utils => emails}/mail.server.ts (95%) diff --git a/app/utils/mail.server.ts b/app/emails/mail.server.ts similarity index 95% rename from app/utils/mail.server.ts rename to app/emails/mail.server.ts index 3b663c734..62ff18231 100644 --- a/app/utils/mail.server.ts +++ b/app/emails/mail.server.ts @@ -1,8 +1,8 @@ import type { Attachment } from "nodemailer/lib/mailer"; import { config } from "~/config/shelf.config"; import { transporter } from "~/emails/transporter.server"; -import { SMTP_FROM } from "./env"; -import { ShelfError } from "./error"; +import { SMTP_FROM } from "../utils/env"; +import { ShelfError } from "../utils/error"; export const sendEmail = async ({ to, diff --git a/app/modules/booking/email-helpers.ts b/app/modules/booking/email-helpers.ts index d3532dcef..6a6368f5f 100644 --- a/app/modules/booking/email-helpers.ts +++ b/app/modules/booking/email-helpers.ts @@ -1,10 +1,10 @@ import { bookingUpdatesTemplateString } from "~/emails/bookings-updates-template"; +import { sendEmail } from "~/emails/mail.server"; import type { BookingForEmail } from "~/emails/types"; import { getDateTimeFormatFromHints } from "~/utils/client-hints"; import { getTimeRemainingMessage } from "~/utils/date-fns"; import { SERVER_URL } from "~/utils/env"; import { ShelfError } from "~/utils/error"; -import { sendEmail } from "~/utils/mail.server"; import type { ClientHint } from "./types"; /** diff --git a/app/modules/booking/service.server.ts b/app/modules/booking/service.server.ts index 3f96ab102..1a06c9e12 100644 --- a/app/modules/booking/service.server.ts +++ b/app/modules/booking/service.server.ts @@ -9,6 +9,7 @@ import type { } from "@prisma/client"; import { db } from "~/database/db.server"; import { bookingUpdatesTemplateString } from "~/emails/bookings-updates-template"; +import { sendEmail } from "~/emails/mail.server"; import { getStatusClasses, isOneDayEvent } from "~/utils/calendar"; import { calcTimeDifference } from "~/utils/date-fns"; import { sendNotification } from "~/utils/emitter/send-notification.server"; @@ -17,7 +18,6 @@ import { ShelfError } from "~/utils/error"; import { getCurrentSearchParams } from "~/utils/http.server"; import { ALL_SELECTED_KEY } from "~/utils/list"; import { Logger } from "~/utils/logger"; -import { sendEmail } from "~/utils/mail.server"; import { scheduler } from "~/utils/scheduler.server"; import { bookingSchedulerEventsEnum, schedulerKeys } from "./constants"; import { diff --git a/app/modules/booking/worker.server.ts b/app/modules/booking/worker.server.ts index dae580531..62e720d62 100644 --- a/app/modules/booking/worker.server.ts +++ b/app/modules/booking/worker.server.ts @@ -3,10 +3,10 @@ import { BookingStatus } from "@prisma/client"; import type PgBoss from "pg-boss"; import { db } from "~/database/db.server"; import { bookingUpdatesTemplateString } from "~/emails/bookings-updates-template"; +import { sendEmail } from "~/emails/mail.server"; import { getTimeRemainingMessage } from "~/utils/date-fns"; import { ShelfError } from "~/utils/error"; import { Logger } from "~/utils/logger"; -import { sendEmail } from "~/utils/mail.server"; import { scheduler } from "~/utils/scheduler.server"; import { bookingSchedulerEventsEnum, schedulerKeys } from "./constants"; import { diff --git a/app/modules/invite/service.server.ts b/app/modules/invite/service.server.ts index 087d5d5d2..a0aa3a65a 100644 --- a/app/modules/invite/service.server.ts +++ b/app/modules/invite/service.server.ts @@ -5,12 +5,12 @@ import type { Params } from "@remix-run/react"; import jwt from "jsonwebtoken"; import { db } from "~/database/db.server"; import { invitationTemplateString } from "~/emails/invite-template"; +import { sendEmail } from "~/emails/mail.server"; import { INVITE_EXPIRY_TTL_DAYS } from "~/utils/constants"; import { sendNotification } from "~/utils/emitter/send-notification.server"; import { INVITE_TOKEN_SECRET } from "~/utils/env"; import type { ErrorLabel } from "~/utils/error"; import { ShelfError, isLikeShelfError } from "~/utils/error"; -import { sendEmail } from "~/utils/mail.server"; import { generateRandomCode, inviteEmailText } from "./helpers"; import { createTeamMember } from "../team-member/service.server"; import { createUserOrAttachOrg } from "../user/service.server"; diff --git a/app/modules/report-found/service.server.ts b/app/modules/report-found/service.server.ts index 5e3480147..7c88eb03a 100644 --- a/app/modules/report-found/service.server.ts +++ b/app/modules/report-found/service.server.ts @@ -1,7 +1,7 @@ import type { Asset, Kit, Prisma, ReportFound, User } from "@prisma/client"; import { db } from "~/database/db.server"; +import { sendEmail } from "~/emails/mail.server"; import { ShelfError } from "~/utils/error"; -import { sendEmail } from "~/utils/mail.server"; import { normalizeQrData } from "~/utils/qr"; export async function createReport({ diff --git a/app/routes/_layout+/account-details.general.tsx b/app/routes/_layout+/account-details.general.tsx index f2a2a75bd..33f13bb03 100644 --- a/app/routes/_layout+/account-details.general.tsx +++ b/app/routes/_layout+/account-details.general.tsx @@ -15,6 +15,7 @@ import { DeleteUser } from "~/components/user/delete-user"; import PasswordResetForm from "~/components/user/password-reset-form"; import ProfilePicture from "~/components/user/profile-picture"; +import { sendEmail } from "~/emails/mail.server"; import { useUserData } from "~/hooks/use-user-data"; import { sendResetPasswordLink } from "~/modules/auth/service.server"; import { @@ -31,7 +32,6 @@ import { makeShelfError } from "~/utils/error"; import { isFormProcessing } from "~/utils/form"; import { getValidationErrors } from "~/utils/http"; import { data, error, parseData } from "~/utils/http.server"; -import { sendEmail } from "~/utils/mail.server"; import { zodFieldIsRequired } from "~/utils/zod"; export const UpdateFormSchema = z.object({ diff --git a/app/routes/_layout+/settings.team.users.tsx b/app/routes/_layout+/settings.team.users.tsx index fbaa4cd0b..4196a81d3 100644 --- a/app/routes/_layout+/settings.team.users.tsx +++ b/app/routes/_layout+/settings.team.users.tsx @@ -18,6 +18,7 @@ import { Button } from "~/components/shared/button"; import { Td, Th } from "~/components/table"; import { TeamUsersActionsDropdown } from "~/components/workspace/users-actions-dropdown"; import { db } from "~/database/db.server"; +import { sendEmail } from "~/emails/mail.server"; import { revokeAccessEmailText } from "~/modules/invite/helpers"; import { createInvite } from "~/modules/invite/service.server"; import type { TeamMembersWithUserOrInvite } from "~/modules/settings/service.server"; @@ -27,7 +28,6 @@ import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { sendNotification } from "~/utils/emitter/send-notification.server"; import { makeShelfError, ShelfError } from "~/utils/error"; import { data, error, parseData } from "~/utils/http.server"; -import { sendEmail } from "~/utils/mail.server"; import { PermissionAction, PermissionEntity, diff --git a/app/routes/_welcome+/onboarding.tsx b/app/routes/_welcome+/onboarding.tsx index 238d174d0..de6f97557 100644 --- a/app/routes/_welcome+/onboarding.tsx +++ b/app/routes/_welcome+/onboarding.tsx @@ -12,6 +12,7 @@ import Input from "~/components/forms/input"; import PasswordInput from "~/components/forms/password-input"; import { Button } from "~/components/shared/button"; import { config } from "~/config/shelf.config"; +import { sendEmail } from "~/emails/mail.server"; import { onboardingEmailText } from "~/emails/onboarding-email"; import { useSearchParams } from "~/hooks/search-params"; import { @@ -27,7 +28,6 @@ import { makeShelfError } from "~/utils/error"; import { isFormProcessing } from "~/utils/form"; import { getValidationErrors } from "~/utils/http"; import { assertIsPost, data, error, parseData } from "~/utils/http.server"; -import { sendEmail } from "~/utils/mail.server"; import { createStripeCustomer } from "~/utils/stripe.server"; function createOnboardingSchema(userSignedUpWithPassword: boolean) { diff --git a/app/routes/api+/stripe-webhook.ts b/app/routes/api+/stripe-webhook.ts index 493ab806e..3d09565ba 100644 --- a/app/routes/api+/stripe-webhook.ts +++ b/app/routes/api+/stripe-webhook.ts @@ -3,10 +3,10 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import type Stripe from "stripe"; import { db } from "~/database/db.server"; +import { sendEmail } from "~/emails/mail.server"; import { trialEndsSoonText } from "~/emails/stripe/trial-ends-soon"; import { ShelfError, makeShelfError } from "~/utils/error"; import { error } from "~/utils/http.server"; -import { sendEmail } from "~/utils/mail.server"; import { fetchStripeSubscription, getDataFromStripeEvent, From 0b027165b7f06c5fa40bdc035a107e5aed3209a5 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Wed, 7 Aug 2024 11:33:54 +0300 Subject: [PATCH 25/27] fixing bug with clearing filters on index --- app/hooks/search-params/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/hooks/search-params/index.ts b/app/hooks/search-params/index.ts index 9c5b993d4..1dcd70229 100644 --- a/app/hooks/search-params/index.ts +++ b/app/hooks/search-params/index.ts @@ -199,7 +199,7 @@ export function useClearValueFromParams(...keys: string[]): Function { const isAssetIndexPage = useIsAssetIndexPage(); function clearValuesFromParams() { - if (isAssetIndexPage && currentOrganization && currentOrganization?.id) { + if (isAssetIndexPage && currentOrganization) { destroyCookieValues(currentOrganization.id, keys, cookieSearchParams); deleteKeysInSearchParams(keys, setSearchParams); return; @@ -217,8 +217,7 @@ export function useClearValueFromParams(...keys: string[]): Function { export function useCookieDestroy() { const cookieSearchParams = useAssetIndexCookieSearchParams(); const currentOrganization = useCurrentOrganization(); - // const isAssetIndexPage = useIsAssetIndexPage(); - const isAssetIndexPage = false; + const isAssetIndexPage = useIsAssetIndexPage(); /** * Function to destroy specific keys from cookies if on the asset index page. From 0df480e4aac48552b7b4157247c449e9eadfaa7e Mon Sep 17 00:00:00 2001 From: ZuebeyirEser Date: Wed, 7 Aug 2024 12:22:19 +0200 Subject: [PATCH 26/27] started adding useUserRoleHelper --- docs/hooks.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/hooks.md b/docs/hooks.md index e92bbec33..14e850330 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -135,7 +135,7 @@ const QRScannerComponent = () => { }; ``` -### `useDisabled` +## `useDisabled` The `useDisabled` hook is used to determine if a button should be disabled during navigation. By default, it operates with the navigation state, but it can optionally accept a fetcher to use as the state. @@ -178,3 +178,11 @@ const MyComponent = () => { - `useNavigation`: A hook that provides the current navigation state. - `isFormProcessing`: A function that checks if the form is currently processing based on the state. + +## useUserRoleHelper + +The `useUserRoleHelper` hook is helps you to always know the roles of the current user and also returns some helper boolean values to make it easier to check for specific roles. + +**Dependencies:** + +- `useRouteLoaderData`: From 06b72ffc90292554364ea4fde970220c0aedd726 Mon Sep 17 00:00:00 2001 From: ZuebeyirEser Date: Wed, 7 Aug 2024 12:58:58 +0200 Subject: [PATCH 27/27] finished the useUserRoleHelper hook --- docs/hooks.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/hooks.md b/docs/hooks.md index 14e850330..6809644d2 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -183,6 +183,39 @@ const MyComponent = () => { The `useUserRoleHelper` hook is helps you to always know the roles of the current user and also returns some helper boolean values to make it easier to check for specific roles. +The useUserRoleHelper function returns an object(roles) and helper boolean attributes: + +- `roles`: enum that provides role of the current user +- `isAdministrator`: A boolean value indicating whether the user has the 'ADMIN' role. +- `isOwner`: A boolean value indicating whether the user has the OWNER role. +- `isAdministratorOrOwner`: A boolean value indicating whether the user has either the 'ADMIN' or 'OWNER'role. +- `isSelfService`: A boolean value indicating whether the user has the 'SELF_SERVICE' role. +- `isBase`: A boolean value indicating whether the user has the 'BASE' role. +- `isBaseOrSelfService`: A boolean value indicating whether the user has either the BASE or 'SELF_SERVICE' role. + +**Usage:** +The "New Asset" button is rendered only if isAdministratorOrOwner is true. +```typescript +import React from 'react'; +import { useUserRoleHelper } from '~/hooks/user-user-role-helper'; + +export default function AssetIndexPage() { + const { isAdministratorOrOwner } = useUserRoleHelper(); + + return ( +
+
+ {isAdministratorOrOwner && ( + + )} +
+
+ ); +} +``` + **Dependencies:** -- `useRouteLoaderData`: +- `useRouteLoaderData`: hook from `@remix-run/react` that returns the loader data for a given route by ID.