Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion apps/api/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,9 +568,22 @@
"submit": "Save changes",
"success": "Role updated"
},
"tabs": {
"general": "General",
"content": "Content"
},
"form": {
"name": "Name",
"color": "Color"
"color": "Color",
"upload": {
"allow": "Allow upload files",
"total_max_storage": "Total Max Storage",
"max_storage_for_submit": "Max Storage for Submit",
"max_storage_for_submit_desc": "If you set 1000, user can upload only 1 file with size 1000kB or 2 files with size 500kB for each submit.",
"in_unit": "in kB",
"or": "or",
"unlimited": "Unlimited"
}
}
},
"staff": {
Expand Down
18 changes: 9 additions & 9 deletions apps/docs/content/docs/dev/search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ VitNode ships a content **search & discovery** system. Every searchable item is
projected into a single canonical table, `core_search_index`, which powers two
public pages and the AdminCP:

- **`/search`** full-text search with relevance ranking and filters.
- **`/discover`** the same index sorted newest-first (a community timeline).
- **AdminCP → user profile → Timeline** a single member's indexed activity.
- **`/search`** - full-text search with relevance ranking and filters.
- **`/discover`** - the same index sorted newest-first (a community timeline).
- **AdminCP → user profile → Timeline** - a single member's indexed activity.

Search and discovery are **locale-aware**: results follow the active language, so
a member browsing in Polish sees Polish titles, snippets and links. See
[Multi-language content](#multi-language-content) below.

The query engine is **pluggable**. The default requires zero setup:

- **Postgres** (default) Postgres full-text search (`tsvector` + GIN, title
- **Postgres** (default) - Postgres full-text search (`tsvector` + GIN, title
weighted above body). Great for small and medium communities.
- **Elasticsearch** (`@vitnode/elasticsearch`) offloads querying to an external
- **Elasticsearch** (`@vitnode/elasticsearch`) - offloads querying to an external
cluster and unlocks advanced ranking (time-decay, author-boost).

`core_search_index` is always the source of truth, so switching engines is a
Expand Down Expand Up @@ -79,13 +79,13 @@ for (const languageCode of enabledLanguageCodes) {

The `/search` and `/discover` requests send the active locale as a `lang` query
param, and the query keeps only rows for that locale. Content that isn't
translated can leave `languageCode` empty (`""`) those rows are **language
translated can leave `languageCode` empty (`""`) - those rows are **language
agnostic** and match every locale, so single-language plugins need no changes.

<Callout type="info">
Postgres full-text ranking uses the `english` text-search configuration for all
languages (there is no bundled dictionary for most locales). Matching still works
across languages; only stemming and stop-words are English-tuned.
Postgres full-text ranking uses the `english` text-search configuration for
all languages (there is no bundled dictionary for most locales). Matching
still works across languages; only stemming and stop-words are English-tuned.
</Callout>

## Registering a rebuild indexer
Expand Down
26 changes: 13 additions & 13 deletions apps/docs/content/docs/ui/alert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
Alert,
AlertDescription,
AlertTitle,
} from '@vitnode/core/components/ui/alert';
import { TriangleAlertIcon } from 'lucide-react';
} from "@vitnode/core/components/ui/alert";
import { TriangleAlertIcon } from "lucide-react";
```

```tsx
Expand All @@ -26,24 +26,24 @@ import { TriangleAlertIcon } from 'lucide-react';
</Alert>
```

An optional leading icon is picked up automatically render any icon as the
An optional leading icon is picked up automatically - render any icon as the
first child and the layout aligns it with the title and description.

## Variants

- `default` neutral, informational messages.
- `warning` a non-blocking caution the user should act on (amber).
- `destructive` an error or a failed action (red).
- `default` - neutral, informational messages.
- `warning` - a non-blocking caution the user should act on (amber).
- `destructive` - an error or a failed action (red).

## Dismissible

Wrap a control in `AlertAction` to pin it to the top-right corner, e.g. a close
button. The alert reserves space for it automatically.

```tsx
import { AlertAction } from '@vitnode/core/components/ui/alert';
import { Button } from '@vitnode/core/components/ui/button';
import { XIcon } from 'lucide-react';
import { AlertAction } from "@vitnode/core/components/ui/alert";
import { Button } from "@vitnode/core/components/ui/button";
import { XIcon } from "lucide-react";

<Alert variant="warning">
<TriangleAlertIcon />
Expand All @@ -59,14 +59,14 @@ import { XIcon } from 'lucide-react';

## Props

import { TypeTable } from 'fumadocs-ui/components/type-table';
import { TypeTable } from "fumadocs-ui/components/type-table";

<TypeTable
type={{
variant: {
description: 'The visual style of the alert.',
type: 'default | warning | destructive',
default: 'default',
description: "The visual style of the alert.",
type: "default | warning | destructive",
default: "default",
},
}}
/>
74 changes: 74 additions & 0 deletions apps/docs/content/docs/ui/auto-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,80 @@ const formSchema = z.object({
/>
```

## Tabs

Group fields into tabs by passing the `tabs` prop and tagging each field with a
`tab`. Fields without a `tab` fall into the first tab. Every tab panel stays
mounted, so field values and validation are preserved when switching tabs.

```tsx
<AutoForm
formSchema={formSchema}
// [!code ++]
tabs={[
// [!code ++]
{ value: "general", label: "General" },
// [!code ++]
{ value: "content", label: "Content" },
// [!code ++]
]}
fields={[
{
id: "username",
tab: "general", // [!code ++]
component: props => <AutoFormInput {...props} label="Username" />,
},
{
id: "content",
tab: "content", // [!code ++]
component: props => <AutoFormEditor {...props} label="Content" />,
},
]}
/>
```

## Conditional Fields

Show or hide a field based on the current form values with the `hidden`
predicate. It receives the live form values and returns `true` to hide the
field.

<Callout type="warn">
Hidden fields still submit their (default) values, so keep them optional or
give them a `default` in the schema. Otherwise a hidden-but-invalid field can
keep the submit button disabled with no visible error.
</Callout>

```ts
const formSchema = z.object({
allow_uploads: z.boolean().default(false),
// Optional so it never blocks submission while hidden.
max_storage: z.number().int().min(0).nullable().default(null),
});
```

```tsx
import { AutoFormSwitch } from "@vitnode/core/components/form/fields/switch";
```

```tsx
<AutoForm
formSchema={formSchema}
fields={[
{
id: "allow_uploads",
component: props => <AutoFormSwitch {...props} label="Allow uploads" />,
},
{
id: "max_storage",
// Only shown once uploads are enabled. // [!code ++]
hidden: values => !values.allow_uploads, // [!code ++]
component: props => <AutoFormInput {...props} label="Max storage" />,
},
]}
/>
```

## Form Submission

To activate submit button and handle form submission with the `onSubmit` callback:
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/ui/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"editor",
"input",
"input-group",
"nullable-number",
"radio-group",
"select",
"switch",
Expand Down
106 changes: 106 additions & 0 deletions apps/docs/content/docs/ui/nullable-number.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: Nullable Number
description: Numeric field paired with a checkbox that toggles the value to null - for "unlimited", "never", "no limit", and similar.
---

## Preview

<Preview name="nullable-number" />

## Usage

```ts
import { z } from "zod";
import { AutoForm } from "@vitnode/core/components/form/auto-form";
import { AutoFormNullableNumber } from "@vitnode/core/components/form/fields/nullable-number";
```

```ts
const formSchema = z.object({
max_members: z.number().int().min(1).nullable().default(10),
});
```

```tsx
<AutoForm
formSchema={formSchema}
fields={[
{
id: "max_members",
component: props => (
<AutoFormNullableNumber
{...props}
label="Maximum members"
toggleLabel="Unlimited"
/>
),
},
]}
/>
```

<Callout type="info" title="Value shape">
The field value is `number | null`. A number is whatever is typed in the
input; `null` means the checkbox is checked and the input is disabled. Back it
with a `z.number().nullable()` schema, and keep it optional or give it a
`default` when the field can be
[hidden](/docs/ui/auto-form#conditional-fields) so it never blocks submission.
Unchecking the box restores the last number you entered.
</Callout>

## Adapting the labels

The three label props are plain text, so the same field works for any domain -
an unlimited storage cap, a session that never expires, an uncapped rate limit,
and so on:

- `unitLabel` - shown right after the input (e.g. `kB`, `minutes`, `%`).
- `orLabel` - an optional connector rendered before the checkbox (e.g. `or`).
- `toggleLabel` - the checkbox label; checking it sets the value to `null`.

```tsx
<AutoFormNullableNumber
{...props}
label="Auto-logout"
unitLabel="minutes"
orLabel="or"
toggleLabel="Never"
/>
```

Any other props (`min`, `max`, `step`, `placeholder`, …) are forwarded to the
underlying number input; validation constraints come from the Zod schema.

## Props

import { TypeTable } from "fumadocs-ui/components/type-table";

<TypeTable
type={{
label: {
description: "The label displayed above the field.",
type: "React.ReactNode",
default: "",
},
description: {
description: "A short description displayed below the field.",
type: "React.ReactNode",
default: "",
},
toggleLabel: {
description:
"Label for the checkbox. Checking it sets the value to null and disables the input. Required.",
type: "React.ReactNode",
},
unitLabel: {
description: "Optional unit shown right after the input.",
type: "React.ReactNode",
default: "",
},
orLabel: {
description: "Optional connector text rendered before the checkbox.",
type: "React.ReactNode",
default: "",
},
}}
/>
3 changes: 3 additions & 0 deletions apps/docs/migrations/0019_add_role_upload_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE "core_roles" ADD COLUMN "allowUploadFiles" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "core_roles" ADD COLUMN "totalMaxStorage" integer;--> statement-breakpoint
ALTER TABLE "core_roles" ADD COLUMN "maxStorageForSubmit" integer;
Loading
Loading