feat(ui): add backend icons to variant selectors
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const BACKEND_ICONS: Record<string, { path: string; isIco?: boolean }> = {
|
||||
alibaba: { path: '/icons/backends/alibaba.svg' },
|
||||
alibabacloud: { path: '/icons/backends/alibaba.svg' },
|
||||
aws: { path: '/icons/backends/aws-light.svg' },
|
||||
amazonwebservices: { path: '/icons/backends/aws-light.svg' },
|
||||
azure: { path: '/icons/backends/azure.ico', isIco: true },
|
||||
baidu: { path: '/icons/backends/baiducloud-color.svg' },
|
||||
baiducloud: { path: '/icons/backends/baiducloud-color.svg' },
|
||||
bunny: { path: '/icons/backends/bunny.svg' },
|
||||
bunnynet: { path: '/icons/backends/bunny.svg' },
|
||||
cloudflare: { path: '/icons/backends/cloudflare.svg' },
|
||||
digitalocean: { path: '/icons/backends/digital-ocean.svg' },
|
||||
dnsimple: { path: '/icons/backends/dnsimple.svg' },
|
||||
foundationdb: { path: '/icons/backends/foundationdb.svg' },
|
||||
google: { path: '/icons/backends/google.svg' },
|
||||
googlecloud: { path: '/icons/backends/google.svg' },
|
||||
mariadb: { path: '/icons/backends/mysql.svg' },
|
||||
mysql: { path: '/icons/backends/mysql.svg' },
|
||||
ovh: { path: '/icons/backends/ovh.svg' },
|
||||
porkbun: { path: '/icons/backends/porkbun.png' },
|
||||
postgres: { path: '/icons/backends/postgresql.svg' },
|
||||
postgresql: { path: '/icons/backends/postgresql.svg' },
|
||||
quad9: { path: '/icons/backends/quad9.svg' },
|
||||
redis: { path: '/icons/backends/redis.svg' },
|
||||
rediscluster: { path: '/icons/backends/redis.svg' },
|
||||
redissentinel: { path: '/icons/backends/redis.svg' },
|
||||
redisvalkey: { path: '/icons/backends/redis.svg' },
|
||||
rocksdb: { path: '/icons/backends/rocksdb.svg' },
|
||||
sqlite: { path: '/icons/backends/sqlite.svg' },
|
||||
valkey: { path: '/icons/backends/valkey.svg' },
|
||||
vercel: { path: '/icons/backends/vercel.svg' },
|
||||
};
|
||||
|
||||
interface BackendIconProps {
|
||||
backend: string | null | undefined;
|
||||
className?: string;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function BackendIcon({ backend, className, fallback = null }: BackendIconProps): React.ReactElement | null {
|
||||
if (!backend) return null;
|
||||
|
||||
const key = backend.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const icon = BACKEND_ICONS[key] ?? BACKEND_ICONS[backend.toLowerCase()];
|
||||
if (!icon) return fallback as React.ReactElement | null;
|
||||
|
||||
return (
|
||||
<img
|
||||
src={icon.path}
|
||||
alt={`${backend} icon`}
|
||||
className={className ?? 'h-4 w-4 object-contain'}
|
||||
loading="lazy"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function isBackendIconKnown(backend: string | null | undefined): boolean {
|
||||
if (!backend) return false;
|
||||
const key = backend.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
return Boolean(BACKEND_ICONS[key] ?? BACKEND_ICONS[backend.toLowerCase()]);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -53,6 +53,7 @@ import { SECRET_MASK } from '@/lib/jmapUtils';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { logFormChange } from '@/lib/debug';
|
||||
import { FieldWidget } from '@/components/forms/FieldWidget';
|
||||
import { BackendIcon } from '@/components/common/BackendIcon';
|
||||
|
||||
import type { Field, Fields, Form, FormField, Schema } from '@/types/schema';
|
||||
import type { JmapSetResponse, JmapSetError, JmapMethodCall } from '@/types/jmap';
|
||||
@@ -773,17 +774,26 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
|
||||
if (!visible) return null;
|
||||
|
||||
if (formField.name === '@type' && sch.type === 'multiple') {
|
||||
const selectedVariantLabel = sch.variants.find((v) => v.name === selectedVariant)?.label;
|
||||
return (
|
||||
<div key="@type" className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">{formField.label}</Label>
|
||||
<Select value={selectedVariant} onValueChange={handleVariantChange} disabled={readOnly}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('form.selectType', 'Select type...')} />
|
||||
<span className="flex flex-1 items-center gap-2 overflow-hidden">
|
||||
<BackendIcon backend={selectedVariant} />
|
||||
<span className="truncate">
|
||||
{selectedVariantLabel || t('form.selectType', 'Select type...')}
|
||||
</span>
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sch.variants.map((v) => (
|
||||
<SelectItem key={v.name} value={v.name}>
|
||||
{v.label}
|
||||
<span className="flex items-center gap-2">
|
||||
<BackendIcon backend={v.name} />
|
||||
{v.label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { cn } from '@/lib/utils';
|
||||
import { useAccountStore } from '@/stores/accountStore';
|
||||
import { useEffectiveEdition } from '@/components/forms/FormEditionContext';
|
||||
import { useObjectList, useObjectLabel, useNoPermissionMessage, type ObjectOption } from '@/lib/objectOptions';
|
||||
import { BackendIcon } from '@/components/common/BackendIcon';
|
||||
import { SECRET_MASK } from '@/lib/jmapUtils';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { jmapGet, getAccountId } from '@/services/jmap/client';
|
||||
@@ -1468,18 +1469,28 @@ function EmbeddedObjectField({
|
||||
onChange(newObj);
|
||||
};
|
||||
|
||||
const selectedVariantLabel = resolvedSchema.variants.find((v) => v.name === currentType)?.label;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border p-4 space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">{formField.label ?? t('field.type', 'Type')}</Label>
|
||||
<Select value={currentType} onValueChange={handleVariantChange} disabled={readOnly}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('form.selectType', 'Select type...')} />
|
||||
<span className="flex flex-1 items-center gap-2 overflow-hidden">
|
||||
<BackendIcon backend={currentType} />
|
||||
<span className="truncate">
|
||||
{selectedVariantLabel || t('form.selectType', 'Select type...')}
|
||||
</span>
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{resolvedSchema.variants.map((v) => (
|
||||
<SelectItem key={v.name} value={v.name}>
|
||||
{v.label}
|
||||
<span className="flex items-center gap-2">
|
||||
<BackendIcon backend={v.name} />
|
||||
{v.label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
Reference in New Issue
Block a user