feat: compare limit 9, format-aware import placeholder, help links
- Raise compare limit from 8 to 9 (server + client, toast on overflow) - Make import textarea placeholder match selected format (CSV/JSON/YAML/auto) - Add GuideHelp links to admin create/edit user dialogs and tool import dialog
This commit is contained in:
@@ -268,8 +268,8 @@ router.get("/compare", requireFeature("compare"), async (req, res): Promise<void
|
||||
res.status(400).json({ error: "Provide at least one ids value, e.g. ?ids=1,2,3" });
|
||||
return;
|
||||
}
|
||||
if (ids.length > 8) {
|
||||
res.status(400).json({ error: "Maximum of 8 tools can be compared" });
|
||||
if (ids.length > 9) {
|
||||
res.status(400).json({ error: "Maximum of 9 tools can be compared" });
|
||||
return;
|
||||
}
|
||||
const tools = await db.select().from(toolsTable).where(and(isNull(toolsTable.deletedAt), inArray(toolsTable.id, ids)));
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { GuideHelp } from "@/components/guide-help";
|
||||
import { Upload, FileUp, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
|
||||
type Format = "auto" | "csv" | "json" | "yaml";
|
||||
@@ -62,14 +63,24 @@ export function ToolImportDialog({
|
||||
|
||||
const canSubmit = data.trim().length > 0 && !importTools.isPending;
|
||||
|
||||
const placeholder =
|
||||
format === "csv"
|
||||
? t("adminTools.importTextareaPlaceholderCsv")
|
||||
: format === "json"
|
||||
? t("adminTools.importTextareaPlaceholderJson")
|
||||
: format === "yaml"
|
||||
? t("adminTools.importTextareaPlaceholderYaml")
|
||||
: t("adminTools.importTextareaPlaceholderAuto");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Upload className="w-4 h-4" />
|
||||
{t("adminTools.importTitle")}
|
||||
</DialogTitle>
|
||||
<GuideHelp guide="administration" label={t("adminTools.importTitle")} />
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
@@ -131,7 +142,7 @@ export function ToolImportDialog({
|
||||
<Textarea
|
||||
value={data}
|
||||
onChange={(e) => setData(e.target.value)}
|
||||
placeholder={t("adminTools.importTextareaPlaceholder")}
|
||||
placeholder={placeholder}
|
||||
className="min-h-[220px] font-mono text-sm"
|
||||
data-testid="input-tool-import-data"
|
||||
/>
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"compareCount": "Vergleichen ({{count}})",
|
||||
"addToCompare": "Zum Vergleich hinzufügen",
|
||||
"removeFromCompare": "Aus dem Vergleich entfernen",
|
||||
"compareLimitReached": "Maximal 9 Tools können verglichen werden.",
|
||||
"comparePremiumTitle": "Vergleichen ist ein Premium-Feature",
|
||||
"comparePremiumSub": "Das Vergleichen von Tools nebeneinander ist für Premium- und Enterprise-Nutzer verfügbar. Erweitere deinen Plan, um es freizuschalten.",
|
||||
"notNow": "Später",
|
||||
@@ -418,6 +419,10 @@
|
||||
"importYamlSub": "YAML-Liste von Objekten (oder ein einzelnes Objekt) mit den Feldern name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||
"importFile": "Datei laden",
|
||||
"importTextareaPlaceholder": "name;description;category;features;tags\nMein Tool;Eine tolle Beschreibung;Monitoring;Deployment|Open Source;devops",
|
||||
"importTextareaPlaceholderCsv": "name;description;category;features;tags\nMein Tool;Eine tolle Beschreibung;Monitoring;Deployment|Open Source;devops",
|
||||
"importTextareaPlaceholderJson": "[\n {\n \"name\": \"Mein Tool\",\n \"description\": \"Eine tolle Beschreibung\",\n \"category\": \"Monitoring\",\n \"features\": [\"Deployment\", \"Open Source\"],\n \"tags\": [\"devops\"]\n }\n]",
|
||||
"importTextareaPlaceholderYaml": "- name: Mein Tool\n description: Eine tolle Beschreibung\n category: Monitoring\n features:\n - Deployment\n - Open Source\n tags:\n - devops",
|
||||
"importTextareaPlaceholderAuto": "name;description;category\nMein Tool;Eine tolle Beschreibung;Monitoring",
|
||||
"importing": "Wird importiert…",
|
||||
"importDone": "{{count}} von {{total}} Tools erfolgreich importiert.",
|
||||
"importErrors": "{{count}} Zeilen mit Fehlern:",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"compareCount": "Compare ({{count}})",
|
||||
"addToCompare": "Add to compare",
|
||||
"removeFromCompare": "Remove from compare",
|
||||
"compareLimitReached": "A maximum of 9 tools can be compared.",
|
||||
"comparePremiumTitle": "Compare is a Premium feature",
|
||||
"comparePremiumSub": "Comparing tools side-by-side is available to Premium and Enterprise users. Upgrade your plan to unlock it.",
|
||||
"notNow": "Not now",
|
||||
@@ -418,6 +419,10 @@
|
||||
"importYamlSub": "A YAML list of objects (or a single object) with the fields name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||
"importFile": "Upload file",
|
||||
"importTextareaPlaceholder": "name;description;category;features;tags\nMy Tool;A great description;Monitoring;Deployment|Open Source;devops",
|
||||
"importTextareaPlaceholderCsv": "name;description;category;features;tags\nMy Tool;A great description;Monitoring;Deployment|Open Source;devops",
|
||||
"importTextareaPlaceholderJson": "[\n {\n \"name\": \"My Tool\",\n \"description\": \"A great description\",\n \"category\": \"Monitoring\",\n \"features\": [\"Deployment\", \"Open Source\"],\n \"tags\": [\"devops\"]\n }\n]",
|
||||
"importTextareaPlaceholderYaml": "- name: My Tool\n description: A great description\n category: Monitoring\n features:\n - Deployment\n - Open Source\n tags:\n - devops",
|
||||
"importTextareaPlaceholderAuto": "name;description;category\nMy Tool;A great description;Monitoring",
|
||||
"importing": "Importing…",
|
||||
"importDone": "{{count}} of {{total}} tools imported successfully.",
|
||||
"importErrors": "{{count}} rows with errors:",
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench, Server } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { AdminToolsTab } from "@/components/admin-tools-tab";
|
||||
import { GuideHelp } from "@/components/guide-help";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Admin() {
|
||||
@@ -359,8 +360,9 @@ export default function Admin() {
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||
<DialogTitle>{t("admin.createNewUser")}</DialogTitle>
|
||||
<GuideHelp guide="administration" label={t("admin.createNewUser")} />
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
@@ -412,8 +414,9 @@ export default function Admin() {
|
||||
|
||||
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) { setEditUser(null); setEditPassword(""); } }}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||
<DialogTitle>{t("admin.editUser", { username: editUser?.username })}</DialogTitle>
|
||||
<GuideHelp guide="administration" label={t("admin.editUser", { username: editUser?.username })} />
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||
import { useBrowsePreferences, isViewMode, isDensity } from "@/hooks/use-browse-preferences";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { CompareBar } from "@/components/compare-bar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -91,12 +92,20 @@ export default function ToolsBrowse() {
|
||||
const { serverView, serverDensity, localView, localDensity, persist } = useBrowsePreferences();
|
||||
const { hasFeature } = useAuth();
|
||||
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [compareIds, setCompareIds] = useState<number[]>([]);
|
||||
const [compareUpsellOpen, setCompareUpsellOpen] = useState(false);
|
||||
|
||||
function toggleCompare(id: number) {
|
||||
setCompareIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||
setCompareIds((prev) => {
|
||||
if (prev.includes(id)) return prev.filter((x) => x !== id);
|
||||
if (prev.length >= 9) {
|
||||
toast({ title: t("browse.compareLimitReached"), variant: "destructive" });
|
||||
return prev;
|
||||
}
|
||||
return [...prev, id];
|
||||
});
|
||||
}
|
||||
|
||||
const [search, setSearch] = useState(initialSearch);
|
||||
|
||||
Reference in New Issue
Block a user