From 6c504af80deb017a99a5b76a091220e88668c68f Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 26 Nov 2024 12:20:29 +0200 Subject: [PATCH 1/4] fix: issues with export assets - make sure name column is visible - make sure qrId column works - improve exporting ux by adding a loading state --- .../assets-index/export-assets-button.tsx | 51 +++++-- app/utils/csv.server.ts | 133 ++++++++++-------- 2 files changed, 114 insertions(+), 70 deletions(-) diff --git a/app/components/assets/assets-index/export-assets-button.tsx b/app/components/assets/assets-index/export-assets-button.tsx index a91b839c5..817645401 100644 --- a/app/components/assets/assets-index/export-assets-button.tsx +++ b/app/components/assets/assets-index/export-assets-button.tsx @@ -1,11 +1,17 @@ +import { useState } from "react"; +import { useFetcher } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { Button } from "~/components/shared/button"; +import { Spinner } from "~/components/shared/spinner"; +import { useDisabled } from "~/hooks/use-disabled"; import { isSelectingAllItems } from "~/utils/list"; export function ExportAssetsButton() { + const fetcher = useFetcher(); + const disabled = useDisabled(fetcher); const selectedAssets = useAtomValue(selectedBulkItemsAtom); - const disabled = selectedAssets.length === 0; + const [isDownloading, setIsDownloading] = useState(false); const allSelected = isSelectingAllItems(selectedAssets); const title = `Export selection ${ @@ -14,27 +20,50 @@ export function ExportAssetsButton() { /** Get the assetIds from the atom and add them to assetIds search param */ const assetIds = selectedAssets.map((asset) => asset.id); - let url = `/assets/export/assets-${new Date() + const url = `/assets/export/assets-${new Date() .toISOString() .slice(0, 10)}.csv`; - if (assetIds.length > 0) { - url += `?assetIds=${assetIds.join(",")}`; - } + const searchParams = + assetIds.length > 0 ? `?assetIds=${assetIds.join(",")}` : ""; + + /** Handle the download via fetcher and track state */ + const handleExport = async () => { + setIsDownloading(true); + try { + const response = await fetch(`${url}${searchParams}`); + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = downloadUrl; + link.setAttribute("download", url.split("/").pop() || "export.csv"); + document.body.appendChild(link); + link.click(); + link.remove(); + } finally { + setIsDownloading(false); + } + }; + return ( ); } diff --git a/app/utils/csv.server.ts b/app/utils/csv.server.ts index b55c88c52..b44477ce7 100644 --- a/app/utils/csv.server.ts +++ b/app/utils/csv.server.ts @@ -20,8 +20,12 @@ import type { AdvancedIndexAsset, ShelfAssetCustomFieldValueType, } from "~/modules/asset/types"; -import type { Column } from "~/modules/asset-index-settings/helpers"; +import type { + Column, + FixedField, +} from "~/modules/asset-index-settings/helpers"; import { parseColumnName } from "~/modules/asset-index-settings/helpers"; +import { checkExhaustiveSwitch } from "./check-exhaustive-switch"; import { getAdvancedFiltersFromRequest } from "./cookies.server"; import { isLikeShelfError, ShelfError } from "./error"; import { ALL_SELECTED_KEY } from "./list"; @@ -265,7 +269,10 @@ export async function exportAssetsFromIndexToCsv({ // Pass both assets and columns to the build function const csvData = buildCsvExportDataFromAssets({ assets, - columns: settings.columns as Column[], + columns: [ + { name: "name", visible: true, position: 0 }, + ...(settings.columns as Column[]), + ], }); // Join rows with CRLF as per CSV spec @@ -303,63 +310,71 @@ export const buildCsvExportDataFromAssets = ({ // Handle different column types let value: any; - switch (column.name) { - case "id": - value = asset.id; - break; - case "name": - value = asset.title; - break; - case "description": - value = asset.description ?? ""; - break; - case "category": - value = asset.category?.name ?? "Uncategorized"; - break; - case "location": - value = asset.location?.name; - break; - case "kit": - value = asset.kit?.name; - break; - case "custody": - value = asset.custody - ? resolveTeamMemberName(asset.custody.custodian) - : ""; - break; - case "tags": - value = asset.tags?.map((t) => t.name).join(", ") ?? ""; - break; - case "status": - value = asset.status; - break; - case "createdAt": - value = asset.createdAt - ? new Date(asset.createdAt).toISOString() - : ""; - break; - case "valuation": - value = asset.valuation; - break; - case "availableToBook": - value = asset.availableToBook ? "Yes" : "No"; - break; - default: - // Handle custom fields - if (column.name.startsWith("cf_")) { - const fieldName = column.name.replace("cf_", ""); - const customField = asset.customFields?.find( - (cf) => cf.customField.name === fieldName - ); - - if (!customField) { - value = ""; - } else { - const fieldValue = - customField.value as unknown as ShelfAssetCustomFieldValueType["value"]; - value = formatCustomFieldForCsv(fieldValue, column.cfType); - } - } + // If it's not a custom field, it must be a fixed field or 'name' + if (!column.name.startsWith("cf_")) { + const fieldName = column.name as FixedField | "name"; + + switch (fieldName) { + case "id": + value = asset.id; + break; + case "qrId": + value = asset.qrId; + break; + case "name": + value = asset.title; + break; + case "description": + value = asset.description ?? ""; + break; + case "category": + value = asset.category?.name ?? "Uncategorized"; + break; + case "location": + value = asset.location?.name; + break; + case "kit": + value = asset.kit?.name; + break; + case "custody": + value = asset.custody + ? resolveTeamMemberName(asset.custody.custodian) + : ""; + break; + case "tags": + value = asset.tags?.map((t) => t.name).join(", ") ?? ""; + break; + case "status": + value = asset.status; + break; + case "createdAt": + value = asset.createdAt + ? new Date(asset.createdAt).toISOString() + : ""; + break; + case "valuation": + value = asset.valuation; + break; + case "availableToBook": + value = asset.availableToBook ? "Yes" : "No"; + break; + default: + checkExhaustiveSwitch(fieldName); + value = ""; + } + } else { + // Handle custom fields + const fieldName = column.name.replace("cf_", ""); + const customField = asset.customFields?.find( + (cf) => cf.customField.name === fieldName + ); + if (!customField) { + value = ""; + } else { + const fieldValue = + customField.value as unknown as ShelfAssetCustomFieldValueType["value"]; + value = formatCustomFieldForCsv(fieldValue, column.cfType); + } } return formatValueForCsv(value); From c1d72ff90b18bf12821beb7dfcfc3c5dd160d6bf Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 26 Nov 2024 12:26:25 +0200 Subject: [PATCH 2/4] small adjustment --- .../assets/assets-index/export-assets-button.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/components/assets/assets-index/export-assets-button.tsx b/app/components/assets/assets-index/export-assets-button.tsx index 817645401..fc36e1874 100644 --- a/app/components/assets/assets-index/export-assets-button.tsx +++ b/app/components/assets/assets-index/export-assets-button.tsx @@ -1,16 +1,14 @@ import { useState } from "react"; -import { useFetcher } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { Button } from "~/components/shared/button"; import { Spinner } from "~/components/shared/spinner"; -import { useDisabled } from "~/hooks/use-disabled"; import { isSelectingAllItems } from "~/utils/list"; export function ExportAssetsButton() { - const fetcher = useFetcher(); - const disabled = useDisabled(fetcher); const selectedAssets = useAtomValue(selectedBulkItemsAtom); + const disabled = selectedAssets.length === 0; + const [isDownloading, setIsDownloading] = useState(false); const allSelected = isSelectingAllItems(selectedAssets); @@ -51,9 +49,9 @@ export function ExportAssetsButton() { className="font-medium" title={title} disabled={ - selectedAssets.length === 0 + disabled ? { reason: "You must select at least 1 asset to export" } - : disabled || isDownloading + : isDownloading } >
From 2c45a13ac48ff59e787d8a97abf8f085415a8c72 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 26 Nov 2024 13:17:51 +0200 Subject: [PATCH 3/4] fixing some styles with dynamic select and dropdown --- .../advanced-filters/value-field.tsx | 28 ++++++++++++++++--- .../assets/custom-fields-inputs.tsx | 2 +- .../dynamic-select/dynamic-select.tsx | 7 ++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/components/assets/assets-index/advanced-filters/value-field.tsx b/app/components/assets/assets-index/advanced-filters/value-field.tsx index 2dc1f8b2c..9d7bcaa3c 100644 --- a/app/components/assets/assets-index/advanced-filters/value-field.tsx +++ b/app/components/assets/assets-index/advanced-filters/value-field.tsx @@ -563,7 +563,12 @@ function CustodyEnumField({ className="w-full justify-start font-normal [&_span]:w-full [&_span]:max-w-full [&_span]:truncate" >
- + {value === "without-custody" ? "Without custody" : selectedIds.length > 0 @@ -675,7 +680,12 @@ function CategoryEnumField({ className="w-full justify-start font-normal [&_span]:w-full [&_span]:max-w-full [&_span]:truncate" >
- + {selectedIds.length > 0 ? selectedIds .map((id) => { @@ -778,7 +788,12 @@ function LocationEnumField({ className="w-full justify-start font-normal [&_span]:w-full [&_span]:max-w-full [&_span]:truncate" >
- + {selectedIds.length > 0 ? selectedIds .map((id) => { @@ -881,7 +896,12 @@ function KitEnumField({ className="w-full justify-start font-normal [&_span]:w-full [&_span]:max-w-full [&_span]:truncate" >
- + {selectedIds.length > 0 && data.kits && data.kits.length > 0 ? selectedIds .map((id) => { diff --git a/app/components/assets/custom-fields-inputs.tsx b/app/components/assets/custom-fields-inputs.tsx index 3b5ad0175..49775ecbf 100644 --- a/app/components/assets/custom-fields-inputs.tsx +++ b/app/components/assets/custom-fields-inputs.tsx @@ -84,7 +84,7 @@ export default function AssetCustomFields({ DATE: (field) => (
- + {triggerValue} From 52a6b5426c2e34c9c2503dd54595d61550d68ae0 Mon Sep 17 00:00:00 2001 From: Donkoko Date: Tue, 26 Nov 2024 14:34:31 +0200 Subject: [PATCH 4/4] fix: correct export permissions - make sure free users cannot use premium feature --- .../assets/assets-index/export-assets-button.tsx | 16 ++++++++++++++-- app/components/marketing/upgrade-message.tsx | 6 +++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/components/assets/assets-index/export-assets-button.tsx b/app/components/assets/assets-index/export-assets-button.tsx index fc36e1874..50df7090e 100644 --- a/app/components/assets/assets-index/export-assets-button.tsx +++ b/app/components/assets/assets-index/export-assets-button.tsx @@ -1,14 +1,17 @@ import { useState } from "react"; +import { useLoaderData } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { selectedBulkItemsAtom } from "~/atoms/list"; +import { UpgradeMessage } from "~/components/marketing/upgrade-message"; import { Button } from "~/components/shared/button"; import { Spinner } from "~/components/shared/spinner"; +import type { AssetIndexLoaderData } from "~/routes/_layout+/assets._index"; import { isSelectingAllItems } from "~/utils/list"; export function ExportAssetsButton() { const selectedAssets = useAtomValue(selectedBulkItemsAtom); + const { canImportAssets } = useLoaderData(); const disabled = selectedAssets.length === 0; - const [isDownloading, setIsDownloading] = useState(false); const allSelected = isSelectingAllItems(selectedAssets); @@ -49,7 +52,16 @@ export function ExportAssetsButton() { className="font-medium" title={title} disabled={ - disabled + !canImportAssets + ? { + reason: ( + <> + Exporting is not available on the free tier of shelf.{" "} + + + ), + } + : disabled ? { reason: "You must select at least 1 asset to export" } : isDownloading } diff --git a/app/components/marketing/upgrade-message.tsx b/app/components/marketing/upgrade-message.tsx index 4dc323b2a..bfc1279d3 100644 --- a/app/components/marketing/upgrade-message.tsx +++ b/app/components/marketing/upgrade-message.tsx @@ -4,7 +4,11 @@ export function UpgradeMessage() { return ( <> Please consider{" "} - {" "} your subscription to access this feature.