DeenruvDeenruv
Rozszerzanie panelu administracyjnego

Hooki

Wszystkie hooki udostępniane przez @deenruv/react-ui-devkit — zarządzanie formularzami, listami, tłumaczeniami, zasobami i więcej

Pakiet @deenruv/react-ui-devkit udostępnia zestaw dedykowanych hooków React do budowania pluginów UI panelu administracyjnego. Hooki te obsługują typowe wzorce, takie jak stan formularza, paginowane listy, tłumaczenia i zarządzanie zasobami.

Zarządzanie formularzami

Deenruv wykorzystuje React Hook Form + Zod do zarządzania stanem formularzy i walidacji. Devkit udostępnia trzy komplementarne API:

APIZastosowanie
useDeenruvFormGłówny hook formularza — opakowuje RHF z walidacją Zod
createFormSchemaWygodny helper do budowania schematów Zod
useZodValidatorsGotowe, zlokalizowane walidatory Zod

Współpracują one z prymitywami formularzy (Form, FormField, FormItem, FormLabel, FormControl, FormMessage), tworząc w pełni dostępne, walidowane formularze.


useDeenruvForm — Główny hook formularza

Standardowy sposób tworzenia stanu formularza w pluginach panelu administracyjnego Deenruv. Opakowuje useForm z React Hook Form z walidacją schematu Zod, udostępniając wygodny helper setField oraz flagi poprawności.

Sygnatura

function useDeenruvForm<T extends FieldValues>(
  options: UseDeenruvFormOptions<T>,
): UseDeenruvFormReturn<T>;

interface UseDeenruvFormOptions<T extends FieldValues> {
  /** Schemat Zod do walidacji — przekaż z.object({...}) lub dowolny konkretny typ Zod. */
  schema: ZodSchema<T>;
  defaultValues?: UseFormProps<T>['defaultValues'];
  mode?: UseFormProps<T>['mode']; // domyślnie: 'onTouched'
}

Podstawowe użycie

import { useDeenruvForm, z } from '@deenruv/react-ui-devkit';

const schema = z.object({
  name: z.string().min(1, 'Nazwa jest wymagana'),
  slug: z.string().min(1, 'Slug jest wymagany'),
  description: z.string(),
  enabled: z.boolean(),
});

const form = useDeenruvForm({
  schema,
  defaultValues: { name: '', slug: '', description: '', enabled: true },
});

Dostęp do stanu i aktualizacja

// Obserwuj wartość pola (reaktywnie)
const name = form.watch('name');

// Ustaw pojedyncze pole (auto-walidacja + oznaczenie jako zmienione)
form.setField('name', 'Nowy Produkt');

// Użyj pełnego API RHF
form.setValue('slug', 'nowy-produkt');
form.reset({ name: '', slug: '', description: '', enabled: true });

Użycie z prymitywami formularza

import {
  useDeenruvForm, z,
  DeenruvForm,
  FormField, FormItem, FormLabel, FormControl, FormMessage,
  Input, Button,
} from '@deenruv/react-ui-devkit';

const schema = z.object({
  name: z.string().min(1, 'Nazwa jest wymagana'),
  slug: z.string(),
});

function ProductForm() {
  const form = useDeenruvForm({
    schema,
    defaultValues: { name: '', slug: '' },
  });

  const onSubmit = async (data: z.infer<typeof schema>) => {
    await apiClient('mutation')({
      updateProduct: [{ input: { id: productId, ...data } }, { id: true }],
    });
  };

  return (
    <DeenruvForm form={form} onSubmit={onSubmit}>
      <FormField
        control={form.control}
        name="name"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Nazwa produktu</FormLabel>
            <FormControl>
              <Input {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
      <FormField
        control={form.control}
        name="slug"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Slug</FormLabel>
            <FormControl>
              <Input {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
      <Button type="submit">Zapisz</Button>
    </DeenruvForm>
  );
}

DeenruvForm opakowuje provider shadcn/ui Form i element <form>. Przekazuje cały wynik useDeenruvForm do kontekstu RHF, dzięki czemu FormField, FormMessage itd. działają automatycznie.

Użycie z DetailView

import {
  DetailView, useDeenruvForm, z,
  DeenruvForm,
  FormField, FormItem, FormLabel, FormControl, FormMessage,
  Input, Button, Switch, PageBlock, RichTextEditor, useTranslation,
} from '@deenruv/react-ui-devkit';

const productSchema = z.object({
  name: z.string().min(1, 'Wymagane'),
  slug: z.string(),
  description: z.string(),
  enabled: z.boolean(),
});

type ProductFormValues = z.infer<typeof productSchema>;

function ProductDetailPage() {
  const { id } = useParams();
  const { t } = useTranslation('catalog');

  const form = useDeenruvForm({
    schema: productSchema,
    defaultValues: { name: '', slug: '', description: '', enabled: true },
  });

  const handleSave = async (data: ProductFormValues) => {
    await apiClient('mutation')({
      updateProduct: [
        {
          input: {
            id,
            enabled: data.enabled,
            translations: [{
              languageCode: LanguageCode.en,
              name: data.name,
              slug: data.slug,
              description: data.description,
            }],
          },
        },
        { id: true },
      ],
    });
  };

  return (
    <DeenruvForm form={form} onSubmit={handleSave}>
      <DetailView
        title={form.watch('name') || t('product.new')}
        locationId="products-detail-view"
        tabs={[
          {
            id: 'product',
            label: t('tabs.product'),
            content: (
              <PageBlock title={t('product.basicInfo')}>
                <FormField
                  control={form.control}
                  name="name"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>{t('product.name')}</FormLabel>
                      <FormControl><Input {...field} /></FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
                <FormField
                  control={form.control}
                  name="slug"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>{t('product.slug')}</FormLabel>
                      <FormControl><Input {...field} /></FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
                <FormField
                  control={form.control}
                  name="description"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>{t('product.description')}</FormLabel>
                      <FormControl>
                        <RichTextEditor value={field.value} onChange={field.onChange} />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
              </PageBlock>
            ),
          },
        ]}
        sidebar={
          <FormField
            control={form.control}
            name="enabled"
            render={({ field }) => (
              <FormItem>
                <FormLabel>{t('product.enabled')}</FormLabel>
                <FormControl>
                  <Switch checked={field.value} onCheckedChange={field.onChange} />
                </FormControl>
              </FormItem>
            )}
          />
        }
        actions={<Button type="submit">{t('common.save')}</Button>}
      />
    </DeenruvForm>
  );
}

Opis zwracanych wartości

useDeenruvForm zwraca wszystko z useForm React Hook Form, plus:

WłaściwośćTypOpis
setField(field, value) => voidUstaw pole z automatyczną walidacją i oznaczeniem jako zmienione
hasErrorsbooleanCzy formularz ma aktualnie jakiekolwiek błędy walidacji
isFormValidbooleanCzy formularz spełnia wszystkie reguły walidacji
controlControl<T>Kontrolka RHF — przekaż do FormField
watch(name?) => T[K]Subskrybuj zmiany wartości pola
handleSubmit(onValid) => (e) => voidHandler wysłania formularza
reset(values?) => voidZresetuj formularz do domyślnych lub podanych wartości
setValue(name, value, options?) => voidNiskopoziomowy setter pola RHF
getValues() => TPobierz wszystkie aktualne wartości pól
formStateFormState<T>Pełny stan formularza RHF (errors, isDirty, isSubmitting itp.)

createFormSchema — Budowniczy schematów

Wygodny wrapper wokół z.object() dla spójnego tworzenia schematów:

import { createFormSchema } from '@deenruv/react-ui-devkit';
import { z } from 'zod';

const schema = createFormSchema({
  name: z.string().min(1, 'Nazwa jest wymagana'),
  code: z.string().min(1, 'Kod jest wymagany'),
  price: z.number().min(0),
  customFields: z.record(z.unknown()).optional(),
});

To odpowiednik bezpośredniego wywołania z.object(...), ale czyni intencję jasną i zapewnia standardowy wzorzec dla schematów formularzy w panelu administracyjnym.


useZodValidators — Gotowe walidatory

Udostępnia zlokalizowane schematy Zod dla typowych wzorców walidacji. Komunikaty błędów są automatycznie tłumaczone przy użyciu systemu tłumaczeń panelu administracyjnego.

import { useZodValidators, createFormSchema, useDeenruvForm } from '@deenruv/react-ui-devkit';

function MyForm() {
  const { requiredString, email, positiveNumber, customFields } = useZodValidators();

  const schema = createFormSchema({
    name: requiredString(),           // z.string().min(1, t('validation.required'))
    emailAddress: email(),            // z.string().email(t('validation.invalidEmail'))
    price: positiveNumber(),          // z.number().min(0, t('validation.mustBePositive'))
    customFields,                     // z.record(z.unknown()).optional().default({})
  });

  const form = useDeenruvForm({
    schema,
    defaultValues: { name: '', emailAddress: '', price: 0, customFields: {} },
  });

  // ...
}

Dostępne walidatory

WalidatorZwracaOpis
requiredString(msg?)z.ZodStringNiepusty string, domyślnie zlokalizowany "Wymagane"
email(msg?)z.ZodStringPoprawny email, domyślnie zlokalizowany "Nieprawidłowy email"
positiveNumber(msg?)z.ZodNumberLiczba >= 0, domyślnie zlokalizowany "Musi być dodatnia"
nonEmptyArray(msg?)z.ZodArrayTablica z co najmniej 1 elementem
translationsWithNamez.ZodArrayTablica obiektów tłumaczeń z niepustym name
customFieldsz.ZodOptionalPermisywny record dla pól niestandardowych, domyślnie {}

DeenruvForm — Komponent wrappera formularza

Komponent wrapper, który zapewnia kontekst formularza i obsługuje wysyłanie:

import { DeenruvForm } from '@deenruv/react-ui-devkit';

<DeenruvForm form={form} onSubmit={handleSubmit} className="space-y-4">
  {/* Komponenty FormField tutaj */}
</DeenruvForm>
PropTypOpis
formUseDeenruvFormReturn<T>Wartość zwracana z useDeenruvForm
onSubmit(data: T) => void | Promise<void>Wywoływany ze zwalidowanymi danymi przy wysłaniu formularza
childrenReactNodeTreść formularza (komponenty FormField, przyciski itp.)
classNamestring?Opcjonalna klasa CSS dla elementu <form>

Prymitywy formularza

Devkit reeksportuje prymitywy formularzy shadcn/ui do budowania dostępnych, walidowanych pól:

KomponentOpis
FormRHF FormProvider — zapewnia kontekst formularza dla dzieci
FormFieldWrapper RHF Controller — łączy pole z formularzem
FormItemKontener pola z odpowiednimi odstępami
FormLabelDostępna etykieta, która zmienia kolor na czerwony przy błędach walidacji
FormControlSlot przekazujący aria-invalid i aria-describedby do inputa
FormDescriptionOpcjonalny tekst opisu pola
FormMessageWyświetla komunikat błędu walidacji pola

Standardowy wzorzec

<FormField
  control={form.control}
  name="fieldName"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Etykieta</FormLabel>
      <FormControl>
        <Input {...field} />
      </FormControl>
      <FormDescription>Tekst pomocniczy</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Dostępność: Gdy pole ma błędy walidacji, FormControl ustawia aria-invalid="true" na inpucie i łączy go z FormMessage przez aria-describedby. Czytniki ekranu ogłaszają błędy automatycznie.


useList — Paginowane listy

Zarządza stanem paginowanej listy z parametrami URL do sortowania, filtrowania i paginacji. Zwraca gotowy komponent JSX Paginate.

Podstawowe użycie

import { useList, apiClient } from '@deenruv/react-ui-devkit';

const {
  Paginate,           // Gotowy komponent paginacji JSX
  objects,            // Elementy bieżącej strony
  total,              // Łączna liczba elementów
  setSort,            // Ustaw kolumnę sortowania
  setFilter,          // Ustaw obiekt filtra
  setFilterField,     // Ustaw pojedyncze pole filtra
  removeFilterField,  // Usuń pojedyncze pole filtra
  resetFilter,        // Wyczyść wszystkie filtry
  optionInfo,         // Bieżące { page, perPage, sort, filter, filterOperator }
  refetch,            // Ręczne ponowne pobranie
  isFilterOn,         // Czy jakiekolwiek filtry są aktywne
} = useList({
  route: (options) =>
    apiClient('query')({
      products: [
        {
          options: {
            take: options.perPage,
            skip: (options.page - 1) * options.perPage,
            sort: options.sort
              ? { [options.sort.key]: options.sort.sortDir }
              : undefined,
            filter: options.filter,
          },
        },
        { totalItems: true, items: { id: true, name: true, slug: true } },
      ],
    }).then((r) => r.products),
  listType: 'products',
});

Użycie z szablonem DetailList

Hook useList jest używany wewnętrznie przez komponent szablonu DetailList. Jeśli budujesz standardową stronę listy, preferuj DetailList dla interfejsu wyższego poziomu. Użyj useList bezpośrednio, gdy potrzebujesz pełnej kontroli nad układem listy.

Opis zwracanych wartości

WłaściwośćTypOpis
PaginateJSX.ElementGotowy komponent paginacji
objectsT[]Elementy bieżącej strony
totalnumberŁączna liczba na wszystkich stronach
setSort(sort) => voidUstaw kolumnę i kierunek sortowania
setFilter(filter) => voidUstaw pełny obiekt filtra
setFilterField(field, value) => voidUstaw pojedyncze pole filtra
removeFilterField(field) => voidUsuń pojedyncze pole filtra
resetFilter() => voidWyczyść wszystkie filtry
optionInfoobjectBieżąca strona, perPage, sort, filter, filterOperator
refetch() => voidUruchom ręczne ponowne pobranie
isFilterOnbooleanCzy jakikolwiek filtr jest aktywny

useTranslation — Internacjonalizacja

Nigdy nie importuj react-i18next bezpośrednio. Zawsze używaj useTranslation z @deenruv/react-ui-devkit. Ten hook wiąże się z globalną instancją i18n Deenruv poprzez window.__DEENRUV_SETTINGS__.i18n, zapewniając prawidłowe działanie tłumaczeń z systemem językowym panelu administracyjnego.

Podstawowe użycie

import { useTranslation } from '@deenruv/react-ui-devkit';

const { t, tEntity, i18n } = useTranslation('my-plugin-namespace');

// Standardowe tłumaczenie
t('my.key');                                    // "Mój przetłumaczony tekst"

// Tłumaczenie uwzględniające encje z pluralizacją
tEntity('entity.title', 'Product', 'one');      // "Product"
tEntity('entity.title', 'Product', 'many');     // "Products"
tEntity('entity.title', 'Product', 5);          // "5 Products"

W komponentach

function MyComponent() {
  const { t } = useTranslation('reviews');

  return (
    <div>
      <h1>{t('page.title')}</h1>
      <p>{t('page.description')}</p>
    </div>
  );
}

useAssets — Zarządzanie zasobami

Zarządza przeglądaniem zasobów z paginacją, wyszukiwaniem tekstowym i filtrowaniem tagów:

import { useAssets } from '@deenruv/react-ui-devkit';

const {
  assets,           // Bieżąca strona zasobów
  isPending,        // Stan ładowania
  error,            // Komunikat błędu
  totalItems,       // Łączna liczba zasobów
  refetchData,      // Ręczne ponowne pobranie
  page, setPage,
  perPage, setPerPage,
  searchTerm, setSearchTerm,
  searchTags, setSearchTags,
  totalPages,
} = useAssets();

Opis zwracanych wartości

WłaściwośćTypOpis
assetsAsset[]Bieżąca strona zasobów
isPendingbooleanCzy trwa pobieranie
errorstring | undefinedKomunikat błędu jeśli pobieranie się nie powiodło
totalItemsnumberŁączna liczba zasobów
refetchData() => voidUruchom ręczne ponowne pobranie
page / setPagenumber / (n) => voidBieżąca strona
perPage / setPerPagenumber / (n) => voidElementów na stronę
searchTerm / setSearchTermstring / (s) => voidZapytanie wyszukiwania tekstowego
searchTags / setSearchTagsstring[] / (tags) => voidFiltr tagów
totalPagesnumberObliczona łączna liczba stron

useDebounce

Reeksportowany z pakietu use-debounce. Opóźnia wartość z konfigurowalnym czasem:

import { useDebounce } from '@deenruv/react-ui-devkit';

const [debouncedSearch] = useDebounce(searchTerm, 300);

useLocalStorage

Trwałe zarządzanie stanem z użyciem localStorage przeglądarki:

import { useLocalStorage } from '@deenruv/react-ui-devkit';

const [viewMode, setViewMode] = useLocalStorage('plugin-view-mode', 'grid');

useErrorHandler

Scentralizowana obsługa błędów dla błędów GraphQL i API:

import { useErrorHandler } from '@deenruv/react-ui-devkit';

const { handleError } = useErrorHandler();

try {
  await apiClient('mutation')({ /* ... */ });
} catch (error) {
  handleError(error);
}

useCustomSearchParams

Helper do zarządzania parametrami URL w widokach list. Używany wewnętrznie przez useList:

import { useCustomSearchParams } from '@deenruv/react-ui-devkit';

const { searchParams, setSearchParam } = useCustomSearchParams();

Na tej stronie