# Was ist ein Widget?
Source: https://www.arc-rider.com/docs/intro/was-sind-widgets
# Was ist ein Widget in React?
Ein arcWidget ist eine konfigurierbare Komponente für eine Geschäftsanwendung: zum Beispiel eine Tabelle, ein Kalender oder ein Kanban-Board. Du übergibst die Darstellung als `data` und verbindest Ereignisse über Callback-Props mit deiner Anwendung.
## Ein vollständiges Beispiel
```tsx
import {
ArcWidgetBadge,
ArcWidgetIcon,
ArcWidgetKpiBar,
ArcWidgetProgressBar,
ArcWidgetTable,
type ArcWidgetTableData,
type ArcWidgetTableRow,
} from '@arcrider/arcwidgets-react';
import { useMemo, useState, type CSSProperties } from 'react';
const requested = new URLSearchParams(location.search).get('data');
const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo';
type Theme = 'light' | 'dark';
type DbStatus = 'Active' | 'Idle' | 'Locked';
type DbRow = {
id: string;
schema: string;
name: string;
type: 'Table' | 'View';
rows: number;
size: string;
status: DbStatus;
};
const STATUS_LOOK: Record<
DbStatus,
{ fontColor: string; backgroundColor: string }
> = {
Active: { fontColor: '#15803D', backgroundColor: '#DCFCE7' },
Idle: { fontColor: '#52525B', backgroundColor: '#F4F4F5' },
Locked: { fontColor: '#991B1B', backgroundColor: '#FFE4E6' },
};
const STATUS_LOOK_DARK: Record<
DbStatus,
{ fontColor: string; backgroundColor: string }
> = {
Active: { fontColor: '#86EFAC', backgroundColor: '#14532D' },
Idle: { fontColor: '#A1A1AA', backgroundColor: '#27272A' },
Locked: { fontColor: '#FDA4AF', backgroundColor: '#4C0519' },
};
const SCHEMA_THEME = {
public: {
light: { bg: '#E8F1FF', ink: '#2563EB', track: '#BFDBFE' },
dark: { bg: '#152033', ink: '#93C5FD', track: '#1E3A5F' },
},
auth: {
light: { bg: '#FFF4E8', ink: '#C2410C', track: '#FED7AA' },
dark: { bg: '#24180E', ink: '#FDBA74', track: '#4A2E12' },
},
storage: {
light: { bg: '#E8F8EF', ink: '#15803D', track: '#BBF7D0' },
dark: { bg: '#122018', ink: '#86EFAC', track: '#14532D' },
},
} as const;
type SchemaName = keyof typeof SCHEMA_THEME;
const SCHEMAS = Object.keys(SCHEMA_THEME) as SchemaName[];
const STATUSES: DbStatus[] = ['Active', 'Idle', 'Locked'];
const demoRows: DbRow[] = [
{
id: 'tbl-users',
schema: 'public',
name: 'users',
type: 'Table',
rows: 1204,
size: '4.2 MB',
status: 'Active',
},
{
id: 'tbl-orders',
schema: 'public',
name: 'orders',
type: 'Table',
rows: 43012,
size: '128.0 MB',
status: 'Active',
},
{
id: 'tbl-products',
schema: 'public',
name: 'products',
type: 'Table',
rows: 890,
size: '1.1 MB',
status: 'Idle',
},
{
id: 'vw-revenue',
schema: 'public',
name: 'monthly_revenue',
type: 'View',
rows: 0,
size: '0.0 MB',
status: 'Idle',
},
{
id: 'tbl-sessions',
schema: 'auth',
name: 'sessions',
type: 'Table',
rows: 52,
size: '0.4 MB',
status: 'Active',
},
{
id: 'tbl-audit',
schema: 'auth',
name: 'audit_log',
type: 'Table',
rows: 12301,
size: '45.0 MB',
status: 'Locked',
},
{
id: 'tbl-identities',
schema: 'auth',
name: 'identities',
type: 'Table',
rows: 104,
size: '0.8 MB',
status: 'Active',
},
{
id: 'tbl-objects',
schema: 'storage',
name: 'objects',
type: 'Table',
rows: 4019,
size: '12.5 MB',
status: 'Idle',
},
{
id: 'tbl-buckets',
schema: 'storage',
name: 'buckets',
type: 'Table',
rows: 4,
size: '0.1 MB',
status: 'Idle',
},
];
const busyRows: DbRow[] = [
...demoRows,
...Array.from({ length: 24 }, (_, index): DbRow => ({
id: `busy-${index + 1}`,
schema: SCHEMAS[index % SCHEMAS.length],
name: `extra_${String(index + 1).padStart(2, '0')}`,
type: index % 5 === 0 ? 'View' : 'Table',
rows: 20 + ((index * 37) % 9000),
size: `${(0.1 + (index % 9) * 0.4).toFixed(1)} MB`,
status: STATUSES[index % STATUSES.length],
})),
];
const rows = pack === 'empty' ? [] : pack === 'busy' ? busyRows : demoRows;
const connections = pack === 'empty' ? 0 : pack === 'busy' ? 12 : 4;
const statusLabel: Record {selected}`timeSlotWidth: "250px" ,`
→ Jeder Wochentag wird mit **250 Pixeln Breite** angezeigt.
📌 **Hinweis:**
- Der Standardwert liegt bei ca. **100 px**, kann aber je nach Layout-Anforderungen angepasst werden.
- Eine höhere Zahl sorgt für **mehr Platz pro Tag**, besonders hilfreich bei vielen Einträgen.
### timeSlotHeight – Höhe der Stundenzeilen festlegen
Mit dem Parameter `**timeSlotHeight**` bestimmst du, **wie hoch jede einzelne Stunde im Kalender mindestens dargestellt wird**.
`timeSlotHeight: "80px",`
→ Jede Stunde erhält mindestens eine **Höhe von 80 Pixeln** in der vertikalen Zeitachse.
📌 **Hinweis:**
- Der Standardwert liegt bei **50 px**.
- Ein höherer Wert sorgt für **mehr Raum pro Stunde.**
- Ist der Kalender höher als die Stunden bei dieser Mindesthöhe brauchen, werden die Zeilen **automatisch gestreckt** und füllen die Höhe aus.
### allDayHeight – Höhe der Zeile für Ganztagseinträge
Mit dem Parameter `allDayHeight` legst du fest, wie hoch die obere Zeile für Ganztagseinträge dargestellt wird.
→ Die Zeile für Ganztagseinträge wird mit 60 Pixeln Höhe angezeigt.
📌 Hinweis:
- Standardmäßig liegt die Höhe bei ca. 40 px.
- Ein höherer Wert schafft mehr Platz für mehrere Ganztagseinträge oder größere Textdarstellungen.
### styles – individuelles Styling für deinen Kalender
Mit `**styles**` kannst du das **Aussehen deines Kalenders anpassen** – hier kannst du aktuell die Hintergrundfarbe deines Kalenders individualisieren.
### createNew – neue Termine durch Ziehen erstellen
Mit dem Parameter `**createNew**` aktivierst du die Möglichkeit, **neue Termine direkt durch Aufziehen eines Zeitbereichs im Kalender zu erstellen**.
🖱️ **So funktioniert’s:**
- Du klickst in einen freien Bereich des Kalenders, **ziehst mit gedrückter Maustaste**, und lässt wieder los.
- Dabei wird automatisch ein neuer Termin erstellt – inklusive **Startzeit und Dauer**.
📌 **Hinweis:**
- Alle Feldbezüge (`dateFrom`, `dateTo`, `timeFrom`, `timeTo`) müssen mit den **Field IDs** der entsprechenden Felder aus deiner **Kalendereintrags-Tabelle (**`**tableId**`**)** übereinstimmen.
- Du kannst die Field IDs ganz einfach mit der Ninox-Funktion `fieldId()` herausfinden und direkt in die Konfiguration übernehmen.
**💡 **Tipp: Ideal für eine schnelle und intuitive Terminplanung – z. B. bei Schichten, Buchungen oder Raumvergabe.
Optional kannst du mit **`changeFieldValues`** Zusatzfelder im neuen Datensatz setzen – z. B. Cache- oder Trigger-Felder:
```javascript
createNew: {
tableId: "B",
dateFrom: fieldId(Nr, "Datum von"),
timeFrom: fieldId(Nr, "Von"),
dateTo: fieldId(Nr, "Datum bis"),
timeTo: fieldId(Nr, "Bis"),
changeFieldValues: [{
fieldId: fieldId(Nr, "CacheTrigger"),
value: now()
}]
}
```
Optional **`dataBag`**: nach erfolgreichem Anlegen denselben JSON-State-Topf patchen wie bei Button-Create. Läuft nur, wenn der Datensatz wirklich angelegt wurde. `##newRecordId##` wird durch die ID des neuen Termins ersetzt. Typisch: Auswahl leeren, damit der Ghost verschwindet.
```javascript
createNew: {
tableId: "B",
dateFrom: fieldId(Nr, "Datum von"),
timeFrom: fieldId(Nr, "Von"),
dateTo: fieldId(Nr, "Datum bis"),
timeTo: fieldId(Nr, "Bis"),
item: if Auswahl then {
/* Ghost */
},
dataBag: {
recordId: Nr,
fieldId: fieldId(Nr, "helper_uiState"),
base: helper_uiState,
patch: { auswahlId: null }
}
}
```
`null` im Patch löscht den Key. Die neue Termin-ID mit ablegen: `patch: { auswahlId: null, lastTerminId: "##newRecordId##" }`. Syntax: [`data-bag-actions`](../patterns/data-bag-actions.md).
📌 **Persistenz / React:** Create läuft über `{ type: "create", tableId, changeFieldValues, setNewRecordId?, dataBag?, popup?, fullscreen? }`. React: dieselbe Action über `onAction` verarbeiten.
### createNew.item – vorgebauten Termin per Klick setzen
Mit **`createNew.item`** legst du einen **fertigen Termin** in den Kalender, statt einen Zeitraum aufzuziehen. Typischer Fall: Auswahl in einer Seitenleiste (Typ, Dauer, Phasen) schreibt ins Formelfeld — der Kalender zeigt einen **Ghost**, der der Maus folgt. Ein Klick setzt ihn.
Ohne `item` bleibt das heutige Aufziehen. Mit `item` (und gesetzter `duration` oder Segment-Dauern) ist Aufziehen aus — außer **`active`** ist `false`.
Den Platzieren-Modus schaltest du **außerhalb** des Kalenders: ein Ja/Nein-Feld, eine Checkbox in der Seitenleiste oder die Auswahl selbst.
```javascript
createNew: {
tableId: "B",
dateFrom: fieldId(first(Termine), "Datum von"),
timeFrom: fieldId(first(Termine), "Von"),
dateTo: fieldId(first(Termine), "Datum bis"),
timeTo: fieldId(first(Termine), "Bis"),
changeFieldValues: if Auswahl then [{
fieldId: fieldId(first(Termine), "Typ"),
value: number(Auswahl)
}] else [{
fieldId: fieldId(first(Termine), "CacheTrigger"),
value: now()
}],
item: if Auswahl then {
active: Platzieren,
title: Auswahl.Name,
styles: {
backgroundColor: color(Auswahl),
color: "#fff",
borderRadius: "8px"
},
segments: [{
title: "Anfahrt",
linked: false,
offset: { days: -1 },
timeFrom: time(16, 0),
timeTo: time(17, 0)
}, {
title: "Kernarbeit",
timeFrom: time(11, 0),
timeTo: time(14, 0),
splits: [{
title: "Pause",
timeFrom: time(12, 30),
timeTo: time(13, 0)
}]
}, {
title: "Abfahrt",
linked: false,
offset: { days: 1 },
timeFrom: time(8, 0),
timeTo: time(9, 0)
}]
}
}
```
**Felder in `item`:**
- **`active`** — optional. `true` oder weggelassen: Ghost an, Klick setzt. `false` / leer: Ghost aus, Aufziehen wieder an. Bindet an ein Feld außerhalb des Widgets (z. B. `Platzieren`).
- **`duration`** — relative Länge. Reine Duration-Phasen hängen sich an die vorherige Follow-Phase (erste: an die Maus).
- **`timeFrom` / `timeTo`** — Uhr auf dem Anker-Tag (Tag unter der Maus). Pinnt **diese** Phase. Mit `linked: false` bleibt sie Satellit — der Follow-Strang folgt weiter der Maus. Ohne `linked: false` hängt die Kette an dieser Uhr.
- **`linked`** — wie bei gespeicherten Segmenten. `false` = Satellit (Anfahrt/Abfahrt). Standard `true`.
- **`offset`** — `{ days, time }`, beide optional und signed. `days: -1` Vortag, `days: 1` Folgetag. `time: -time(1, 0)` eine Stunde früher (Maus / vorige Phase). Kombinierbar: `{ days: -1, time: -time(1, 0) }`. Mit fester Uhr wählt `days` den Kalendertag; `time` schiebt die Uhr mit. `days: 0` hält die Phase auf dem Anker-Tag, wenn die Phase davor auf einem anderen Tag lag. Nicht verwechseln mit dem flachen `offset: time(…)` in **`splits`**.
- **`dateFrom` / `dateTo`** — echte Datumswerte (wie bei `timeEntries`), wenn die Phase fest auf einem Kalendertag liegt.
- **`splits`** — eine Ebene Unterbrechungen in dieser Phase (typisch Pause). Beide Kern-Stücke behalten den Phasen-Titel. Siehe unten.
- **`title` / `subtitle` / `value` / `customLayout` / `styles` / `insetBorder`** — wie ein `timeEntry`.
Auflösung pro Phase, erste passende Regel: festes Datum + Uhr → Uhr (`timeFrom`/`timeTo`, optional `offset.days` / `offset.time`) → `offset` + `duration` → nur `duration`. `linked: false` + Uhr aktualisiert den Follow-Cursor nicht.
**Umschalttaste** beim Ghost: ganze Kette inkl. Satelliten mitverschieben. Loslassen: Satelliten zurück auf ihre Uhr. Klick speichert das, was du siehst.
Kern unter die Maus, Satellit mit fester Uhr:
```javascript
segments: [{
title: "Anfahrt",
linked: false,
offset: { days: -1 },
timeFrom: time(16, 0),
timeTo: time(17, 0)
}, {
title: "Vorarbeit",
offset: { days: 0 },
duration: time(0, 30)
}, {
title: "Kern",
duration: time(3, 0)
}, {
title: "Abfahrt",
offset: { days: 1 },
duration: time(1, 0)
}]
```
Ohne feste Uhren (folgt der Maus):
```javascript
segments: [{
title: "Anfahrt",
offset: { days: -1, time: -time(1, 0) },
duration: time(1, 0)
}, {
title: "Kernarbeit",
offset: { days: 0 },
duration: time(3, 0),
splits: [{
title: "Pause",
offset: time(1, 30),
duration: time(0, 30)
}]
}, {
title: "Abfahrt",
offset: { days: 1 },
duration: time(1, 0)
}]
```
Sollen die Stücke **anders heißen** (Teil 1 / Pause / Teil 2): drei `segments` mit `duration`, kein `splits`.
Der Ghost darf über mehrere Tage gehen. Pro Phase läuft die Uhrzeit mit (`##entrytime##`).
Der Ghost nimmt eine **eigene Spur** ein — bestehende Termine zur gleichen Zeit rücken zur Seite, wie bei zwei echten Einträgen. Maus weg: die anderen gehen wieder auf volle Breite.
Ausblenden: **Auge** in der Toolbar oder **Esc**. Wieder einblenden nur über das Auge. Die Seitenleisten-Auswahl bleibt.
Gespeichert wird **ein** neuer Datensatz auf dieselben vier `createNew`-Felder: Von der ersten Phase bis Bis der letzten. `changeFieldValues` und `dataBag` wie bei Aufziehen.
Leere Auswahl: `item` weglassen (kein `else ""`). Dann gilt wieder Aufziehen.
## timeEntries – deine Kalendereinträge im Detail
Der `timeEntries`-Block legt fest, welche konkreten Zeiteinträge im Kalender angezeigt werden.
Hier bestimmst du, wie jeder einzelne Eintrag dargestellt wird – von Titel über Farbe bis hin zu eigenen Layouts. So kannst du deine Kalenderkarten ganz individuell gestalten und an deinen Anwendungsfall anpassen. Dabei wird automatisch ein neuer Termin erstellt – inklusive Startzeit und Dauer.
📌 **Hinweis:**
Für die Darstellung der **Echtzeit-Veränderung** der Termindaten (beim Erstellen oder auch beim Verschieben eines Termins) kann der Platzhalter **"##entrytime##"** überall in `timeEntries` eingesetzt werden, wo die **Terminzeit** angezeigt werden soll, also im title, subtitle, value, aber auch in allen Widgets, die hier eingesetzt werden. Wie zB. wenn ein `customLayout` für das Design des timeEntries genutzt wird oder in Buttons, Inputs usw.
Zum Beispiel innerhalb eines Layout-Blocks im Text Widget:
```javascript
...
value: arcCustomText({
alignY: "top",
fontSize: typo.paragraph3.fontSize,
fontWeight: "400",
fontColor: appointmentType.colorShade,
value: "##entrytime##"
}),
...
```
➡️ Alle Parameter für die timeEntries findest du im **folgenden Abschnitt**.
### title – Titel des Kalendereintrags
Mit dem Parameter `title` legst du fest, was in der ersten Zeile des Kalendereintrags angezeigt wird.
🔧 Zwei Optionen:
- Eigener Titel:
Gib ein Feld oder einen Text an, der als Titel im Eintrag angezeigt werden soll.
- Keine Angabe (`"`):
Lässt du das Feld leer (`"`), wird stattdessen die Uhrzeit des Termins angezeigt.
styles: {
backgroundColor: "rgba(0,0,255,0.01)"
},
📌 Hinweis:
- Wenn du keinen Titel vergibst und stattdessen die Uhrzeit angezeigt wird, wird diese Uhrzeit bei Änderungen live aktualisiert, z. B. beim Ziehen oder Verlängern eines Eintrags.
- **Leerer Wert:** Wenn `title` leer oder nur Leerzeichen enthält, wird die Titel-Zeile ausgeblendet – der Platz geht an `subtitle` oder `##entrytime##`.
### subtitle – Beschreibung unter dem Titel
Mit dem Parameter `subtitle` kannst du eine zusätzliche Zeile unter dem Titel im Kalendereintrag anzeigen lassen.
Typischerweise wird hier eine kurze Beschreibung oder ein ergänzender Hinweis wie z. B. der Grund des Termins oder ein Status hinterlegt.
title: Kalendertitel, // Ninox Feld
title: "", // Default: Gibt die Uhrzeit des Kalendereintrages an
→ Zeigt z. B. den Inhalt des Feldes *„Description“* unter dem Titel im Kalendereintrag an.
📌 **Hinweis:**
- Der `subtitle` ist optional und wird **nur angezeigt, wenn ein entsprechender Feldwert vorhanden ist**.
- **Leerer Wert:** Wenn `subtitle` leer oder nur Leerzeichen enthält, wird die Subtitle-Zeile ausgeblendet – der Platz geht an den Titel. Titel und Subtitle werden symmetrisch behandelt: Wenn beide leer sind, wird nur `##entrytime##` angezeigt.
### value – Zusatzinformationen oder interaktive Felder
Der Parameter `value` steht für die dritte Zeile deines Kalendereintrags. Hier kannst du weitere Informationen anzeigen oder sogar interaktive Elemente wie Eingabefelder oder Auswahllisten integrieren.
🔧 Einsatzmöglichkeiten:
- Zusätzliche Informationen wie z. B. Ort, Status, Verantwortlicher
- Interaktive Widgets direkt im Kalendereintrag – z. B.:subtitle: Description, // Ninox Feld
subtitle: "" // Textfeld
Ein Select-Feld, um den zuständigen Mitarbeiter auszuwählen - Ein Input-Feld für kurze Notizen oder Rückmeldungen
value: "",
value: Kommentar, → Zeigt den Inhalt des Feldes *„Kommentar“* in der dritten Zeile an.
📌 **Hinweis:**
- `value` ist **optional**, erweitert aber die Darstellung deiner Einträge um eine zusätzliche Ebene.
- In Verbindung mit eingebetteten Widgets entsteht ein **interaktives Kalendererlebnis direkt im Eintrag**.
💡 **Tipp: **Nutze `value`, um wichtige Zusatzinfos sofort sichtbar zu machen – oder um deine Nutzer direkt im Kalender arbeiten zu lassen.
### customLayout – vollständiges individuelles Layout für Kalendereinträge
Mit dem Parameter `**customLayout**` kannst du ein **komplett eigenes Layout** für deine Kalendereinträge definieren.
Wenn du diesen Parameter nutzt, werden die Standardfelder `**title**`, `**subtitle**` und `**value**` **nicht mehr angezeigt**. Stattdessen kannst du frei gestalten, wie dein Kalendereintrag aussieht – mit beliebigen Widgets.
`customLayout: "",`💡 Tipp: Nutze `customLayout`, wenn du eine komplett individuelle Darstellung brauchst, z. B. mit eigenen Farben, Icons, Struktur oder logischen Bedingungen. ### dateFrom, dateTo, timeFrom, timeTo – Start- und Endzeit je Eintrag Mit diesen vier Parametern gibst du an, **welche Felder aus deinem Zeiteintrags-Datensatz** für **Datum und Uhrzeit** verwendet werden sollen. ```javascript dateFrom: 'Date From', dateTo: 'Date To', timeFrom: 'Time From', timeTo: 'Time To', ```  📌 Hinweis: - Es müssen echte Datums- und Zeitfelder aus Ninox angegeben werden. - Die Kombination aus Datum + Uhrzeit bestimmt die Position und Länge des Kalendereintrags. ⚠️ Wichtig zu wissen: - Fehlt `dateTo`, wird automatisch der Tag aus `dateFrom` verwendet (und umgekehrt). - Fehlen beide Datumsfelder, wird der Eintrag nicht angezeigt. - Fehlt `timeTo`, wird automatisch `timeFrom + 5 Minuten` verwendet. - Fehlt `timeFrom`, wird `timeTo - 5 Minuten` angenommen. → So ist eine minimale Anzeige trotzdem möglich. 💡 Tipp: Wenn du nur Tages-Einträge ohne genaue Uhrzeiten darstellen willst, kannst du auch nur mit Datumsfeldern arbeiten – der Kalender stellt den Eintrag dann als Ganztagstermin dar. ### styles – individuelles Styling für Kalendereinträge Mit dem Parameter `**styles**` kannst du das **Aussehen einzelner Kalendereinträge** anpassen – z. B. Farben, Innenabstand (Padding) oder abgerundete Ecken (Border-Radius).
styles: {
backgroundColor: color(Person),
color: "#fff",
borderRadius: "8px",
padding: ""
},📌 **Unterstützte Eigenschaften:**
- `backgroundColor`: Hintergrundfarbe der Karte
- `color`: Schriftfarbe
- `padding`: Innenabstand (z. B. `"6px"`)
- `borderRadius`: Abrundung der Ecken (z. B. `"8px"`)
💡 Tipp: Mit `styles` kannst du deine Einträge **visuell voneinander abheben**, etwa nach Status, Typ oder Priorität – auch dynamisch, wenn du Felder deiner Datenquelle als Wert verwendest.
### dragAction – Felder für das Verschieben von Einträgen festlegen
Mit dem Parameter `**dragAction**` definierst du, **welche Datums- und Zeitfelder beim Ziehen oder Verschieben eines Eintrags aktualisiert werden sollen**.
So wird sichergestellt, dass der neue Zeitpunkt im **Datensatz gespeichert** wird, wenn ein Eintrag im Kalender bewegt wird.
```javascript
dragAction: {
recordId: Nr,
dateFrom: fieldId(Nr, "Datum von"),
timeFrom: fieldId(Nr, "Von"),
dateTo: fieldId(Nr, "Datum bis"),
timeTo: fieldId(Nr, "Bis"),
changeFieldValues: [{
fieldId: fieldId(Nr, "CacheTrigger"),
value: now()
}]
}
```
📌 Hinweis:
- Gib hier die korrekten Feld-IDs der einzelnen Felder in der Zeiteintrags-Tabelle an.
- Du kannst hier die Ninox Funktion `fieldId()` benutzen.
- **changeFieldValues**: Optional. Array von `{ fieldId, value }` – wird **nach** einem erfolgreichen Drag geschrieben (Datum/Zeit haben sich geändert). Typischer Einsatz: Cache- oder Trigger-Felder aktualisieren. Aufbau wie bei `createNew.changeFieldValues` und wie in der Timeline.
⚠️ Wichtig: `recordId` und die Datums-/Zeitfelder sind zwingend erforderlich, wenn du möchtest, dass Einträge per Drag & Drop verschoben oder in ihrer Dauer verändert werden können.
📌 **Persistenz / React:** Beim Speichern nach Drag sendet das Widget Action-Performer-Actions `{ type: "update", recordId, field, value }` (ein Action pro geändertes Feld). In Ninox mappt der Adapter das auf `database.update`. Im React-Adapter landet das in `onAction` — ohne globales `database`.
### clickAction – Einträge per Klick in Ninox öffnen
Mit dem Parameter `**clickAction**` ermöglichst du es, dass der Nutzer durch einen Klick auf einen Kalendereintrag direkt den **zugehörigen Datensatz in Ninox öffnen** kann.
clickAction: {
type: "popup",
recordId: Nr
}📌 Bedeutung der Parameter:
- `type: "popup"` – Öffnet den Datensatz in einem Popup-Fenster (weitere Typen können ggf. folgen).
- `recordId`: Das Feld im Kalendereintrag, das den Primärschlüssel (Record ID) enthält – meist `"Nr"`.
⚠️ Wichtig:
- Das angegebene Feld bei `recordId` muss die echte Ninox-Datensatz-ID enthalten.
- Wird kein gültiger Wert gefunden, passiert beim Klick nichts.
### insetBorder – vertikale Inset-Linie im Kalendereintrag
Mit `**insetBorder**` kannst du eine **dezente vertikale Linie** am linken (oder rechten) Rand jedes Kalendereintrags **mit Uhrzeit** anzeigen lassen – ähnlich dem Stil des Apple Kalenders. Ganztägige Einträge zeigen sie nicht. Das Feature ist **standardmäßig aktiv** und muss nur deaktiviert werden, wenn du es nicht möchtest.
```javascript
// On data: applies to all timed entries (hide everywhere with enabled: false)
insetBorder: { enabled: false }
// On a timeEntry or segment: applies to that entry only
insetBorder: {
enabled: true, // default: true; false hides the line
position: 'left', // 'left' (default) or 'right'
width: '2px', // line width, default: 3px
stroke: 'dotted', // 'solid' (default), 'dashed' or 'dotted'
color: '#ffffff' // default: the entry's styles.color, then #18181b
}
```
📌 **Hinweis:**
- Setze `insetBorder` in den Widget-`data`, um alle Einträge mit Uhrzeit auf einmal zu ändern, z. B. `{ enabled: false }`, um die Linie überall auszublenden.
- Auf einem `timeEntry` oder Segment gesetzt, gilt es nur dort. Dort gesetzte Felder haben Vorrang vor der Widget-Ebene; nicht gesetzte Felder übernehmen deren Wert.
- Setzen Eintrag und eines seiner Segmente beide `insetBorder`, gilt für dieses Segment der Wert des Segments.
- Die Linie reagiert automatisch auf den `borderRadius` des Eintrags und wird kürzer, je höher der Radius ist.
- Bei zusammenhängenden Segmenten (Kette) wird sie an Übergängen nahtlos fortgesetzt.
**Stücke der Linie (`segments`)**
Die Linie lässt sich in Stücke teilen, zum Beispiel eine Farbe pro Person. Das ist nicht dasselbe wie die `segments` eines Eintrags (seine Phasen): Hier steht `segments` innerhalb von `insetBorder` und teilt nur die Linie.
```javascript
insetBorder: {
color: '#18181b', // Standard der Stücke
align: 'start', // 'start' (Standard), 'center' oder 'end': wo Stücke ohne Zeit sitzen, wenn sie die Linie nicht füllen
segments: [
// Mit Zeit sitzt das Stück dort, wo die Zeit liegt (Uhrzeiten am ersten Tag des Eintrags):
{ color: '#dc2626', label: 'Anna', timeFrom: 9 * 3600000, timeTo: 10.5 * 3600000 },
// oder relativ zum Start des Eintrags; es wandert mit dem Eintrag:
{ color: '#2563eb', label: 'Ben', offset: 60 * 60000, duration: 90 * 60000 },
// Ohne Zeit teilen sich die Stücke die Linie nach size:
// { color: '#16a34a', label: 'Clara', size: '25%' } // 'fraction' / 'auto' (Standard), '24px', '25%'
]
}
```
- `label` erscheint als Tooltip und wird von Screenreadern vorgelesen.
- Stücke mit Zeit dürfen sich überlappen: Wo zwei gleichzeitig aktiv sind, werden sie nebeneinander gezeichnet (bis zu drei Spuren) und der Text rückt ein. Zeit, die kein Stück abdeckt, bleibt leer.
- Hat irgendein Stück eine Zeit, werden Stücke ohne Zeit nicht gezeichnet. Ohne jede Zeit werden die Stücke nach `size` verteilt; sind die festen Größen länger als die Linie, schrumpfen alle im gleichen Verhältnis.
- `offset` zählt ab dem Start des angezeigten Stücks des Eintrags. Bei einem Eintrag, der durch `splits` geteilt ist, zählt jedes Stück ab seinem eigenen Start.
- Jedes Stück kann eigene `color` und `stroke` setzen; nicht gesetzte Felder übernehmen die der Linie.
### segments – Termine in Phasen unterteilen
Mit dem Parameter `**segments**` kannst du einen Termin in mehrere **zusammengehörige Zeitphasen** aufteilen – z. B. Anfahrt, Vorarbeit, Kernarbeit, Pause, Nacharbeit und Abfahrt.
Jedes Segment ist ein eigenständiger Eintrag mit eigener Zeit und eigenem Design, kann aber mit anderen Segmenten **verknüpft** werden, sodass sie beim Verschieben gemeinsam bewegt werden.
```javascript
timeEntries: (select Termine).[{
title: Bezeichnung,
dateFrom: 'Datum von',
dateTo: 'Datum bis',
timeFrom: Von,
timeTo: Bis,
styles: {
backgroundColor: "#5b6af0",
color: "#fff",
borderRadius: "8px"
},
dragAction: {
recordId: Nr,
dateFrom: fieldId(Nr, "Datum von"),
timeFrom: fieldId(Nr, "Von"),
dateTo: fieldId(Nr, "Datum bis"),
timeTo: fieldId(Nr, "Bis")
},
segmentSettings: {
dayJump: {
threshold: time(22, 0),
startTime: time(7, 0)
}
},
segments: [{
title: "Anfahrt",
dateFrom: 'Seg Datum von',
dateTo: 'Seg Datum bis',
timeFrom: SegVon,
timeTo: SegBis,
styles: {
backgroundColor: "#3b4fd0",
color: "#fff"
},
linked: false,
segmentType: "travel",
dragAction: {
recordId: Nr,
dateFrom: fieldId(Nr, "Seg Datum von"),
timeFrom: fieldId(Nr, "SegVon"),
dateTo: fieldId(Nr, "Seg Datum bis"),
timeTo: fieldId(Nr, "SegBis")
}
}, {
title: "Kernarbeit",
dateFrom: 'Kern Datum von',
dateTo: 'Kern Datum bis',
timeFrom: KernVon,
timeTo: KernBis,
styles: {
backgroundColor: "#5b6af0",
color: "#fff"
},
linked: true
}]
}]
```
#### Felder je Segment
| Parameter | Typ | Beschreibung |
|---|---|---|
| `title` | Text | Titel des Segments |
| `subtitle` | Text | Untertitel (optional) |
| `value` | Text/Widget | Zusatzzeile oder eingebettetes Widget |
| `dateFrom`, `dateTo` | Datum | Start- und Enddatum |
| `timeFrom`, `timeTo` | Zeit | Start- und Endzeit |
| `styles` | Objekt | Wie beim `timeEntry` – überschreibt Parent-Styles |
| `linked` | Boolean | `true` = Segment wird mit anderen `linked`-Segmenten gemeinsam verschoben (Standard: `true`) |
| `segmentType` | Text | Frei wählbarer Typ-Bezeichner (z. B. `"travel"`, `"core"`) |
| `insetBorder` | Objekt | Inset-Linie für dieses Segment (überschreibt Entry-Level) |
| `dragAction` | Objekt | Felder für Drag & Drop – Aufbau identisch mit `timeEntry.dragAction` |
| `segmentSettings` | Objekt | Segment-Einstellungen für dieses einzelne Segment (z. B. `dayJump`) |
| `splits` | Array | Eine Ebene Unterbrechungen in dieser Phase (z. B. Pause). Das Widget macht daraus: Teil 1 → Pause → Teil 2. |
💡 **Tipp:** Felder die nicht im Segment gesetzt werden, erben den Wert des übergeordneten `timeEntry`. So reicht es, nur die Unterschiede pro Segment zu definieren.
#### splits – Pause in einer Phase
`splits` teilt **eine** Phase (meist Kernarbeit), ohne dass du in Ninox „Kern Teil 1 / Kern Teil 2“ anlegen musst. Keine weitere Verschachtelung — nur diese eine Ebene.
```javascript
{
title: "Kernarbeit",
dateFrom: 'Datum von',
dateTo: 'Datum bis',
timeFrom: time(11, 0),
timeTo: time(14, 0),
splits: [{
title: "Pause",
timeFrom: time(12, 30),
timeTo: time(13, 0),
styles: {
backgroundColor: "#94a3b8"
}
}]
}
```
Oder relativ zum Phasen-Start: `offset: time(1, 30)`, `duration: time(0, 30)`.
Mehrere Splits sind erlaubt; sie werden nach Startzeit sortiert und auf den Phasen-Zeitraum begrenzt. Kern-Stücke behalten Look und Titel der Phase, die Pause ihren eigenen.
Unterschiedliche Titel für die Kern-Stücke (Teil 1 / Teil 2): **drei `segments`**, kein `splits` — z. B. `duration: time(1, 30)`, Pause, `duration: time(1, 30)`.
#### linked – Segmente verknüpfen oder entkoppeln
Mit `linked: true` (Standard) wird ein Segment Teil der **verbundenen Kette**. Beim Verschieben im Kalender bewegen sich alle verknüpften Segmente gemeinsam.
Mit `linked: false` wird ein Segment **entkoppelt** – z. B. Anfahrt und Abfahrt, die auf einem anderen Tag liegen können und unabhängig verschoben werden sollen. Im **Placement-Ghost** (`createNew.item`) gilt dasselbe: eine Uhr auf einem `linked: false`-Satelliten nagelt den Kern nicht fest.
#### Toolbar
Unten rechts sitzt eine **kleine Toolbar**. Sie ist **immer sichtbar**, außer `segmentSettings.toolbar` ist `false`.
**Schloss** — Segmente gemeinsam oder einzeln verschieben:
- **Gesperrt** (Schloss zu): Alle Segmente des `timeEntry` werden gemeinsam verschoben
- **Einzeln** (Schloss offen): `linked`-Segmente bilden eine Gruppe, `linked: false`-Segmente können unabhängig verschoben werden
Ohne Segmente ist das Schloss inaktiv. Hover zeigt einen kurzen Hinweis.
**Auge** — nur bei `createNew.item`: Ghost ein- und ausblenden. **Esc** blendet aus. Wieder einblenden nur über das Auge. Ausgeblendet gilt wieder Aufziehen, ohne die Auswahl in der Seitenleiste zu ändern.
💡 **Shift-Taste:** Während eines Drags oder beim **Placement-Ghost** hältst du **Shift**, um den Lock umzukehren: gespeicherte Satelliten bzw. Ghost-Satelliten wandern mit. Loslassen setzt Ghost-Satelliten auf ihre Uhr zurück. Das Schloss-Icon zeigt den Zustand live.
💡 **Shift + Resize:** Wenn du die Shift-Taste hältst und an einem Segment-Rand ziehst (oben/unten), verschieben sich alle angrenzenden Segmente mit – der **Gesamtblock wird größer**, statt dem Folge-Segment Zeit wegzunehmen.
## segmentSettings – Globale Einstellungen für Segmente
Der optionale Parameter `**segmentSettings**` auf Ebene des gesamten Widgets steuert das **Standardverhalten aller Segmente** – z. B. ob Gruppen standardmäßig gesperrt sind oder ob die Toolbar angezeigt wird.
```javascript
arcCustomCalendarWeek({
// ...
segmentSettings: {
lockGroups: true,
edgeMagnetism: true,
toolbar: true,
dayJump: {
threshold: time(22, 0),
startTime: time(7, 0)
}
},
timeEntries: [...]
})
```
📌 **Parameter im Überblick:**
| Parameter | Typ | Beschreibung |
|---|---|---|
| `lockGroups` | Boolean | `true` = Segmente werden standardmäßig gemeinsam verschoben (Standard: `true`) |
| `edgeMagnetism` | Boolean | `true` = Beim Resize bleibt die gemeinsame Grenze angrenzender Segmente synchron (Standard: `true`) |
| `toolbar` | Boolean | `false` = Toolbar ausblenden. Sonst immer sichtbar (Schloss + Auge bei `createNew.item`) |
| `dayJump.threshold` | Zeit | Ab dieser Uhrzeit springt ein Segment beim Drag auf den nächsten Tag |
| `dayJump.startTime` | Zeit | Startzeit am neuen Tag nach dem Sprung |
⚠️ **Priorität:** Einstellungen auf `timeEntry`-Ebene (`segmentSettings` innerhalb eines Eintrags) überschreiben die globalen `segmentSettings`. Einstellungen auf Segment-Ebene überschreiben wiederum die Entry-Ebene.
---
Mit dem Custom Calendar Week bringst du strukturierte Planung und volle Flexibilität in deine Ninox-Datenbank – individuell anpassbar, intuitiv bedienbar und bereit für deinen Alltag.
Happy Widgeting 🥳
---
# Custom Calendar Grid
Source: https://www.arc-rider.com/docs/widgets/custom-calendar-grid
# CalendarGrid in React
Termine in einer Monatsübersicht anzeigen und Tage oder Einträge auswählen.
## AI Defaults (read first)
- Kopiere `examples/widgets/calendar_grid/calendar_grid-00-defaults.tsx` aus dem npm-Paket.
- Daten sind Epoch-Millisekunden des Tages um 00:00 UTC (`Date.UTC(2027, 0, 4)`), wie im Beispiel. `dateTo` ist **inklusive**. Gleiches `dateFrom` und `dateTo` = ein Tag. ISO-Strings werden nicht akzeptiert.
- Timed-Chip: `timeFrom` und `timeTo` als Millisekunden ab Mitternacht (`9 * 3600000` = 09:00). Ganztagsbalken: **beide Zeiten weglassen**. Dieselben Zeiten über mehrere Tage zeichnen einen Balken über diese Tage, bündig mit den Tagen; die Endzeit steht rechts. Bricht er in die nächste Woche um, zeigt die erste Zeile die Startzeit am rechten Rand des Starttags, die letzte Zeile wiederholt den Titel und zeigt die Endzeit.
- `dateSettings.month` ist 1–12. Den Monat nicht unter `weekdaySettings` setzen.
- `timeZoneBalance` verschiebt nur die Uhrzeit-Labels der Chips um ganze Stunden; `0` lassen, außer die Zeiten liegen in einer anderen Zone.
- `onEntryClick`, `onDayClick` und `onEntryDragSave` verdrahten. Einträge sind nur mit `draggable: true` verschiebbar. Dasselbe Flag erlaubt es, den linken oder rechten Rand eines Eintrags zu ziehen, um ihn zu kürzen oder zu verlängern. Persistiere **`endData`** aus `onEntryDragSave(entryIndex, startData, endData)` — nicht `startData` (sonst Snap-back).
- `title` ist ein String. JSX gehört in `customLayout`. Chip-Farbe: `styles.backgroundColor` und `styles.color` (`fontColor` fällt auf `color`).
Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components).
## Vollständiges Beispiel
```tsx
import { useState } from "react";
import { ArcWidgetCalendarGrid } from "@arcrider/arcwidgets-react";
import type {
ArcWidgetCalendarGridDailyEntryDragSavePayload,
ArcWidgetCalendarGridEntryDragSavePayload,
ArcWidgetCalendarGridTimeEntry,
} from "@arcrider/arcwidgets-react";
const day = (date: number) => Date.UTC(2027, 0, date);
const at = (hours: number, minutes = 0) => (hours * 60 + minutes) * 60000;
const initialEntries: ArcWidgetCalendarGridTimeEntry[] = [
{
title: "Team Offsite",
dateFrom: day(4),
dateTo: day(5),
draggable: true,
},
{
title: "Public Holiday",
dateFrom: day(6),
dateTo: day(6),
draggable: true,
},
{
title: "Team Meeting",
dateFrom: day(4),
dateTo: day(4),
timeFrom: at(9),
timeTo: at(10, 30),
draggable: true,
},
{
title: "Code Review",
dateFrom: day(5),
dateTo: day(5),
timeFrom: at(10),
timeTo: at(11),
draggable: true,
},
{
title: "Workshop",
subtitle: "Room 2",
dateFrom: day(5),
dateTo: day(5),
timeFrom: at(13, 30),
timeTo: at(17),
draggable: true,
},
{
title: "Lunch",
dateFrom: day(5),
dateTo: day(5),
timeFrom: at(12),
timeTo: at(13),
draggable: true,
},
{
title: "Retro",
dateFrom: day(5),
dateTo: day(5),
timeFrom: at(17),
timeTo: at(18),
draggable: true,
},
{
title: "Sprint Planning",
dateFrom: day(8),
dateTo: day(8),
timeFrom: at(9),
timeTo: at(12),
draggable: true,
},
// Timed, several days in one week. The bar lines up with Thursday–Saturday.
// 15:00 is written at the right edge.
{
title: "Conference",
dateFrom: day(7),
dateTo: day(9),
timeFrom: at(11),
timeTo: at(15),
draggable: true,
},
// Continues onto the next week row: that row repeats the title, and 11:00
// is written at the right edge of the last piece only.
{
title: "Client Workshop",
dateFrom: day(8),
dateTo: day(11),
timeFrom: at(16),
timeTo: at(11),
draggable: true,
},
{
title: "Stand-up",
dateFrom: day(18),
dateTo: day(18),
timeFrom: at(9),
timeTo: at(9, 30),
draggable: true,
},
{
title: "Design Review",
dateFrom: day(20),
dateTo: day(20),
timeFrom: at(14),
timeTo: at(16),
draggable: true,
},
{
title: "All-Hands",
dateFrom: day(22),
dateTo: day(22),
timeFrom: at(10),
timeTo: at(11),
draggable: true,
},
{
title: "Office Closed",
dateFrom: day(29),
dateTo: day(29),
draggable: true,
},
{
title: "Release",
subtitle: "v2.1",
dateFrom: day(18),
dateTo: day(22),
timeFrom: at(10),
timeTo: at(17),
draggable: true,
},
{
title: "Onboarding",
subtitle: "Berlin office",
dateFrom: day(25),
dateTo: day(28),
timeFrom: at(9),
timeTo: at(16),
draggable: true,
},
];
export default function Example() {
// Only the month and the entries are set; look and behavior are widget defaults.
// A timed entry on one day is text with a stripe. The start time sits at the right edge.
// Entries without times are light gray bars (Public Holiday, Team Offsite), with the same stripe.
// A timed entry across days is the same kind of bar, lined up with the days. The end time sits at the right edge; if the bar wraps, its first row shows the start time at the right edge of the start day.
//
// Drag and save: `draggable: true` lets an entry be moved to another day. Its left and right
// edge make it shorter or longer, from one day to several. The calendar only reports the
// result. Write `endData` into the entry at `entryIndex`, then save it to your API.
// If saving fails, put `startData` back. Without this step the entry jumps back on the next update.
//
// Day 5 has four timed entries. `maxTimeEntries.amount: 2` shows two and turns the rest into "+ 2 more".
const [entries, setEntries] = useState(initialEntries);
const persist = (
entryIndex: number,
endData:
| ArcWidgetCalendarGridEntryDragSavePayload
| ArcWidgetCalendarGridDailyEntryDragSavePayload,
) => {
setEntries((current) =>
current.map((entry, index) =>
index === entryIndex ? { ...entry, ...endData } : entry,
),
);
};
return (
| Feld | Beschreibung |
|---|---|
| `allowedTypes` | Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"` und `"svg"` angeben. |
| `target` | Legt fest, wohin die Datei exportiert werden soll. `"file"` bedeutet, die Datei wird heruntergeladen. `"field"` bedeutet, die Datei wird in ein Datei-Feld in Ninox hochgeladen. |
| `recordId` | Gibt den Datensatz an, in den exportiert werden soll (nur bei `target: "field"` erforderlich). |
| `fieldId` | Definiert das genaue Feld im Datensatz, in das die exportierte Datei geschrieben wird. |
`header`: Titel (z. B. „Mangel #3“) - `content`: Beschreibung oder weitere Infos - `hide`: `true` oder `false` – blendet das Item in der Sidebar aus, ohne es zu löschen (z. B. für gefilterte Ansichten)
| Feld | Beschreibung |
|---|---|
| `allowedTypes` | Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"` und `"svg"` angeben. |
| `target` | Legt fest, wohin die Datei exportiert werden soll. `"file"` bedeutet, die Datei wird heruntergeladen. `"field"` bedeutet, die Datei wird in ein Datei-Feld in Ninox hochgeladen. |
| `recordId` | Gibt den Datensatz an, in den exportiert werden soll (nur bei `target: "field"` erforderlich). |
| `fieldId` | Definiert das genaue Feld im Datensatz, in das die exportierte Datei geschrieben wird. |
`header`: Titel (z. B. „Mangel #3“) - `content`: Beschreibung oder weitere Infos - `hide`: `true` oder `false` – blendet das Item in der Sidebar aus, ohne es zu löschen (z. B. für gefilterte Ansichten)
cards: [{
card: 1,
title: "Gesamtumsatz",
value: "€" + shortNumbers(cnt((select Projekte).Umsatz)),
subtitle: "€" +
shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 2,
title: "Abgeschlossen gesamt",
value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)),
subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 3,
title: "Conversion Rate",
value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) +
"%",
subtitle: round(cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) /
cnt(select Projekte where month(Abgeschlossen) = month(today())) *
100, 2) + "%" + " in diesem Monat"
}]
## Parameter
In dem Detail-Bereich deiner KPI-Karten kannst du verschiedenste Parameter definieren. Dabei kannst du selbst entscheiden, welche Parameter du definieren möchtest. Lässt du Parameter einfach weg, werden diese nicht ausgegeben. Hier siehst du alle Parameter, die du bei der KPI Bar verwenden kannst:
```javascript
card: "",
settings: {
colorLabel: "",
fontSizeLabel: "",
colorValue: "",
fontSizeValue: "",
backgroundColor: "",
borderColor: ""
},
title: "",
value: "",
subtitle: "",
actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Feld"), value: "..." }]
```
## Funktionen
Du kannst deine Parameter mit allen Ninox Funktionen & Schreibweisen definieren. Wir haben dir aber auch einige nützliche arcRider Funktionen erstellt, die dir deine Werte noch übersichtlicher machen.
*shortNumbers() *kürzt lange Zahlen ab 4 Stellen ab und setzt den Zahlennamen (Tsd.,Mio.,Mrd.,Bill.) dahinter.
```javascript
shortNumbers("2000") // Gibt den abgekürzten Wert der statischen Zahl aus (2 Tsd.)
shortNumbers(cnt((select Projekte).Umsatz)) // Gibt den abgekürzten Wert der dynamischen Zahl aus (z.B. 35 Mio.)
```
## Beispiele
Folgend findest du einige Praxisbeispiele, die dir den unterschiedlichen Einsatz der KPI Bar veranschaulichen.
### KPI Card Fallback

```javascript
let data := {
uniqueId: "KPI Bar 1",
settings: {
height: "",
colorLabel: "",
fontSizeLabel: "15",
colorValue: "",
fontSizeValue: "40px",
backgroundColor: "",
borderColor: ""
},
cards: [{
card: 1,
title: "Gesamtumsatz",
value: "€" + shortNumbers(cnt((select Projekte).Umsatz)),
subtitle: "€" +
shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 2,
title: "Abgeschlossen gesamt",
value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)),
subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 3,
title: "Conversion Rate",
value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) +
"%",
subtitle: round(cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) /
cnt(select Projekte where month(Abgeschlossen) = month(today())) *
100, 2) +
"%" +
" in diesem Monat"
}]
};
arcKPIBar(data)
```
### KPI Bar individuell

```javascript
let data := {
uniqueId: "KPI Bar 2",
settings: {
height: "",
colorLabel: "",
fontSizeLabel: "15",
colorValue: "",
fontSizeValue: "40px",
backgroundColor: "",
borderColor: ""
},
cards: [{
card: 1,
settings: {
colorLabel: "#F9244B",
fontSizeLabel: "15",
colorValue: "#F9244B",
fontSizeValue: "40px",
backgroundColor: "#FCD9DF",
borderColor: "#EABDC5"
},
title: "Gesamtumsatz",
value: "€" + shortNumbers(cnt((select Projekte).Umsatz)),
subtitle: "€" +
shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 2,
settings: {
colorLabel: "#000",
fontSizeLabel: "15",
colorValue: "#1f96bf",
fontSizeValue: "40px",
backgroundColor: "#FDE8A5",
borderColor: "#D9C78C"
},
title: "Abgeschlossen gesamt",
value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)),
subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 3,
settings: {
colorLabel: "#CAECCD",
fontSizeLabel: "15",
colorValue: "#CAECCD",
fontSizeValue: "40px",
backgroundColor: "#56C275",
borderColor: "#9DCDA1"
},
title: "Conversion Rate",
value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) +
"%",
subtitle: cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) /
cnt(select Projekte where month(Abgeschlossen) = month(today())) *
100 +
"%" +
" in diesem Monat"
}]
};
arcKPIBar(data)
```
### KPI Bar Darkmode

```javascript
let data := {
uniqueId: "KPI Bar 3",
settings: {
height: "",
colorLabel: "",
fontSizeLabel: "15",
colorValue: "",
fontSizeValue: "40px",
backgroundColor: "",
borderColor: ""
},
cards: [{
card: 1,
settings: {
colorLabel: "#8e99c4",
fontSizeLabel: "15",
colorValue: "#8e99c4",
fontSizeValue: "40px",
backgroundColor: "#49547e",
borderColor: "#646f9b"
},
title: "Gesamtumsatz",
value: "€" + shortNumbers(cnt((select Projekte).Umsatz)),
subtitle: "€" +
shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 2,
settings: {
colorLabel: "#8e99c4",
fontSizeLabel: "15",
colorValue: "#8e99c4",
fontSizeValue: "40px",
backgroundColor: "#49547e",
borderColor: "#646f9b"
},
title: "Abgeschlossen gesamt",
value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)),
subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
" in diesem Monat"
}, {
card: 3,
settings: {
colorLabel: "#8e99c4",
fontSizeLabel: "15",
colorValue: "#8e99c4",
fontSizeValue: "40px",
backgroundColor: "#49547e",
borderColor: "#646f9b"
},
title: "Conversion Rate",
value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) +
"%",
subtitle: cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) /
cnt(select Projekte where month(Abgeschlossen) = month(today())) *
100 +
"%" +
" in diesem Monat"
}]
};
arcKPIBar(data)
```
**Weiterführende Links: **
[> NX Custom KPI Bar ](https://www.arc-rider.de/custom-kpi-bar)
---
# Custom Layout
Source: https://www.arc-rider.com/docs/widgets/custom-layout
# Layout in React
Widgets und komplette Seiten horizontal oder vertikal anordnen. Beginne für neue Apps mit Arc Layout; HTML kann als Host und semantische Grenze bestehen bleiben. Ein eigener Host legt nur die Fensterhöhe fest, er muss das Seitenlayout nicht mit CSS Grid oder Flexbox nachbauen.
Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components).
## Widget-First-App-Shell: AI Defaults
Für Sidebar, Kopfzeile und Inhalt nutze die [vollständige React-Vorlage](/docs/react/templates/react-starter), auch im Paket als `examples/blueprints/blueprint-app-shell.tsx`.
- Host: definierte Höhe, z. B. `100dvh`; verschachtelte Rahmen: `height: '100%'`.
- Vertikale Blöcke: Kopfzeile `height: 'auto'`, restlicher Inhalt `height: 'fraction'`.
- Scrollen: im restlichen Block ein Layout mit `height: '100%'` und `scrollSettings: { scrollY: true }`.
- Horizontale Blöcke: feste/automatische Sidebar-Breite, Hauptbereich `width: 'fraction'`.
- `min-width:0;min-height:0;` für begrenzte Blöcke; responsive Richtung und Größen zusammen umstellen.
- `fraction` als Block-Höhe nutzen, nicht als Höhe des inneren Layouts. Der Elternbereich muss eine definierte Höhe besitzen.
## Vollständiges Beispiel
```tsx
import {
ArcWidgetBadge,
ArcWidgetInput,
ArcWidgetLayout,
} from '@arcrider/arcwidgets-react';
const row = (number: string, title: string) => ({
value: (
Direkt auf oberster Ebene (`uniqueId`, `embedded`, `hoverActions`) - **Menü-Konfiguration (**`**menu[…]**`**)**
Steuert das Layout der Menüleiste – z. B. Positionierung, Schriftgröße und Farben für normale und aktive Zustände - **Menüpunkte (**`**menu[…].menuItems[…]**`**)**
Einzelne Buttons mit Titel, Icon, Aktivitätszustand und Aktionen (z. B. Feldwert ändern, Popup öffnen)
```javascript
let current := this;
arcCustomMenu({
uniqueId: "menu " + Nr,
embedded: false,
borderRadius: "4px",
hoverActions: {
backgroundColor: "#192e65",
fontColor: "#fff",
borderColor: "#192e65",
iconColor: "#fff",
animation: "0.2s"
},
menu: [{
tableId: tableId(current),
embedded: false,
settings: {
menuBar: {
width: "",
position: "",
backgroundColor: "",
borderColor: "",
fontSize: "20px"
},
stateNormal: {
backgroundColor: "",
borderColor: "",
fontColor: ""
},
stateActive: {
backgroundColor: "",
borderColor: "",
fontColor: ""
}
}
}, {
menuItems: [{
item: 1,
title: "Table",
active: Menu = text("A"),
icon: arcCustomIcon({
name: "table"
}),
actions: [{
recordId: Nr,
type: "update",
field: "K",
value: "A"
}]
}, {
item: 2,
title: "Kanban",
active: Menu = text("B"),
icon: arcCustomIcon({
name: "kanban"
}),
actions: [{
recordId: Nr,
type: "update",
field: "K",
value: "B"
}]
}, {
item: 3,
title: "KPI Bar",
active: Menu = text("C"),
icon: arcCustomIcon({
name: "copy"
}),
actions: [{
recordId: Nr,
type: "update",
field: "K",
value: "C"
}]
}]
}]
})
```

## Allgemeine Parameter-Einstellungen
### uniqueId
**Typ:** `text`
**Pflichtfeld:** ja
Definiert eine eindeutige ID für die jeweilige Menü-Instanz. Diese ID wird intern zur gezielten CSS-Ansprache verwendet und ist notwendig, damit mehrere Menüs auf derselben Seite unabhängig voneinander funktionieren.
💡 **Tipp: **Setze die ID dynamisch, z. B. mit `uniqueId: "menu-" + Nr`, wenn du das Widget mehrfach aufrufst.
### embedded
**Typ:** `boolean`
**Default:** `false`
Legt fest, ob das Widget in ein bestehendes Widget (z. B. [Layout](https://www.arc-rider.de/documentation/custom-layout)) eingebettet wird oder als eigenständiges Widget auf der Ninox Oberfläche integriert wird.
💡 **Hinweis: **Wenn du `embedded: true` setzt, musst du sicherstellen, dass das umgebende Element eine passende Höhe und ggf. ein eigenes Layout mitbringt. Das Widget passt sich dann vollständig diesem Rahmen an.
### hoverActions
**Typ:** `object`
**Default:** kein Hover-Verhalten
Definiert das Verhalten beim Überfahren eines Menüpunktes mit der Maus.
Du kannst hier gezielt Farben und Animationen setzen, die beim Hover-Effekt angewendet werden sollen. Alle Felder sind optional – wenn du nichts angibst, bleibt das Standarddesign erhalten.
Feld
Typ
Beschreibung
`backgroundColor`
`text`
Hintergrundfarbe beim Hover. Z. B. HEX-Wert: `"#192e65"`
`fontColor`
`text`
Schriftfarbe beim Hover. Z. B. HEX-Wert: `"#fff"`
`borderColor`
`text`
Rahmenfarbe beim Hover. Z. B. HEX-Wert: `"#192e65"`
`iconColor`
`text`
Farbe des Icons beim Hover. Z. B. HEX-Wert:`"#fff"`
`animation`
`text`
Transition-Zeit, z. B. `"0.2s"` für weiche Effekte
`type`
Beschreibung
Aufbau Code
`update`
Setzt den Wert eines Feldes im angegebenen Datensatz
`recordId`, `field`, `value`
`popup`
Öffnet den angegebenen Datensatz als Popup
`recordId`
`delete`
Löscht den angegebenen Datensatz aus der Datenbank
`recordId`
Feld
Beschreibung
`height`
Höhe des Tracks, z. B. `"6px"`
`backgroundColor`
Farbe des Tracks, z. B. `"#E9ECF4"`
`borderRadius`
Border-Radius des Tracks, z. B. `"3px"`
Feld
Beschreibung
`width` / `height`
Größe des Knopfs, z. B. `"20px"`
`backgroundColor`
Füllfarbe des Knopfs
`borderColor` / `borderWidth` / `borderRadius`
Rahmen des Knopfs, z. B. `borderRadius: "50%"` für rund
Feld
Beschreibung
`label`
Icon oder Text, z. B. `"🔥"`, `"✓"`, `"⚠"`, `"✕"`
`value`
Schwellenwert – ab diesem Wert wird der Status aktiv
`color`
Farbe des Icon-Texts
`track` / `fill` / `thumb`
Optional: gleiche Struktur wie oben, überschreibt das Standard-Styling für diesen Status (mit `!important`)
Feld
Typ
Beschreibung
`value`
`text`
Inhalt des Slides: einfacher Text, HTML-String (` `label`
`text`
Fallback-Text, falls `value` leer ist oder kein String – z. B. `"Slide 1"`.
`width` / `height`
`text`
Größe des einzelnen Slides (Default: `100%` / `100%`).
`backgroundColor`
`text`
Individuelle Hintergrundfarbe für diesen Slide.
Feld
Typ
Beschreibung
`title`
`text`
Titel oberhalb des Kreises
`value`
`text`/`number`
Zentrale Anzeige im Kreis
`subtitle`
`text`
Text unterhalb des Kreises
`width`
`text`
Individuelle Breite des Steps
`colorLabel`
`text`
Textfarbe für `title` und `subtitle`
`fontSizeLabel`
`text`
Schriftgröße für `title` und `subtitle`
`colorValue`
`text`
Textfarbe des zentralen Werts
`fontSizeValue`
`text`
Schriftgröße des Werts
`backgroundColor`
`text`
Hintergrundfarbe des Kreises und der Verbindungslinien
`borderColor`
`text`
Farbe der Step-Trennlinie
`actions[]`
`array`
Aktionen, die beim Klick ausgelöst werden können (siehe unten)
Feld
Typ
Beschreibung
`title`
`text`
Titel oberhalb des Kreises
`value`
`text`/`number`
Zentrale Anzeige im Kreis
`subtitle`
`text`
Text unterhalb des Kreises
`width`
`text`
Individuelle Breite des Steps
`colorLabel`
`text`
Textfarbe für `title` und `subtitle`
`fontSizeLabel`
`text`
Schriftgröße für `title` und `subtitle`
`colorValue`
`text`
Textfarbe des zentralen Werts
`fontSizeValue`
`text`
Schriftgröße des Werts
`backgroundColor`
`text`
Hintergrundfarbe des Kreises und der Verbindungslinien
`borderColor`
`text`
Farbe der Step-Trennlinie
`actions[]`
`array`
Aktionen, die beim Klick ausgelöst werden können (siehe unten)
",
width: "100px"
}, {
field: "Abgeschlossen",
title: "Abgeschlossen am",
value: text(Abgeschlossen),
width: "100px"
}]
}],
footer: {
showFooter: false,
actionButtonTitle: "",
rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte"
}
};
arcCustomTable(data)
```
### Custom Table Complex

```javascript
let current := this;
arcCustomTable(data);
let list := do as transaction
select Aufgaben
end;
let filteredList := list[if current.Suche != null then
testx(Mitarbeiter.'First Name' + "," + text(Mitarbeiter.'Last Name') + "," +
text(Aufgabe) +
"," +
text(Projekte.Bezeichnung) +
"," +
", "(?:" + current.Suche + ")\.*[^]", "gi")
else
true
end and
if current.'Erledigte einblenden' = true then
true
else
Erledigt != true
end];
let data := {
uniqueListId: "Projektliste A",
tableId: tableId(first(list)),
height: "500px",
theme: "",
groupedBy: text('Gruppieren nach'),
embedded: false,
table: filteredList.[{
recordId: Nr,
rowColor: "",
rowHeight: "60px",
groupRowColor: let currentRecord := this;
if cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung and Erledigt = true]) = cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung]) then
"#dcf2de"
else
"#eee"
end,
columns: [{
field: "Erledigt",
title: arcCheckBox({
uniqueId: "checkbox all check",
value: Erledigt,
embedded: true,
clickAction: {
recordId: Nr,
fieldId: "G",
value: if cnt(list[Erledigt = true]) != cnt(list) then
false
else
if cnt(list[Erledigt = null]) = cnt(list) then
null
else
if cnt(list[Erledigt = true]) = cnt(list) then
true
end
end
end
}
}),
value: arcCheckBox({
uniqueId: "checkbox single check",
value: Erledigt,
embedded: true,
clickAction: {
recordId: Nr,
fieldId: "G",
value: if Erledigt = true then null else true end
}
}),
width: "100px",
align: "center",
fixed: "left",
groupValue: arcCustomIcon({
name: "caret-down"
})
}, {
field: "Bild",
title: "Logo",
width: "150px",
value:",
actions: [{
recordId: Projekte.Nr,
type: "popup"
}],
groupValue: "",
}, {
field: "Projekt",
title: "Projekt",
width: "",
value: Projekte.Bezeichnung,
actions: [{
recordId: Projekte.Nr,
type: "popup"
}],
groupValue: Projekte.Bezeichnung,
}, {
field: "Aufgaben",
title: "Aufgabe",
value: Aufgabe,
align: "left",
width: "200px",
actions: [{
recordId: Nr,
type: "popup"
}, {
recordId: Nr,
type: "change",
field: "A"
}],
groupValue: if current.'Gruppieren nach' = 3 then
html(---
**{ Aufgabe }**
---)
end
}, {
field: "Mitarbeiter",
title: "Mitarbeiter",
value:",
actions: [{
recordId: Mitarbeiter.Nr,
type: "popup"
}]
}, {
field: "delete",
title: "",
width: "40px",
align: "center",
value: "",
actions: [{
recordId: Nr,
type: "delete"
}]
}]
}],
footer: {
showFooter: true,
showActionButton: true,
actionButtonTitle: "Neue Aufgabe hinzufügen",
rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben"
}
};
arcCustomTable(data)
```
# Custom Table
## AI Defaults (read first)
- **Column widths**: Every column MUST have explicit `width`. Prefer at least one `width: "fraction"` or `"auto"`. If every column is a fixed length and the tracks are narrower than the table, the last column grows to fill the leftover space.
- **Height**: Use `height: "100%"` (not `calc()`). Especially when `embedded: true`.
- **uniqueListId**: Required, use `"my-table-" + Nr`.
- **tableId**: Required (e.g. from `tableId("Tabellenname")`).
- **recordId**: Always raw `Nr`, NEVER `number(Nr)`.
- **Empty state**: Use `emptyTable: { title: "...", value: "..." }`, do NOT wrap table in if/else.
- **theme.uniqueId**: Must match `uniqueListId`.
- **Columns value**: Each column needs `recordId: Nr` inside the mapping.
- **Row actions**: Use `actions` array with `type: "popup"` or `type: "update"`, recordId = `Nr`.
- **Sort**: Client-side only; `header.columns[n].sort: { enabled: true, type: "text"|"number"|"date" }`; optional `columns[n].sort.value` for sort key when `value` is not plain text. Optional `header.sortIcons: { color, activeColor, hoverColor }` for caret colors (defaults: muted gray / black active / stronger on header hover).
- **Hover**: `actions: [{ type: "hover", fontColor, backgroundColor, ... }]` on table root, row, or column; `type: "hover"` is not passed to click handlers. Legacy `rowHoverAction` still supported.
- **Borders**: Live under `styles.borders`. Default is a rounded frame + row lines, **no vertical cell lines**. Hide the frame with `styles: { borders: { outer: false } }`. Opt into a grid with `columns: true`. Set `styles.borderColor` for dark backgrounds; a line may be `{ color, width }` instead of a boolean.
- **styles hierarchy**: Same `styles` bag on table, header, row, and column — prefer `{ backgroundColor, fontColor, … }` over a CSS string. Table `styles.backgroundColor` is the default surface. A row or column overrides with its own `styles.backgroundColor`. Header uses `header.backgroundColor` / `header.styles`. Do not invent extra props like `rowBackgroundColor`.
# Custom Table
Das **Custom Table**-Widget ermöglicht dir die Darstellung dynamischer, interaktiver Tabellen direkt auf deiner Ninox-Oberfläche – maßgeschneidert nach deinen Anforderungen. Es eignet sich ideal, um strukturierte Daten übersichtlich darzustellen, individuell zu formatieren und mit Aktionen zu verknüpfen (z. B. für Bearbeiten, Löschen, Öffnen von Datensätzen oder API-Aufrufe).
## Beispiel:

## Gesamter Anwendungscode
Im Folgenden siehst du einen beispielhaften Anwendungscode, der die Basis deiner **Custom Table** definiert. Da der Code – je nach Anwendungsfall – sehr umfangreich werden kann, führen wir dich schrittweise **von einer einfachen Basis** zu **komplexeren Varianten**.
Der Aufbau gliedert sich in zwei zentrale Bereiche:
- `**data**` – definiert den Inhalt und die Struktur deiner Tabelle (Zeilen und Spalten)
- `**arcCustomTable()**` – ist die globale Funktion, die das Widget rendert. Ihr übergibst du die `data`
```javascript
let current := this;
let projectList := Artikel;
let data := {
uniqueListId: "beispiel" + Nr,
tableId: "",
height: "500px",
minHeight: "",
groupedBy: "",
groupsCollapsed: false,
theme: arcCustomThemeCleanWhite({
uniqueId: "beispiel"
}),
embedded: false,
rowHoverAction: {
hoverActive: true,
backgroundColor: "",
fontColor: ""
},
header: {
showHeader: true,
height: "",
fontColor: "",
backgroundColor: "",
fontSize: "",
columns: [{
title: "Prio",
width: ""
}]
},
emptyTable: {
title: "Keine Ergebnisse",
backgroundColor: "",
value: ""
},
scrollBar: {
showScrollBar: false,
height: "",
backgroundColor: "",
handle: {
height: "",
borderRadius: "",
backgroundColor: ""
}
},
table: projectList.[{
recordId: Nr,
rowColor: "",
rowHeight: "auto",
rowPaddingY: "10px",
groupRowColor: "",
groupRowSettings: {
height: "",
},
columns: [{
field: "",
title: "",
value: "",
color: "",
backgroundColor: "",
width: "",
align: "left",
paddingX: "",
paddingY: "",
groupExpandAction: false,
groupValue: arcCustomIcon({
name: "caret-down"
}),
groupByValue: "",
actions: [{
type: "popup",
showPopupButton: false,
recordId: Nr
}]
}]
}],
footer: {
showFooter: false,
showActionButton: true,
backgroundColor: "",
actionButtonTitle: "",
leftSideContent: "",
rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte"
}
};
arcCustomTable(data)
```
### uniqueListId
Der Parameter `**uniqueListId**` ist die **individuelle Bezeichnung deiner Tabelle**. Er sorgt dafür, dass deine Tabelle intern eindeutig identifiziert wird – besonders dann, wenn **mehrere Tabellen gleichzeitig auf einer Seite** angezeigt werden.
**Warum ist das wichtig?**
- Style-Einstellungen (z. B. Farben, Schriftarten, Hover-Effekte) werden gezielt nur auf die Tabelle mit der angegebenen `uniqueListId` angewendet.
- Bei mehreren Tabellen auf einer Ansicht kann es sonst zu **Konflikten bei CSS-Klassen oder Zuständen** kommen.
**✅ Best Practice:**
Verwende **sprechende, eindeutige Namen** – am besten in camelCase oder mit Unterstrichen.
```javascript
uniqueListId: "Projektliste Offen", // Text
```
### tableId
Mit `**tableId**` gibst du die **interne ID der Tabelle in Ninox** an, auf die sich das Widget bezieht. Diese ID wird z. B. benötigt, um:
- neue Datensätze über den **„+“-Button** im Footer anzulegen (`create`)
- bestehende Datensätze zu öffnen oder bearbeiten zu können (`popup`, `openFullscreen`, `update`)
- Aktionen korrekt auf Datensätze dieser Tabelle zu beziehen
```javascript
tableId: "AB", // Die ID als Textform
tableId: tableId("Kontakte"), // Ninox Funktion zum Herausfinden der Table ID deiner darzustellenden Tabelle
```
### height
Mit `**height**` bestimmst du die **Höhe des Tabellen-Widgets** in **Pixeln**. So kannst du die Darstellung flexibel an das Layout deiner Seite anpassen – egal, wie viele Daten enthalten sind.
**💡 Werte**
- Der Wert wird in Pixeln angegeben – z. B. `"300px"`, `"500px"` oder `"800px"`.
- `**"auto"**`: Die Höhe passt sich automatisch an den Inhalt an. Perfekt für dynamische Inhalte – beachte aber mögliche Sprünge im Layout.
- In Kombination mit `maxHeight` kannst du die automatische Höhe begrenzen.
```javascript
height: "500px", // Pixel Werte
```
### minWidth
Mit `**minWidth**` legst du die **minimale Breite** deiner Tabelle fest – unabhängig davon, wie viele Spalten oder Daten enthalten sind. So stellst du sicher, dass die Tabelle nicht zu schmal dargestellt wird, z. B. bei schmalen Containern, Tabs oder eingebetteten Ansichten.
🔍 **Format**
- Der Wert wird als **Text mit Einheit** übergeben, z. B. `"600px"`
- Ohne Angabe kann die Tabelle bei zu wenig Inhalt schmal zusammenfallen – was das Layout stören kann.
💡 **Hinweis:**
`minWidth` wirkt sich auf die äußere Tabellenhülle aus – nicht auf die Spaltenbreiten im Inneren. Für Spalten gibt es separate Optionen wie `width` und `minWidth` direkt im `columns`-Block.
### groupedBy
Mit `**groupedBy**` definierst du, nach welchem **Feld (Spalten-Key bzw. **`**field**`**)** deine Tabelle gruppiert werden soll. Gruppen werden dabei visuell als **separate Zeilen mit eigener Formatierung** dargestellt – die darunterliegenden Einträge lassen sich ein- und ausklappen.
🔍 **Verhalten**
- Du gibst hier den **Spaltennamen (**`**field**`**)** an, der in deinem `columns`-Block definiert ist.
- Ist das Feld leer (`"`) oder fehlt ganz, **findet keine Gruppierung statt**.
- Gruppierung kann auch auf komplexen Werten basieren (z. B. `Typ`, `Kategorie`, `Verantwortlicher` etc.).
- Die Gruppen lassen sich dynamisch durch Klick auf die Gruppenzeile **auf- und zuklappen** – dieser Zustand wird **lokal gespeichert**.
```javascript
groupedBy: "", // default: keine Gruppierung ausgewählt
groupedby: "Status", // Text des Feldtitels, nach dem gruppiert werden soll
groupedBy: Gruppierung, // Ninox Feld, das angesprochen wird. In diesem Fall: Ein Auswahlfeld, in dem die Optionen den Titel verschiedener Ninox-felder tragen, nach denen gruppiert werden kann. (z.B. Status, Mitarbeiter, Aufgabe)
```
### groupsCollapsed
Mit `**groupsCollapsed**` legst du fest, ob gruppierte Zeilen **standardmäßig eingeklappt** oder **ausgeklappt** dargestellt werden. Die Gruppenüberschrift (groupRow) ist dabei immer sichtbar – nur die zugehörigen Einträge (valueRows) werden bei Bedarf ein- oder ausgeblendet.
**Dieser Parameter wirkt nur, wenn **`**groupedBy**`** gesetzt ist.**
`**true**` = Gruppen sind beim Laden **eingeklappt** (kompakte Ansicht).
`**false**` = Gruppen sind beim Laden **vollständig geöffnet**.
**Der Zustand (ein-/ausgeklappt) wird lokal gespeichert**, d. h. bei erneutem Öffnen merkt sich das Widget den letzten Zustand pro Benutzer.
**Standardwert: **`**false**`** (Gruppen sind offen)** – wenn nicht angegeben, zeigt die Tabelle alle Gruppeneinträge vollständig an.
```javascript
groupscollapsed: "", // default: false
groupsCollapsed: true,
```
### embedded
Mit `**embedded**` aktivierst du den **eingebetteten Modus** der Tabelle. In diesem Modus passt sich die Tabelle optimal an bestehende Layoutstrukturen an – z. B. in einem Container, Tab oder einem flexiblen UI-Bereich.
**Standardwert: **`**false**` – die Tabelle nimmt dann ihren eigenen, festen Platz auf der Seite ein.
**Wenn **`**embedded: true**`** gesetzt ist, wird das Widget nicht absolut, sondern relativ im Container positioniert** und übernimmt dessen Breite und ggf. auch andere Stilvorgaben. Kombiniert mit Parametern wie `height` und `minWidth` sorgt das für ein sauberes, integriertes Erscheinungsbild.
**Besonders nützlich ist **`**embedded**`** in Dashboards, Tab-Ansichten oder Modalfenstern.**
```javascript
embedded: false,
embedded: true,
embedded: "", // default: false
```
### styles.borders
Rahmen und Gitterlinien leben in **`styles`** — zusammen mit Hintergrund, Radius und Linienfarbe.
Default: abgerundeter Außenrahmen, feine Zeilenlinien, **keine vertikalen Zelllinien**.
Jedes Linienfeld ist `true` / `false` **oder** `{ color, width }`.
| Prop | Default | Bedeutung |
|---|---|---|
| `styles.borderColor` | `#e4e4e7` | Farbe für alle angeschalteten Linien |
| `styles.borderRadius` | `12px` | Eckenradius (nur wenn `outer` an) |
| `styles.borders.outer` | `true` | Außenrahmen. `false` = bündig im Layout |
| `styles.borders.columns` | `false` | Vertikale Linien |
| `styles.borders.rows` | `true` | Horizontale Linien |
| `styles.borders.color` / `width` | — | Gemeinsame Linie, überschreibt `borderColor` / Default-Stärke |
```javascript
styles: {
borderRadius: "12px",
borderColor: "#e4e4e7",
borders: {
outer: true,
columns: false,
rows: true
}
}
// In einem Layout / Card:
styles: { borders: { outer: false } }
// Dunkler Hintergrund — Fläche auf styles, Kopf auf header:
styles: {
backgroundColor: "#2a2822",
fontColor: "#f5f0e6",
borderColor: "#3f3c34"
},
header: {
backgroundColor: "#1c1b16",
fontColor: "#f5f0e6"
}
// Klassisches Gitter:
styles: { borders: { columns: true } }
```
Fertige Varianten (Default, Card, Gitter, flush, Dark): [Custom Table Look](/documentation/custom-table-look).
### rowHoverAction
Mit dem `**rowHoverAction**`-Block bestimmst du, ob das **Hover-Verhalten auf Tabellenzeilen** aktiv sein soll – und falls ja, **welche Farben** beim Überfahren mit der Maus verwendet werden.
- `**hoverActive: true**` aktiviert den Effekt, andernfalls bleibt die Zeile beim Hover unverändert.
- `**backgroundColor**` definiert die **Hintergrundfarbe** der Zeile im Hover-Zustand,
- `**fontColor**` legt die **Textfarbe** während des Hoverns fest.
**Standardwert bei **`**hoverActive**`** ist **`**true**`, falls der Block gesetzt ist. Ohne `rowHoverAction` ist das Verhalten neutral – keine Hover-Effekte.
**Tipp: Setze gedeckte Farben ein**, um den Effekt dezent und UX-freundlich zu halten.
```javascript
rowHoverAction: { hoverActive: true, // wenn hovern an sein soll, und false, wenn nicht
backgroundColor: "#9ca4a9", // default: #f4f6ff
fontColor: "#fff" } // default: #000
```
**Empfehlung:** Für neue Projekte nutze `actions: [{ type: "hover", ... }]` (siehe unten) – `rowHoverAction` bleibt aus Backwards-Compatibility erhalten.
### actions mit type: "hover"
Hover-Styling und optionale Hover-Actions über dasselbe `actions`-Array wie `update` / `popup`. Eintrag mit `**type: "hover"**` wird **nicht** an Klick-Actions weitergegeben, sondern nur vom Widget ausgewertet.
**Merge-Reihenfolge:** `rowHoverAction` (Legacy) → `actions` auf Tabellen-Root → `row.actions` → `columns[n].actions`.
| Sub-Prop | Bedeutung |
|---|---|
| `enabled` | `false` = keine Hover-Hervorhebung für diese Zeile/Zelle. |
| `backgroundColor` | Hintergrundfarbe bei Hover. |
| `fontColor` | Textfarbe bei Hover. |
| `styles` | Zusätzliches CSS (String) für die Zelle bei gehoverter Zeile. |
| `actions` | Optional: bei **mouseenter** an den Action-Handler (z. B. `update`). |
| `leaveActions` | Optional: bei **mouseleave**. |
**Row-Level:** `actions: [{ type: 'hover', backgroundColor: '#eff6ff', fontColor: '#0f172a' }]` auf dem Zeilenobjekt.
**Column-Level:** z. B. `{ type: 'hover', enabled: false }` um eine einzelne Spalte von der Hover-Hervorhebung auszunehmen.
**`row.actions`** (ohne `hover`) – zusätzliche `update`/`popup`-Einträge gelten für die **ganze Zeile** (Klick auf eine Zelle führt Row- und Cell-Actions zusammen aus).
### header
Mit dem ***header***-Block steuerst du die Darstellung deiner **Tabellenkopfzeile** (Spaltenüberschriften).
**Standardverhalten:** Wenn `showHeader` nicht angegeben ist, wird die Header-Zeile **angezeigt**. Ohne weitere Einstellungen gelten Standardwerte für Größe, Farbe und Hintergrund.
Tipp: Wenn du `showHeader: false` setzt, sollte die Bedeutung der Daten trotzdem klar erkennbar bleiben (Position, Farbschema der Spalten).
#### showHeader
Blendet die gesamte Header-Zeile ein oder aus.
```javascript
header: {
showHeader: false, // default: true
}
```
#### height
Höhe der Headerzeile (z. B. `"48px"` oder `"auto"`).
```javascript
header: {
height: "70px", // default: 36px
}
```
#### fontColor
Schriftfarbe der Spaltenüberschriften.
```javascript
header: {
fontColor: "#fff", // default: #000
}
```
#### fontSize
Schriftgröße der Überschriften.
```javascript
header: {
fontSize: "17px", // default: 12px
}
```
#### backgroundColor
Hintergrund der Headerzeile.
```javascript
header: {
backgroundColor: "#3a4a54", // default: #f8f9fc
}
```
#### styles
CSS-String für freies Styling des Header-Containers (überschreibt `fontColor`, `backgroundColor` etc.).
```javascript
header: {
styles: "border-bottom: 1px solid #eee;",
}
```
Vollständiges Beispiel:
```javascript
header: {
showHeader: false, // default: true
height: "70px", // default: 36px
fontColor: "#fff", // default: #52525b
backgroundColor: "#3a4a54", // default: #fafafa
fontSize: "17px", // default: 12px
}
```
### sort (Client-Side)
Sortierung erfolgt **vollständig im Browser** (kein Ninox-Update, kein Helper-Feld). Der Zustand (aktive Spalte, Richtung) wird **intern** verwaltet und wie Scroll/Gruppen im **localStorage** persistiert.
**Header-Spalte** – `sort` aktivieren und Typ setzen:
- `**sort.enabled**`: `true` – Spaltenkopf ist klickbar, zeigt Sort-Icons.
- `**sort.type**`: `'text'` (Standard), `'number'` oder `'date'`.
**Zeilen-Zelle** – optionaler Rohwert für den Vergleich:
- `**sort.value**`: Wert, nach dem sortiert wird. Wenn nicht gesetzt, wird `value` der Zelle verwendet (bei Widgets/HTML in `value` ungeeignet – dann immer `sort.value` setzen).
**Klick-Zyklus:** erste Klick → aufsteigend, zweiter → absteigend, dritter → Sortierung aus.
**Sort-Icons (Outline-Chevrons):** Farben optional über `header.sortIcons` (werden als CSS-Variablen am Header gesetzt):
| Prop | Bedeutung | Standard (CSS-Fallback) |
|------|-----------|-------------------------|
| `color` | Farbe der **inaktiven** Carets | `rgba(0,0,0,0.35)` |
| `activeColor` | Farbe des **aktiven** Carets (aktuelle Sortierrichtung) | `#000000` |
| `hoverColor` | Farbe beider Carets bei **Hover** über den sortierbaren Header (inaktive Carets; aktiver Caret bleibt `activeColor`) | `rgba(0,0,0,0.55)` |
Bei **Gruppierung** (`groupedBy`) wird nur **innerhalb jeder Gruppe** sortiert, nicht über Gruppen hinweg.
```javascript
header: {
showHeader: true,
sortIcons: {
color: 'rgba(0,0,0,0.4)',
activeColor: '#000000',
hoverColor: 'rgba(0,0,0,0.6)'
},
columns: [{
title: 'Name',
width: 'fraction',
sort: { enabled: true, type: 'text' }
}, {
title: 'Betrag',
width: '120px',
sort: { enabled: true, type: 'number' }
}]
},
table: myList.[{
recordId: Nr,
columns: [{
field: 'Name',
value: Name,
width: 'fraction'
}, {
field: 'Betrag',
value: text(Betrag) + ' €',
sort: { value: number(Betrag) },
width: '120px'
}]
}]
```
### emptyTable
Mit dem `**emptyTable**`-Block legst du fest, **was angezeigt werden soll, wenn keine Daten vorhanden sind** – also wenn `data: []` leer ist. Du kannst Text und Gestaltung des leeren Zustands anpassen und so die Nutzererfahrung deutlich verbessern.
- `**title**`: Der Haupttext, der angezeigt wird (meist prominent, z. B. „Keine Einträge gefunden“).
- `**value**`: Optionaler zusätzlicher Text oder HTML-Elemente im Inneren des leeren Bereichs.
- `**backgroundColor**`: Hintergrundfarbe für den leeren Bereich – z. B. `"white"` oder `"#f4f6ff"`.
**Standardverhalten:**
Ohne `emptyTable` wird ein schlichter Text „Keine Ergebnisse“ angezeigt.
💡 **Pro-Tipp:**
Leere Zustände sind keine Fehler – sie sind deine Bühne. Nutze sie für Erklärungen, motivierende Hinweise oder einen klaren Call-to-Action („Jetzt neuen Eintrag erstellen“). So bleiben deine Nutzer nicht im Leeren hängen – im wahrsten Sinne.
```javascript
emptyTable: {
title: "", // Text der bei einer leeren Tabelle in einem abgerundeten Badge dargestellt wird.
backgroundColor: "", // Hintergrundfarbe in HEX angeben
value: "" // Hier kannst du eigene Widgets oder komplexe Ansichten einfügen, die den Title überschreiben.
},
```

### scrollBar
Mit dem `**scrollBar**`-Block konfigurierst du die **horizontale Scrollbar** deiner Tabelle. Diese wird angezeigt, wenn deine Tabelle mehr Spalten enthält, als in den verfügbaren Bereich passen.
- `**showScrollBar**`: `true` oder `false` – bestimmt, ob die Scrollbar überhaupt angezeigt wird.
- `**height**`: Höhe des Scrollbar-Containers (z. B. `"10px"`).
- `**backgroundColor**`: Hintergrundfarbe des Containers.
- `**handle.height**`: Höhe des Schiebers („Handle“) innerhalb der Leiste.
- `**handle.backgroundColor**`: Farbe des Scroll-Handles.
- `**handle.borderRadius**`: Abgerundete Ecken des Schiebers (z. B. `"5px"` oder `"50%"`).
**Standardverhalten:**
Ohne `scrollBar` wird **keine sichtbare Scrollbar angezeigt** – auch wenn Inhalte horizontal überlaufen.
💡 **Pro-Tipp:**
Gerade bei vielen Spalten hilft eine dezente, gut sichtbare Scrollbar, um Orientierung zu geben. Nutze weiche Farben und eine runde Handle-Form, damit die UI nicht „technisch“ wirkt – besonders auf Touch-Geräten zahlt sich das aus.
```javascript
scrollBar: { showScrollbar: true, // default: false (Scrollbar ist standardmäßig ausgeblendet)
height: 40px, // Höhe des Containers. default: 10px
backgroundColor: "#3a4a54" // default: #f0f0f0
handle: { height: "20px", // Höhe des Schiebers. default: 90%
borderRadius: 20px // default: 20px
backgroundColor: "#fff", } // default: #ccc
```
### theme
Mit dem `**theme**`-Parameter kannst du ein **vordefiniertes Design-Template** für deine Tabelle aktivieren. Das Theme beeinflusst das **gesamte visuelle Erscheinungsbild** – von Farben über Schriftarten bis hin zum Layout. Aktuell verfügbar sind:
- `**"clean-white"**`
- `**"naked"**`
**Wichtig:**
Damit das Theme korrekt greift, muss die `theme`-uniqueId **die gleiche **`**uniqueListId**`** verwenden wie deine Tabelle**. Nur so überschreibt das Theme gezielt die richtigen Style-Regeln per CSS.
**Standardverhalten:**
Wird kein `theme` gesetzt, wird das Standarddesign des arcCustomTable-Widgets verwendet (leicht schattiert, kontrastreich).
```javascript
theme: arcCustomThemeCleanWhite({
uniqueId: "Projektliste Demo 1"
}),
```
### styles
Dieselbe `styles`-Tasche auf jeder Ebene. Bevorzugt ein Objekt mit CSS-ähnlichen Namen (`backgroundColor`, `fontColor`, `paddingX`). Ein CSS-String geht weiter.
| Ebene | Prop | Wirkt auf |
|---|---|---|
| Tabelle | `styles: { backgroundColor, borders, … }` | Fläche + Rahmen. Default für alle Zeilen |
| Header | `header: { backgroundColor }` oder `header.styles` | Kopfzeile |
| Header-Zelle | `header.columns[n].styles` | Eine Kopfzelle |
| Zeile | `table: [{ styles: { backgroundColor } }]` | Alle Zellen dieser Zeile |
| Spalte / Zelle | `table: [{ columns: [{ styles: { backgroundColor } }] }]` | Eine Zelle |
| Footer | `footer.styles` | Footer |
Spalte überschreibt Zeile, Zeile überschreibt Tabelle. `styles` gewinnt gegen `rowColor` / flaches `backgroundColor`.
```javascript
styles: {
backgroundColor: "#ffffff",
borderColor: "#e4e4e7",
borders: { outer: true, columns: false, rows: true }
},
header: {
backgroundColor: "#fafafa"
},
table: myList.[{
recordId: Nr,
styles: {
backgroundColor: "#f4f4f5"
},
columns: [{
value: Name,
width: "fraction",
styles: {
fontWeight: "600"
}
}]
}]
```
### footer
Mit dem `**footer**`-Block kannst du einen **unteren Bereich** unterhalb der Tabelle anzeigen lassen – z. B. für Buttons oder Informationen. Er eignet sich ideal für **Aktionsflächen** wie „Neuen Eintrag anlegen“ oder zur Anzeige von Summen, Filtern oder Statusinfos.
- `**showFooter**`: `true` oder `false` – aktiviert oder deaktiviert den Footer.
- `**height**`: Höhe des Footers (z. B. `"48px"`). Ohne Angabe gilt die Header-Höhe, Standard `36px`.
- `**showActionButton**`: Zeigt den Standard-Button *„Neuen Datensatz erstellen“* an.
- `**actionButtonTitle**`: Individueller Text für den Action-Button.
- `**backgroundColor**`: Hintergrundfarbe des Footers.
- `**leftSideContent**`: Freier Inhalt (z. B. Text, HTML, Icons) für die linke Seite.
- `**rightSideContent**`: Inhalt für die rechte Seite – z. B. Statusanzeigen, Summen oder Infos.
**Standardverhalten:**
Ohne `footer`-Block wird kein Footer angezeigt. Wird `showFooter: true` gesetzt, aber keine weiteren Optionen, erscheint ein leerer Footerbereich.
```javascript
footer: {
showFooter: true,
showActionButton: true,
backgroundColor: "",
actionButtonTitle: "Neue Aufgabe hinzufügen",
leftSideContent: "",
rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben"
}
```
So sieht der oben definierte Footer aus:

## table
Im **Detail-Bereich **`**table**` definierst du alle **Spalten (columns)** und deren **Darstellung, Inhalte und Verhalten**. Du legst hier fest, **welche Datenfelder angezeigt werden sollen**, wie sie aussehen und was beim Klicken passieren soll.
- Jede Spalte kann **individuell formatiert** werden (z. B. Farbe, Ausrichtung, Padding, Aktionen).
- Die hier gesetzten Werte **überschreiben** ggf. globale Einstellungen aus dem `settings`-Block (z. B. `fontColor`, `backgroundColor`, `align` etc.).
- Du kannst hier auch **fixierte Spalten**, **Gruppierungen**, **Interaktionen** und **Inline-Editing** steuern.
## Zeilen- und Gruppenparameter
```javascript
projectList.[{
recordId: Nr,
rowColor: "",
rowHeight: "auto",
rowPaddingY: "10px",
groupRowColor: "",
groupRowSettings: {
height: "",
},
```
- `**recordId**`:
Muss die `**Nr**` des Datensatzes sein (also die interne ID in Ninox).
*Wichtig für die Zuordnung von Aktionen, z. B. Öffnen, Bearbeiten, Löschen.*
- `**rowColor**`:
Setzt die **Hintergrundfarbe** der jeweiligen Zeile.
*Akzeptiert HEX-Werte wie *`*"#f4f6ff"*`* oder *`*"transparent"*`*.*
- `**rowHeight**`:
Bestimmt die **Höhe der Zeile**.
*Wert kann *`*"auto"*`* sein oder eine feste Pixelangabe wie *`*"60px"*`*.*
- `**rowPaddingY**`
→ Vertikales **Innenabstands-Polster** (top/bottom) innerhalb der Zellen dieser Zeile.
Wird auf alle Zellen angewendet, **sofern dort kein individuelles Padding gesetzt ist**.
- `**groupRowColor**`:
Setzt die Hintergrundfarbe für eine **Gruppen-Zeile** (wenn `groupedBy` aktiv ist).
*Du kannst entweder HEX-Werte verwenden oder dynamisch auf Felder zugreifen (z. B. *`*row.farbe*`*).*
- `**groupRowSettings.height**`
→ Legt die **Höhe der Gruppenzeile** fest.
Wert z. B. `"40px"` oder `"auto"`. Wirkt nur, wenn die Zeile eine **Gruppenzeile** ist.
- `**styles**`: CSS-String für freies Styling – wird auf jede Zelle der Zeile angewendet. Langfristiger Ersatz für `rowColor` und `rowHeight`.
💡 **Pro-Tipp:**
Du kannst **dynamisch berechnete Farben** einsetzen, um Status-Feedback direkt sichtbar zu machen – z. B. grün für „alle erledigt“, grau für „in Arbeit“:
```javascript
groupRowColor: if cnt(Firma.Projekte) = cnt(Firma.Projekte[Abgeschlossen != null]) then
"#F0FFF1"
else
"#eee"
end,
```
## Nested rows (`items[]`)
Hierarchical tables use the **same row shape** as flat `table[]`, plus optional nested children on each row.
### Top-level input (either/or)
| Form | Use case |
|------|----------|
| **`table: [...]`** | Classic — existing projects, flat + nested |
| **`items: [...]`** | Greenfield — same row objects, Timeline-style root |
Do **not** set both `table` and root `items` at once. If both are present, `table` wins (dev warning).
Each row may contain **`items: [...]`** for children (unlimited depth). Collapse/expand is **client-side** (persisted in `localStorage` per `uniqueListId`).
### Example — `table[]` with nested children
```javascript
arcCustomTable({
uniqueListId: "tasks-" + Nr,
header: {
columns: [
{ title: " ", width: "40px" },
{ title: "Aufgabe", width: "fraction" },
{ title: "Status", width: "120px" }
]
},
collapsible: {
enabled: true,
expandColumn: 0,
indentColumn: 1,
indentSize: "16px"
},
table: [{
id: "p1",
recordId: Nr,
columns: [
{ value: "" },
{ value: Bezeichnung },
{ value: Status }
],
items: Unteraufgaben.[{
id: format(number(Nr), "0000"),
recordId: Nr,
columns: [ /* same column layout */ ],
items: /* deeper levels */
}]
}]
})
```
### Example — root `items[]`
Same row objects; replace `table:` with `items:` at the root.
### `collapsible`
| Property | Meaning |
|----------|---------|
| `enabled` | `true`: parent rows with children show expand toggle |
| `defaultCollapsed` | `true`: alle Elternzeilen starten zugeklappt, auch solche, die später dazukommen. Auf- und Zuklappen bleibt gespeichert |
| `expandColumn` | Column index for toggle (default `0`) |
| `indentColumn` | Column index for depth padding (default `1`) |
| `indentSize` | Padding per level, e.g. `"16px"` |
| `iconSize` | Default caret size when no custom icons — e.g. `"16px"` (default) |
| `iconExpand` | Optionaler verschachtelter Widget-Descriptor (z. B. `arcCustomIcon`), solange die Zeile **eingeklappt** ist, ein Klick klappt also auf — ersetzt das Standard-Caret |
| `iconCollapse` | Optionaler verschachtelter Widget-Descriptor, solange die Zeile **aufgeklappt** ist, ein Klick klappt also zu — ersetzt das Standard-Caret |
**Not combinable with `groupedBy`** in the same table config.
Client-side sort is disabled when nested rows are present — order comes from Ninox (`table` / `items` structure).
### `span` / `colspan` on cells
All rows share the **same number of header columns**. To show a parent row with one wide cell across multiple columns, use **`span`** (alias **`colspan`**) on the cell:
```javascript
// Header: 4 columns (Expand | Name | Status | Date)
columns: [
{ value: "" },
{ value: "Team Nord — 3 members", span: 3 } // 1 + 3 = 4
]
// Child row: four normal cells (span defaults to 1)
columns: [
{ value: "" },
{ value: "Anna M." },
{ value: badgeWidget },
{ value: "14.06.2026" }
]
```
**Invalid:** parent row with 2 `columns` entries and child row with 4 entries **without** `span` — columns will misalign.
Optional **`width`** / **`minWidth`** on a cell still override min-width on that cell; grid tracks come from `header.columns`.
## columns
Im `columns`-Array definierst du die **einzelnen Spalten deiner Tabelle** – also wie sie aussehen, heißen und was darin angezeigt wird. Jede Spalte wird über ein eigenes Objekt innerhalb von `columns` beschrieben.
**Basics, die du einstellen kannst:**
- `**field**`: Das Datenfeld, das angezeigt werden soll – muss zu den Werten in `data.table.columns` passen.
- `**title**`: Der sichtbare Name der Spalte im Header.
- `**width**`: Die Breite der Spalte — fix z. B. `"200px"` / `"10%"`. Für flexible Spalte: `"fraction"`, `"auto"` oder `""` (leer) — gleiche Grid-Logik (`minmax(30px, 1fr)` bzw. gemischt mit fixen Spalten). Sind alle Spalten fix und zusammen schmaler als die Tabelle, übernimmt die letzte Spalte den Rest (wie `"auto"`).
- `**align**`: Ausrichtung des Inhalts – `"left"`, `"center"` oder `"right"`.
- `**backgroundColor**`** / **`**color**`: Hintergrund- und Textfarbe der Spalte.
- `**paddingX**`** / **`**paddingY**`: Innenabstände horizontal und vertikal.
- `**truncate**`: Ob der Text abgeschnitten und mit „…“ versehen werden soll.
- `**fixed**`: `"left"` oder `"right"`, um eine Spalte zu fixieren.
- `**styles**`: CSS-String für freies Styling der Zelle (Header-Cell oder Body-Cell). Überschreibt alle anderen Style-Props.
- `**actions**`: Aktionen wie `update`, `popup`, `change`, `openFullscreen`, `delete`.
```javascript
columns: [{
field: "",
title: "",
value: "",
width: "",
align: "",
fixed: "",
paddingX: "",
paddingY: "",
groupExpandAction: "",
groupValue: "",
actions: [{
recordId: "",
type: "",
field: "",
value: ""
}]
}
```
### Gruppen ausklappen mit groupExpandAction
Mit ***groupExpandAction*** kannst du bestimmen, bei welchen Spalten die Funktion des Ausklappens aktiviert sein soll. Das funktioniert nur, wenn du eine Gruppierung in deiner Tabelle eingestellt hast. Aktivierst du es bei mindestens einer Spalte (mit groupExpandAction:true), ist es bei den anderen Spalten automatisch deaktiviert, sofern du es dort nicht auch mit aktivierst in den Parametern.
```javascript
groupExpandAction: true, // Aktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte.
groupExpandAction: false, // Deaktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte.
groupExpandAction: "", // default: true
```
### actions
Mit dem Parameter `**actions**` kannst du festlegen, **was passieren soll, wenn ein Nutzer auf eine Zelle klickt oder diese bearbeitet**. Aktionen werden auf **Zellebene innerhalb von **`**columns**` (in `data.table`) gesetzt und machen deine Tabelle interaktiv.
**💡 Hinweis:** Du kannst **mehrere Aktionen kombinieren**, indem du sie als Array einfügst.
Beispiel:
```javascript
actions: [{
recordId: Nr,
type: "popup"
}, {
recordId: Nr,
type: "change",
field: "A"
}],
```
#### Aktion: popup
Mit `**type: "popup"**` definierst du eine Aktion, die beim Klick auf die Zelle den verknüpften **Datensatz als Popup** öffnet – direkt innerhalb von Ninox. Dadurch können Nutzer Details einsehen oder bearbeiten, ohne die aktuelle Ansicht zu verlassen.
- `**recordId**` muss die `Nr` des jeweiligen Datensatzes sein.
- Diese Aktion gilt **immer für die ganze Zelle**, nicht nur für den Text.
- Du kannst mit `showPopupButton: true` zusätzlich einen Button anzeigen lassen, der den Popup explizit auslöst – nützlich, wenn du Editieren und Öffnen visuell trennen möchtest.
```javascript
actions: [{
recordId: Nr,
type: "popup", // Text. Muss genau so geschrieben werden.
showPopupButton: true, // true oder false zum Ein- oder Ausblenden des Open-Buttons
popupButton: arcCustomButton({
uniqueId: "ButtonProjekt" + Nr,
icon: "",
title: "Open",
fontSize: "13px",
fontColor: "",
iconColor: "",
backgroundColor: "",
borderColor: ""
})
}]
```
Statt die gesamte Zelle klickbar zu machen, kannst du auch einen **individuellen Button innerhalb der Zelle anzeigen lassen** – z. B. mit dem Widget `arcCustomButton`.
Dazu verwendest du:
- `**showPopupButton: true**` – aktiviert die Schaltfläche.
- `**popupButton**` – enthält das gerenderte Button-Element, z. B. per `arcCustomButton()`.
#### Aktion: delete
Mit `**type: "delete"**` kannst du beim Klick auf eine Zelle den zugehörigen Datensatz **löschen**. Die Aktion wirkt sich – wie bei `popup` – **auf die gesamte Zelle aus**.
- `**recordId**` muss auf die `Nr` des Datensatzes zeigen, der gelöscht werden soll.
- Es wird kein zusätzlicher Bestätigungsdialog eingebaut – das Löschen erfolgt direkt.
💡 **Hinweis:** Diese Aktion sollte **nur mit Bedacht** und **klar visualisiert** eingesetzt werden – z. B. durch eine spezielle Icon-Spalte oder einen rot markierten Button innerhalb der Zelle.
```javascript
actions: [{
recordId: Nr,
type: "delete" // Text. Muss genau so geschrieben werden.
}]
```
#### Aktion: update
Mit `**type: "update"**` kannst du beim Klick auf eine Zelle direkt ein **Feld des verknüpften Datensatzes ändern** – ohne Popup, ohne Edit-Modus.
- `**recordId**`: Die `Nr` des Datensatzes, der verändert werden soll.
- `**field**`: Das Feld, das aktualisiert wird (als Text).
- `**value**`: Der neue Wert, der gesetzt werden soll (Text, Zahl, Bool, etc.).
```javascript
actions: [{
recordId: Nr,
type: "update",
field: "G", // Gibt die genaue Field ID des Feldes an, auf das die Aktion angewendet wird.
value: if erledigt=true then null else true end, // Gibt den Wert an, der bei dem referenzierten Feld eingesetzt werden soll.
}]
```
💡 **Pro-Tipp:**
Wenn du das **Löschen nicht direkt**, sondern erst **nach einer Sicherheitsabfrage** durchführen möchtest, kannst du einen kleinen Umweg über ein **Hilfsfeld + Trigger** nehmen:
- Erzeuge ein **Ja/Nein-Feld** namens z. B. `trigger_delete`.
- Füge in der Tabelle eine `update`-Aktion ein, die `trigger_delete` auf `true` setzt.
- Im Trigger dieses Feldes nutzt du folgenden Dialog:
```javascript
if dialog("Eintrag Löschen", "Soll der Eintrag wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then
delete this
end;
trigger_delete := false
```
#### Aktion: openFullscreen
Mit `**type: "openFullscreen"**` öffnest du den verknüpften Datensatz direkt im **Vollbildmodus** – also so, als würde man ihn klassisch in Ninox vollständig aufrufen.
- `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll.
**Einsatzszenario:**
Wenn du komplexe Datensätze mit vielen Tabs oder Untertabellen hast, ist `openFullscreen` die bessere Wahl gegenüber `popup`.
```javascript
actions: [{
recordId: Nr,
type: "openFullscreen", // Text. Muss genau so geschrieben werden.
}]
```
#### Aktion: openRecord
Mit `**type: "**openRecord**"**` öffnest du das Formular des gegebenen Datensatzes zusammen mit der entsprechenden Tabelle.
- `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll.
```javascript
actions: [{
recordId: Nr,
type: "openRecord",
showPopupButton: true
}]
```
### groupValue
Mit `**groupValue**` definierst du den **Inhalt der Gruppierungszeile** in einer bestimmten Spalte – also das, was in der Zeile angezeigt wird, die z. B. mehrere Datensätze zu einem Projekt, Kunden oder Status bündelt.
- Du kannst `groupValue` **in jeder Spalte** verwenden – auch mehrfach pro Gruppe, wenn gewünscht.
- Du kannst einfachen **Text**, HTML oder auch **Mini-Widgets** einbauen (z. B. Buttons, Layouts oder Statusanzeigen).
- Die Gruppierungszeile wird automatisch angezeigt, wenn `groupedBy` im `settings`-Block gesetzt ist.
```javascript
groupValue: Projekte.Bezeichnung // Ninox-Felder & -Schreibweisen möglich
groupValue: "Projekt:" + Projekte.Bezeichnung // Text + Ninox-Feld
groupValue: arcCustomProgressBar({
uniqueId: "",
width: "",
fontSize: "",
fontColor: "",
backgroundColor: "",
progressColor: "",
valueTotal: "",
valueProgress: "",
valueText: ""
}) // Andere Mini-Widgets
```

### groupByValue
Der `**groupByValue**`-Parameter erlaubt es, deine Tabelle nach **einem anderen Wert als dem angezeigten **`**value**` zu gruppieren. So kannst du die Anzeige der Zelle unabhängig von der Logik der Gruppierung steuern.
- `**value**`: Was der Nutzer in der Zelle sieht (z. B. ein Icon, ein Label, ein HTML-Element).
- `**groupByValue**`: Nach diesem Wert wird die Zeile gruppiert.
**Fallback:**
Wenn `**value**` oder `**groupByValue**` leer ist, wird die Zeile automatisch der Gruppe `**arc-no-value**` zugeordnet.
Diese Gruppe wird ganz normal dargestellt, kann aber auch gezielt angesprochen oder ausgeblendet werden.
```javascript
groupByValue: Projekte.Nr //
groupByValue: Typ // Ninox-Felder
groupByValue: "" // default: es wird nach value gruppiert
```
## Beispiele
Folgend findest du einige Praxisbeispiele, die dir den unterschiedlichen Einsatz der Custom Table veranschaulichen.
### Custom Table Simple

```javascript
let current := this;
let projectList := (select Projekte);
let data := {
uniqueListId: "Projektliste Offen",
tableId: tableId(first(projectList)),
height: "auto",
groupedBy: "",
popupButtonTitle: "Open",
table: projectList.[{
recordId: Nr,
rowColor: "",
groupRowColor: "#eee",
columns: [{
field: "Feldname für Feld-ID",
title: "Titel",
value: "Ninox Wert",
width: "10%"
}, {
field: "Feldname für Feld-ID",
title: "Titel",
value: "Ninox Wert",
width: "15%"
}, {
field: "Feldname für Feld-ID",
title: "Titel",
value: "Ninox Wert",
width: "10%"
}, {
field: "Firma.Name",
title: "Firma",
value: Firma.Name,
width: "10%"
}, {
field: "Bezeichnung",
title: "Projekt",
value: Bezeichnung,
width: "15%",
actions: [{
type: "popup",
recordId: Nr
}, {
type: "change",
recordId: Nr,
field: "A"
}]
}, {
field: "Umsatz",
title: "Umsatz",
value: text(Umsatz + 1000),
width: "20%"
}, {
field: "Aufgaben",
title: "Status Aufgaben",
value: "
",
width: "100px"
}, {
field: "Abgeschlossen",
title: "Abgeschlossen am",
value: text(Abgeschlossen),
width: "100px"
}]
}],
footer: {
showFooter: false,
actionButtonTitle: "",
rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte"
}
};
arcCustomTable(data)
```
### Custom Table Complex

```javascript
let current := this;
arcCustomTable(data);
let list := do as transaction
select Aufgaben
end;
let filteredList := list[if current.Suche != null then
testx(Mitarbeiter.'First Name' + "," + text(Mitarbeiter.'Last Name') + "," +
text(Aufgabe) +
"," +
text(Projekte.Bezeichnung) +
"," +
", "(?:" + current.Suche + ")\.*[^]", "gi")
else
true
end and
if current.'Erledigte einblenden' = true then
true
else
Erledigt != true
end];
let data := {
uniqueListId: "Projektliste A",
tableId: tableId(first(list)),
height: "500px",
theme: "",
groupedBy: text('Gruppieren nach'),
embedded: false,
table: filteredList.[{
recordId: Nr,
rowColor: "",
rowHeight: "60px",
groupRowColor: let currentRecord := this;
if cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung and Erledigt = true]) = cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung]) then
"#dcf2de"
else
"#eee"
end,
columns: [{
field: "Erledigt",
title: arcCheckBox({
uniqueId: "checkbox all check",
value: Erledigt,
embedded: true,
clickAction: {
recordId: Nr,
fieldId: "G",
value: if cnt(list[Erledigt = true]) != cnt(list) then
false
else
if cnt(list[Erledigt = null]) = cnt(list) then
null
else
if cnt(list[Erledigt = true]) = cnt(list) then
true
end
end
end
}
}),
value: arcCheckBox({
uniqueId: "checkbox single check",
value: Erledigt,
embedded: true,
clickAction: {
recordId: Nr,
fieldId: "G",
value: if Erledigt = true then null else true end
}
}),
width: "100px",
align: "center",
fixed: "left",
groupValue: arcCustomIcon({
name: "caret-down"
})
}, {
field: "Bild",
title: "Logo",
width: "150px",
value:",
actions: [{
recordId: Projekte.Nr,
type: "popup"
}],
groupValue: "",
}, {
field: "Projekt",
title: "Projekt",
width: "",
value: Projekte.Bezeichnung,
actions: [{
recordId: Projekte.Nr,
type: "popup"
}],
groupValue: Projekte.Bezeichnung,
}, {
field: "Aufgaben",
title: "Aufgabe",
value: Aufgabe,
align: "left",
width: "200px",
actions: [{
recordId: Nr,
type: "popup"
}, {
recordId: Nr,
type: "change",
field: "A"
}],
groupValue: if current.'Gruppieren nach' = 3 then
html(---
**{ Aufgabe }**
---)
end
}, {
field: "Mitarbeiter",
title: "Mitarbeiter",
value:",
actions: [{
recordId: Mitarbeiter.Nr,
type: "popup"
}]
}, {
field: "delete",
title: "",
width: "40px",
align: "center",
value: "",
actions: [{
recordId: Nr,
type: "delete"
}]
}]
}],
footer: {
showFooter: true,
showActionButton: true,
actionButtonTitle: "Neue Aufgabe hinzufügen",
rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben"
}
};
arcCustomTable(data)
```
## Vue
Das `data`-Objekt wird direkt in die Vue-Komponente als Prop übergeben.
```vue
Feld
Typ
Beschreibung
`label`
`text`
Beschriftung (Text) im Upload-Feld. Wird in der Mitte angezeigt. Wird unter dem Icon angezeigt.
`value`
`widget` `text`
Mit value kannst du dein ganz eigenes Design des Uploads gestalten. Alles, was du bei value angibst, überschriebt das bestehende Standard-Design. Hier kannst du beispielsweise einen einfachen [Custom Button](/documentation/buttons) verwenden oder ein [eigenes Custom Layout](/documentation/custom-layout) gestalten, das sich deiner UI anpasst. Dieser Parameter ist optional und muss natürlich nicht befüllt werden.
`height`
`text`
Höhe des Upload-Feldes, z. B. `"80px"` oder `"100%"`.
`width`
`text`
Breite des Upload-Feldes, z. B. `"100%"`, `"300px"` oder `"auto"`.
`icon`
`widget`
Mit `[arcCustomIcon](https://www.arc-rider.de/documentation/custom-icon)` kann ein individuelles Icon bestimmt werden.
Feld
Typ
Beschreibung
`tableId`
`text`
Ziel-Tabelle, in der das Bild als Datensatz gespeichert wird.
`fieldId`
`text`
Feld-ID des Textfelds, in das der Base64-Bildinhalt geschrieben wird.
`widthFieldId`
`text`
*(optional)* Feld-ID für die gespeicherte Bildbreite (in Pixeln). Erstelle dafür ein Ninox-Zahlenfeld.
`heightFieldId`
`text`
*(optional)* Feld-ID für die gespeicherte Bildhöhe (in Pixeln). Erstelle dafür ein Ninox-Zahlenfeld.
`width` / `height`
`text`
*(optional)* Die width / height innerhalb des image{} Blocks gibt die Weite / Höhe deines Bildes in Pixelbreite an, in der du es speichern möchtest.
`format`
`text`
*(optional)* Ausgabeformat nach dem Upload (offline im Browser). `"jpeg"` / `"jpg"`, `"webp"` oder `"original"` (Default: weglassen / `"original"` = bisheriges Verhalten, MIME vom Eingang). Bei `"jpeg"`/`"webp"` wird das Bild immer neu encodiert — auch wenn es schon kleiner als `width`/`height` ist. Der Dateiname erhält die passende Extension (`.jpg` / `.webp`).
`quality`
`number`
*(optional)* Kompressionsqualität von `0` bis `1` (nur bei `format: "jpeg"` oder `"webp"`). Default `0.8`, wenn `format` gesetzt und `quality` fehlt. Gilt **nicht** bei `"original"`.
Feld
Typ
Beschreibung
`tableId`
`text`
Ziel-Tabelle, in der der Datensatz gespeichert wird
`fieldId`
`text`
Feld-ID, in das der Dateiname geschrieben wird
Feld
Typ
Beschreibung
`fieldId`
`text`
Das Feld im neuen Datensatz, das gesetzt werden soll
`value`
`any`
Der Wert, der in das Feld geschrieben wird
Feld
Typ
Beschreibung
`recordId`
`text`
Der ID-Wert des bestehenden Datensatzes, in dem verlinkt werden soll
`fieldId`
`text`
Das Referenzfeld, das mit dem neuen Upload-Datensatz gefüllt werden soll
Parameter
Typ
Beschreibung
`showBadge`
*boolean*
Legt fest, ob ein Badge angezeigt wird (`true` / `false`). Kann auch per Skript bestimmt werden: `if cnt(Dokumente)>0 then true end `
`badgeTitle`
*text*
Text oder Zahl im Badge
`badgeColor`
*text*
Textfarbe im Badge (z. B. `"#ffffff"`)
`badgeBackground`
*text*
Hintergrundfarbe des Badges (z. B. `"#e9595d"`)
`badgeBorderColor`
*text*
Rahmenfarbe des Badges (z. B. `"#ffffff"`)
`badgePosition`
*text*
Zusätzliche Positionierung des Badges *(z. B. *`*"outside"*`* für Darstellung außerhalb des Buttons)*
`badgeGap`
*text*
Abstand zwischen Button-Inhalt und Badge in der Standard-Position (Standard: `"6px"`). Wird bei `badgePosition: "outside"` nicht angewendet.
Feld
Typ
Beschreibung
Beispielwert
`fontColor`
*text*
Textfarbe beim Hover
`"#ffffff"`
`backgroundColor`
*text*
Hintergrundfarbe beim Hover
`"#004C8A"`
`borderColor`
*text*
Rahmenfarbe beim Hover
`"#003366"`
`iconColor`
*text*
Farbe des Icons beim Hover (nur bei SVG-Icons relevant)
`"#ffffff"`
`animation`
*text*
CSS-Transition-Zeit für sanfte Effekte
`"0.25s"`
`type`
Beschreibung
Code-Beispiel
`"openUrl"`
Öffnet einen externen Link in einem neuen Tab
```javascript
{type: "openUrl",
value: "https://example.com" }
```
`"openRecord"`
Öffnet einen bestehenden Datensatz im aktuellen Fenster
```javascript
{ type: "openRecord",
recordId: first(select Dashboard).Nr}
```
`"openFullscreen"`
Öffnet einen Datensatz im Vollbild-Modus
```javascript
{ type: "openFullscreen",
recordId: Nr,
tab: "Details" }
```
`"popup"`
Öffnet einen Datensatz als Popup
```javascript
{ type: "popup",
recordId: Nr,
tab: "Info" }
```
`"closeRecord"`
Schließt den aktuellen Datensatz
```javascript
{ type: "closeRecord",
recordId: Nr }
```
`"closeFullscreen"`
Schließt den Vollbildmodus
```javascript
{ type: "closeFullscreen",
recordId: Nr }
```
`"closeAllRecords"`
Schließt alle geöffneten Datensätze
```javascript
{ type: "closeAllRecords" }
```
`"delete"`
Löscht den angegebenen Datensatz
```javascript
{ type: "delete",
recordId: Nr }
```
`"update"`
Aktualisiert ein Feld in einem Datensatz
```javascript
{ type: "update",
recordId: Auftrag.Nr,
field: "A1",
value: "Erledigt" }
```
`"create"`
Erstellt einen neuen Datensatz mit optionalem Popup
```javascript
{ type: "create",
tableId: "A",
popup: true,
changeFieldValues: [{ fieldId: "AZ",
value: "Neu" }] }
```
`"customJS"`
Führt benutzerdefinierten JavaScript-Code aus
```javascript
{ type: "customJS",
value: "alert('Hallo Welt!');" }
```
Parameter
Typ
Beschreibung
`showBadge`
*boolean*
Legt fest, ob ein Badge angezeigt wird (`true` / `false`). Kann auch per Skript bestimmt werden: `if cnt(Dokumente)>0 then true end `
`badgeTitle`
*text*
Text oder Zahl im Badge
`badgeColor`
*text*
Textfarbe im Badge (z. B. `"#ffffff"`)
`badgeBackground`
*text*
Hintergrundfarbe des Badges (z. B. `"#e9595d"`)
`badgeBorderColor`
*text*
Rahmenfarbe des Badges (z. B. `"#ffffff"`)
`badgePosition`
*text*
Zusätzliche Positionierung des Badges *(z. B. *`*"outside"*`* für Darstellung außerhalb des Buttons)*
`badgeGap`
*text*
Abstand zwischen Button-Inhalt und Badge in der Standard-Position (Standard: `"6px"`). Wird bei `badgePosition: "outside"` nicht angewendet.
Feld
Typ
Beschreibung
Beispielwert
`fontColor`
*text*
Textfarbe beim Hover
`"#ffffff"`
`backgroundColor`
*text*
Hintergrundfarbe beim Hover
`"#004C8A"`
`borderColor`
*text*
Rahmenfarbe beim Hover
`"#003366"`
`iconColor`
*text*
Farbe des Icons beim Hover (nur bei SVG-Icons relevant)
`"#ffffff"`
`animation`
*text*
CSS-Transition-Zeit für sanfte Effekte
`"0.25s"`
`type`
Beschreibung
Code-Beispiel
`"openUrl"`
Öffnet einen externen Link in einem neuen Tab
```javascript
{type: "openUrl",
value: "https://example.com" }
```
`"openRecord"`
Öffnet einen bestehenden Datensatz im aktuellen Fenster
```javascript
{ type: "openRecord",
recordId: first(select Dashboard).Nr}
```
`"openFullscreen"`
Öffnet einen Datensatz im Vollbild-Modus
```javascript
{ type: "openFullscreen",
recordId: Nr,
tab: "Details" }
```
`"popup"`
Öffnet einen Datensatz als Popup
```javascript
{ type: "popup",
recordId: Nr,
tab: "Info" }
```
`"closeRecord"`
Schließt den aktuellen Datensatz
```javascript
{ type: "closeRecord",
recordId: Nr }
```
`"closeFullscreen"`
Schließt den Vollbildmodus
```javascript
{ type: "closeFullscreen",
recordId: Nr }
```
`"closeAllRecords"`
Schließt alle geöffneten Datensätze
```javascript
{ type: "closeAllRecords" }
```
`"delete"`
Löscht den angegebenen Datensatz
```javascript
{ type: "delete",
recordId: Nr }
```
`"update"`
Aktualisiert ein Feld in einem Datensatz
```javascript
{ type: "update",
recordId: Auftrag.Nr,
field: "A1",
value: "Erledigt" }
```
`"create"`
Erstellt einen neuen Datensatz mit optionalem Popup
```javascript
{ type: "create",
tableId: "A",
popup: true,
changeFieldValues: [{ fieldId: "AZ",
value: "Neu" }] }
```
`"customJS"`
Führt benutzerdefinierten JavaScript-Code aus
```javascript
{ type: "customJS",
value: "alert('Hallo Welt!');" }
```
Paramter
**Beschreibung**
visible
Zeigt die Legende an (`true`) oder blendet sie aus (`false`).
position
Platzierung: `"left"`, `"right"`, `"top"` oder `"bottom"`
label
Was soll in der Legende stehen? Ninox Felder referenzieren oder eigene Textbezeichnungen wählen.
fontSize
Schriftgröße der Labels, z. B. `"12px"`
fontWeight
Schriftstärke: `"normal"`, `"bold"`, `"500"` usw.
fontColor
Farbe des Legendentextes, z. B. `"#333"`
gap
Abstand zwischen den Einträgen, z. B. `"6px"`
Paramter
**Beschreibung**
title
Haupttext in der Mitte (z. B. „Gesamt“)
subtitle
Untertitel direkt darunter (z. B. eine Zahl oder Status)
fontSize
Größe beider Texte (gilt für beide), z. B. `"18px"`
fontColor
Farbe der Schrift, z. B. `"#444"`
`-Element: Der Browser dekodiert es abseits des Hauptthreads, lädt es bei Bedarf und liest `alt` für Screenreader vor.
- Ein Bild, das nicht lädt, zeigt den Platzhalter statt des Browser-Symbols für defekte Bilder. Die Box behält die gesetzte Größe; ohne Größe ist der Platzhalter `48px` groß.
- `initials` ersetzen das Platzhalter-Icon nur, solange es kein Bild gibt oder es nicht geladen werden konnte. Während des Ladens bleibt die Box ruhig grau. Die Schriftgröße folgt der Box; mit `--arc-image-initials-size` (z. B. `14px`) auf dem Image oder einem Eltern-Element lässt sie sich festlegen.
- Die Platzhalter-Farben folgen den CSS-Variablen `--arc-image-placeholder-bg`, `--arc-image-placeholder-color` und `--arc-image-placeholder-size`. Auf einem Eltern-Element gesetzt, gelten sie für alle Bilder darin.
- Das Image gehört in Inhalts-Slots wie `blocks[].value` oder Tabellenzellen. Einen eigenen Klick-Callback hat es nicht.
## Datentypen
Die folgenden widget-bezogenen Typen exportiert das Paket. Gemeinsame Typen wie `ArcReactWidgetProps` sind ebenfalls verfügbar. Konfigurationsobjekte sind geschlossen, ein falsch geschriebener oder nicht unterstützter Schlüssel ist also ein Typfehler.
```typescript
export interface ArcWidgetImageBorder {
color?: string;
width?: string;
style?: 'solid' | 'dashed' | 'dotted' | 'double';
radius?: string;
sides?: Array<'top' | 'right' | 'bottom' | 'left'>;
}
export interface ArcWidgetImageData {
value?: string;
alt?: string;
title?: string;
width?: string;
height?: string;
aspectRatio?: string;
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
objectPosition?: string;
backgroundColor?: string;
initials?: string;
fontColor?: string;
border?: ArcWidgetImageBorder;
loading?: 'lazy' | 'eager';
styles?: string;
}
export interface ArcWidgetImageProps extends ArcReactWidgetProps
Parameter
Typ
Beschreibung
`uniqueId`
Text
Eindeutige ID für das Pagination-Element. Pflichtfeld.
`editable`
Boolean
Ermöglicht manuelle Eingabe der Seitenzahl im Input-Feld.
`recordId`
Text
ID des aktuellen Datensatzes, auf den das Update erfolgt. (Die Aktion ausgeführt wird)
`fieldId`
Text
Das Feld, das die aktuelle Seite speichert.
`direction`
Text
Layout der Pagination: `"horizontal"` (Standard) oder `"vertical"`.
`title`
Text
Der aktuell angezeigte Wert im Input-Feld.
`value`
Zahl
Aktuelle Seitenzahl – wichtig für Navigation und Bedingung von Buttons.
`total`
Zahl
Gesamtzahl an Seiten (z. B. `ceil(cnt(list) / 30)`).
`totalPrefix`
Text
Kleiner Zusatz vor der Gesamtseitenanzahl, z. B. `" von "`.
`valuePrefix`
Text
Optionaler String, der beim Update vor die Seitenzahl gesetzt wird.
`buttonFirst`
Widget
Optionaler Inhalt (Text oder Widget) für den „erste Seite“-Button.
`buttonMinus`
Widget
Optionaler Inhalt (Text oder Widget) für den „zurück“-Button.
`buttonPlus`
Widget
Optionaler Inhalt (Text oder Widget) für den „vor“-Button.
`buttonLast`
Widget
Optionaler Inhalt (Text oder Widget) für den „letzte Seite“-Button.
`Enter` bestätigt eine Auswahl, das Menü bleibt offen. - `Tab` wechselt direkt zum nächsten Feld, ohne automatisch zu bestätigen.
*!! Vorerst bleibt der bisherige Einstellungs-Parameter *`*emptyValue: "zurücksetzen"*`*noch aktiv, nutzt aber bitte in Zukunft für diese Einstellungen den Parameter *`*title: "zurücksetzen"*`* im reset Block.*```javascript emptyValue: "empty", // eigener Text emptyValue: "", // default: "(leer)") ``` ### reset Block Der `reset`-Block steuert, ob und wie im Dropdown-Menü eine **„Zurücksetzen“-Option** angezeigt wird, mit der der aktuelle Wert gelöscht oder auf einen definierten Wert zurückgesetzt werden kann. - `title` – Der angezeigte Text für den Zurücksetzen-Eintrag (z. B. `"(leer)"`, `"Zurücksetzen"`). - `value` – Der Wert, der gesetzt wird, wenn der Eintrag angeklickt wird. Wenn nicht gesetzt, wird automatisch `emptyValue` verwendet. - `hide` – Wenn `true`, wird der Reset-Eintrag **nicht angezeigt**, kann aber trotzdem intern genutzt werden. ```javascript reset: { title: "reset", value: 0, hide: true }, Einzelerklärungen: title: "reset", // eigener Text title: "", // default: Wert in Parameter emptyValue (falls emptyValue: " dann ist hier default: "(leer)") value: 0, // eigener Wert (0 = setzt den Wert auf 0 statt null zurück) value: "", // default: null (Ninox-Feld wird null gesetzt) hide: true, // select kann nicht zurückgesetzt werden hide: "", // default: false (zurücksetzen Button wird angezeigt) ``` ### focusAction Settingsblock Mit dem Settingsblock `focusAction` kannst du das Verhalten des Suchfelds im Select steuern. **Parameter:** - `autoFocus` – Bestimmt, ob der Cursor automatisch in das Suchfeld springt. Besonders für Tablets, wo die Tastatur sofort aufspringt und den User irritieren könnte, ist das Abschalten hilfreich. - `showCurrentValue` – Wenn `true`, wird der `currentValue` (z.B. deine selbst gebauten Badges via `arcCustomLayout`) links angezeigt und das Suchfeld daneben. Ideal für Multi-Select mit Tag-Darstellung. ```javascript focusAction: { autoFocus: false, // true or false // default: true showCurrentValue: true // true or false // default: false } ``` **🧩 Typische Anwendung für showCurrentValue:** Wenn du im `currentValue` eigene Badges/Tags mit `arcCustomLayout` renderst und diese beim Öffnen des Selects weiterhin sichtbar bleiben sollen (mit Suchfeld daneben), setze `showCurrentValue: true`. ### currentValue Der Parameter `currentValue` legt fest, **welcher Wert im Select-Feld bei ausgewählten items** sein soll. Er bestimmt also die **Anzeige** des Feldes im geschlossenen Zustand. **🧩 Verhalten je nach Modus** - **Single-Select (**`**multiselect: false**`**)** Der Wert wird **direkt als einzelner Text oder Zahl** übergeben (z. B. `"Offen"` oder `1`). - **Multi-Select (**`**multiselect: true**`**)** Der Wert ist ein **Array von Strings oder Zahlen**, z. B. `[Bluse, Kleidung, Oberteil]` oder `[1, 2]`. Jeder enthaltene Wert wird in der Liste als **„aktiv“** markiert. ```javascript currentValue: text(Status), // Bezeichnung deines Ninox-Feldes innerhalb von text() currentValue: arcCustomBadge({ uniqueId: "customBadge" + Nr, embedded: true, width: "100%", icon: "", iconPosition: "", fontSize: "", fontWeight: "700", borderRadius: "", singleColor: color(Status), fontColor: "", backgroundColor: "", borderColor: "", paddingY: "5px", paddingX: "10px", value: text(Status) }), // individuell gestaltetes customBadge mit der Bezeichnung deines Ninox-Feldes bei value currentValue: "", // default: es wird nicht der ausgewählte Wert angezeigt, sondern der placeholder ``` ### items Block **Pflichtfeld:** ✅ Der `items`-Block definiert die **Auswahlmöglichkeiten**, die im Dropdown angezeigt werden. Jede Option wird als Objekt mit bestimmten Eigenschaften angegeben – z. B. sichtbarer Text, interner Wert und aktiver Zustand. **🔄 **Zwei Aufbauvarianten: Du kannst das `customSelect` auf zwei verschiedene Arten mit Werten befüllen: 1. **Dynamisch aus einer Ninox-Tabelle** Ideal für Listen, die sich ändern können oder mit anderen Feldern verknüpft sind. ```javascript items: let id := Nr; (select Status_Projekte).[{ title: Titel, active: number(id.Status) = number(Nr), value: number(Nr) }] ``` 2. **Manuelle Liste im Code** Geeignet, um z. B. ein einfaches Auswahlfeld aus Ninox direkt nachzubauen, bzw. anzusteuern. (ohne externe Tabelle). ```javascript items: [{ title: "Offen", active: Status = 1, value: number(Nr) }, { title: "In Arbeit", active: Status = 2, value: number(Nr) }, { title: "Erledigt", active: Status = 3, value: number(Nr) }] ``` **🧩 Struktur pro Eintrag** - `title` – Anzeige im Dropdown (Text oder Widget, z. B. `arcCustomIcon({ name: "check", iconSize: "16px" })` neben Label-Text) - `value` – Der zu speichernde Wert - `active` – Wann wird dieser Eintrag als ausgewählt markiert 💡 **`icon`** (am Select-Root) und **`currentValue`** unterstützen ebenfalls eingebettete Mini-Widgets – dieselbe Regel wie bei `items[].title`. ## 🎹 Tastaturverhalten im Vergleich
| Taste | Single-Select Verhalten | Multi-Select Verhalten |
|---|---|---|
| **Tab** | ✅ Auswahl wird bestätigt➡️ Fokus springt weiter | ➡️ Fokus springt weiter🚫 Keine Bestätigung |
| **Enter** | ✅ Auswahl wird bestätigt🔒 Select wird geschlossen | ✅ Auswahl wird bestätigt🔓 Select bleibt geöffnet |
| **Escape** | ❌ Select wird geschlossen | ❌ Select wird geschlossen |
*Wichtig: Die uniqueId des Themes muss dieselbe sein, wie die uniqueListId der Table, in der das Theme eingesetzt wird !*## Anwendungscode ```javascript arcCustomThemeNaked({ uniqueId: "Aufgaben Demo 1" + Nr, rowSettings: { gap: "10px", borderRadius: "" }, columnSettings: { gap: "40px" } }), ``` ## Beispiel Im folgenden Beispiel siehst du den Anwendungscode des oben dargestellten Bildes. Dieser umfasst eine Custom Table, die Termine von Mitarbeitern darstellt. Hier sind sowohl Header als auch Footer ausgeblendet. Durch das Theme sind keinerlei Rahmenlinien in der Tabelle zu sehen und die einzelnen Zeilen sind durch ein gap von 10px getrennt. ```javascript let current := this; let list := (select Termine from 0 to 30); let data := { uniqueListId: "Aufgaben Demo 1" + Nr, tableId: tableId(first(list)), height: "620px", width: "100px", groupedBy: "", header: { showHeader: false }, theme: arcCustomThemeNaked({ uniqueId: "Aufgaben Demo 1" + Nr, rowSettings: { gap: "10px" }, columnSettings: { gap: "10px" }, borderRadius: "" }), embedded: false, table: list.[{ recordId: Nr, rowColor: "#ededed", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", columns: [{ field: "Kategorie Icon", title: "", width: "35px", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: arcCustomIcon({ iconSet: "phosphor", name: "circle-fill", iconSize: "25px", color: record('Kategorie Aufgabe',number(Kategorie)).Farbe }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Feldname für Feld-ID", title: "", color: "", backgroundColor: "", width: "", align: "left", paddingX: "", paddingY: "", value: arcCustomLayout({ comment: "---------------------------------- LAYOUT TITEL UND KATEGORIE ----------------------------------", uniqueId: "Titel und Kategorie " + Nr, embedded: true, direction: "vertical", alignX: "left", alignY: "center", width: "100%", height: "", gap: "2px", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "600", fontColor: "", value: Auftrag.last(Aufgaben.Aufgabe) }) }, { width: "", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "", fontColor: "#9aa3af", value: text(Kategorie) }) }] }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "", title: "", width: "", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: arcCustomLayout({ comment: "---------------------------------- LAYOUT UHRZEIT & DATUM ----------------------------------", uniqueId: "Datum und Uhrzeit " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "horizontal", alignX: "left", alignY: "center", width: "", height: "", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomIcon({ iconSet: "phosphor", name: "clock", iconSize: "25px", color: "#9aa3af" }) }, { width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "600", fontColor: "#9aa3af", value: 'Datum von' + ", " + Von + " - " + Bis }) }] }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Mitarbeiter", title: "", width: "", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: Mitarbeiter.'Bild-und_Name', actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }] }], footer: { showFooter: false } }; arcCustomTable(data) ``` --- # Ninox: Wie baue ich Builds ein und wie laufen Updates? Source: https://www.arc-rider.com/docs/faq/wie-man-builds-einbaut-und-updatet # Ninox: Wie baue ich Builds ein und wie laufen Updates? ## Was ist ein Build? Ein **Build** ist das fertige Code-Paket für dein Ninox-Team. Nicht jedes Widget einzeln — Universe macht aus den Widgets deines Projekts **einen** Textblock. So hängt das zusammen: 1. **Projekt** — dein Arbeitsbereich in Universe. Dort liegen Lizenz und Widgets. 2. **Build** — der daraus erzeugte Code. Den kopierst du einmal in die **globalen Funktionen** von Ninox. 3. **Anwendungscode** — die kurze Formel im Formelfeld, mit der du ein einzelnes Widget auf einer Seite anzeigst. Ohne den Build im Global Code kann Ninox die Widgets nicht laden. Den Anwendungscode brauchst du trotzdem pro Stelle, an der ein Widget erscheinen soll. Universe hält den **Standard-Build** automatisch aktuell: Neue Widgets und neue Widget-Versionen kommen von selbst hinein, auch größere Versionssprünge. Widgets, die du bewusst auf eine Version festgelegt hast, bleiben dabei unverändert. Eigene benannte Builds nutzt du im Expertenmodus, wenn du einen Stand festhalten willst — zum Beispiel vor einem Go-Live. --- ## Schritt 1: Code in Universe kopieren 1. Gehe in dein Universe-Projekt. 2. Hinterlege unter **„URL freischalten“** zuerst deine Ninox-URL oder Team-ID. Erst dann steckt die Lizenz im Code. 3. Klicke unter **„In Ninox einfügen“** bei **„Globalen Code kopieren“** auf **Kopieren**. Du musst den Code dafür nicht ansehen. Eigene Builds, Widget-Versionen und die Code-Vorschau findest du über den Schalter **Expertenmodus** oben neben „Ninox“. ## Schritt 2: In Ninox einsetzen (Erst-Setup) 1. Wechsel in deine Ninox-Datenbank im **Admin-Modus**. 2. Klicke auf die **drei Punkte** neben dem Datenbanknamen oben links. 3. Gehe auf **„Optionen“ > „Globale Funktionen“**. 4. Füge den kopierten Code am Ende des Textfeldes ein und speichere. Zusätzlich einmalig als **Trigger nach Öffnen** (Datenbankoptionen, ganz oben): ```ninox dialog("ArcRider initialization", html(arcRiderWidgetManager()), ["close"]) ``` Damit startet die Widget-Engine, sobald die Datenbank geöffnet wird. In deinem Universe-Projekt kopierst du dieses Script mit **„Start-Code kopieren“**. --- ## Wie funktionieren Updates? Nach dem ersten Einfügen hakst du in deinem Universe-Projekt **„Ja, Code ist eingefügt“** ab. Ab dann weiß Universe, welcher Stand in deinem Ninox läuft. Wenn wir Fehler beheben, neue Widget-Versionen erscheinen oder sich deine URLs ändern, erzeugt Universe einen neuen Code. In deinem Projekt erscheint dann das Banner **„Neues Update verfügbar“** mit einer Liste, welche Widgets sich geändert haben. Größere Versionssprünge sind dort als **Hauptversion** markiert; prüfe danach kurz deine Widgets. So aktualisierst du Ninox sicher: 1. Klicke im Banner auf **„Neuen Code kopieren“**. 2. Öffne die **globalen Funktionen** in Ninox. 3. Suche den alten Arc-Rider-Block. Er beginnt mit einem klaren Header (ASCII-Wortmarke und `Build ID`). 4. Ersetze diesen Block vollständig durch den neuen Code. Achte darauf, dass deine eigenen globalen Funktionen oberhalb oder unterhalb des Arc-Rider-Blocks unberührt bleiben. Zurück in Universe klickst du auf **„Ja, ich habe die neueste Version eingefügt“**. Dann verschwindet das Banner bis zum nächsten Update. Den Start-Code musst du bei Updates nicht neu einfügen. --- # React: Fehlerbehebung Source: https://www.arc-rider.com/docs/faq/react-troubleshooting # React: Fehlerbehebung ## Das Widget bleibt leer Prüfe zuerst, ob du den exportierten Komponentennamen verwendest und ob alle Optionen in `data` liegen. Die [Komponentenübersicht](/docs/react/intro/react-components) zeigt die verfügbaren Exporte. Eine Höhe von `100%` funktioniert nur in einem Elterncontainer mit berechenbarer Höhe. Beginne zum Eingrenzen mit `height: "320px"`. Bei Table braucht jede Spalte eine Breite, mindestens eine `fraction`. ## Ein Klick löst keinen Callback aus Aktiviere die Klickbarkeit an der richtigen Stelle: etwa an der Table-Zeile, einer Kanban-Karte oder einem Step. Prüfe die Callback-Signatur in der Widget-Referenz. Eine ID ist keine Callback-Position; manche Events liefern Indizes, andere Werte. ## Nach dem Speichern springt die Anzeige zurück Übernimm das bestätigte Ergebnis in React-State und leite ein neues `data`-Objekt daraus ab. Das Widget speichert nicht selbst in deiner Datenbank. Beim Kanban muss `onItemDrop` das Speicher-Promise zurückgeben. ## TypeScript erkennt einen Callback oder JSX-Slot nicht Diese Referenzen gehören zu `0.1.0-alpha.5`. Vergleiche deine installierte Version und vermeide gemischte Kopien des Pakets. Die Typen befinden sich im Paket; du benötigst kein zusätzliches Widget-Typenpaket. ## Server Rendering Die Widget-Laufzeit verwendet Browser-APIs. Importiere sie in einer Anwendung mit Server Rendering erst im Browser. Eine reine Client-Markierung reicht nicht in jedem Framework, wenn Module schon auf dem Server ausgewertet werden. Dieses vollständige Beispiel verschiebt den Laufzeit-Import in einen Effekt; der Typimport wird beim Build entfernt: ```tsx import { useEffect, useState } from 'react'; import type { ComponentType } from 'react'; import type { ArcWidgetButtonProps } from '@arcrider/arcwidgets-react'; export default function Example() { const [Button, setButton] = useState
Could not load widget.
; return Button ? ( ) : (Loading widget …
); } ``` Nutze alternativ den Mechanismus deines Frameworks, der Server Rendering für diese Komponente ausdrücklich deaktiviert. Ein stabiler Platzhalter vermeidet Layoutsprünge. ## React Strict Mode Entwicklung kann Komponenten mehrfach auf- und abbauen. Löse Speichervorgänge in Interaktions-Callbacks aus, nicht beim Rendern. Vergib stabile eigene IDs nur dort, wo du sie benötigst; dieselbe ID darf nicht gleichzeitig mehrfach im Dokument vorkommen. ## Ein Lizenzhinweis bleibt sichtbar Ein Eintrag der Domain im Portal allein konfiguriert das npm-Paket derzeit nicht. Siehe [Paket und Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung). --- # React: Wie funktionieren Paket und Lizenzierung? Source: https://www.arc-rider.com/docs/faq/react-paket-und-lizenzierung # React: Paket und Lizenzierung `@arcrider/arcwidgets-react` enthält die Komponenten, Styles, Typen und vollständige Beispiele. Die Laufzeit wird in deine Anwendung eingebunden. Diese Dokumentation beschreibt `0.1.0-alpha.6`. ## Lokale Entwicklung Installiere das Paket wie unter [React einrichten](/docs/react/intro/setup-react) beschrieben. Auf `localhost` und `127.0.0.1` erscheint kein Lizenzhinweis. Mini-Widgets benötigen auch für den produktiven Einsatz keine Premium-Freischaltung im Rahmen der Nutzungsbedingungen. ## MCP-Apps / Agent-Chat In MCP-Apps bzw. vergleichbaren Agent-Chat-Umgebungen (sandboxed iframe, zum Beispiel Claude oder ChatGPT) erscheint kein Lizenzhinweis. ## Premium-Widgets auf einer Live-Domain Eine Projektlizenz und die technische Aktivierung der Laufzeit sind zwei Schritte. Hinterlege die Produktions-Domain im Universe-Projekt und kläre die Aktivierung des npm-Pakets mit [support@arc-rider.com](mailto:support@arc-rider.com). Der aktuelle Alpha-Stand stellt noch keinen öffentlichen Initialisierungsaufruf für die Lizenz bereit und lädt die Projektfreischaltung nicht automatisch aus dem Portal. Eine gespeicherte Domain allein entfernt den Hinweis deshalb nicht zuverlässig aus einer separat installierten React-App. Verwende die für dein Projekt bereitgestellte Aktivierung; erfinde keinen API-Key-Prop oder Initialisierungsaufruf. ## Lizenzhinweis Auf einer nicht aktivierten Live-Domain kann bei Premium-Widgets ein Hinweis in der Oberfläche und Konsole erscheinen. Die Bedienelemente bleiben funktionsfähig; der Hinweis ersetzt keine Lizenz. Prüfe die technische Aktivierung auf der tatsächlichen Ziel-Domain vor der Freigabe. Die Kennzeichnung einer Umgebung als Entwicklung oder Staging im Portal wird nicht automatisch in die React-App übertragen. Für öffentlich erreichbare Test-Domains muss die technische Aktivierung ebenfalls geprüft werden. [Komponentenübersicht](/docs/react/intro/react-components) · [Preise und Projektmodell](/docs/react/faq/wie-sich-preise-zusammensetzen). --- # Was bedeutet der Lizenzhinweis (Watermark) in der Produktion? Source: https://www.arc-rider.com/docs/faq/lizenzhinweis-watermark-produktion # Lizenzhinweis in React Premium-Widgets können auf einer nicht aktivierten Live-Domain einen Hinweis in Oberfläche und Konsole anzeigen. Mini-Widgets und lokale Entwicklung auf `localhost` oder `127.0.0.1` zeigen ihn nicht. ## Bedienbarkeit Der Hinweis sperrt keine Widget-Funktionen. Deine Anwendung bleibt für Datenzugriff und Speicherung zuständig. ## Aktivierung prüfen Das npm-Paket übernimmt Portal-Einträge derzeit nicht automatisch. Eine gespeicherte Produktions-Domain allein ist deshalb keine technische Aktivierung deiner React-App. Prüfe die konkrete Domain und die für dein Projekt bereitgestellte Freischaltung. [React-Paket und Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung) beschreibt den aktuellen Alpha-Stand und den Kontakt zur Aktivierung. # Was bedeutet der Lizenzhinweis (Watermark) in der Produktion? Wenn du Widgets in ein Projekt legst, sind die Funktionen sofort da. Du kannst Premium-Widgets (Table, Kanban, Calendar, …) vollständig testen, bevor du Ultimate kaufst. Ohne Ultimate erscheint in einer **Produktionsumgebung** ein Hinweis — unten in der Oberfläche und einmal in der Browser-Konsole. Die Widgets bleiben bedienbar.  ## Wann erscheint der Hinweis? - Du nutzt **Premium-Widgets** und das Projekt hat kein **Ultimate-Paket**. - Die App läuft auf einer **Produktions-Umgebung**, die nicht im Universe-Projekt hinterlegt ist. Bei Ninox zählt die Private-Cloud-URL, die On-Premise-URL oder die Public-Cloud-**Team-ID**. Mini-Widgets zeigen den Hinweis nicht. Auf `localhost` und in Test-Stages erscheint er ebenfalls nicht. ## Was wird blockiert? **Nichts.** Der Hinweis ist eine Lizenzanzeige: - Widgets frieren nicht ein. - Daten werden weiter geladen und gespeichert. - Bedienung und Performance bleiben erhalten. In der Konsole steht dieselbe Information hinter dem gelben Badge **Arc Widgets**. Das hilft, wenn jemand die Seite prüft und den Toast schon weggeklickt hat. ## Wie entferne ich den Hinweis? Sobald Ultimate für das Projekt aktiv ist, verschwindet er: Im Universe-Projekt den Build neu holen und den Global Code in Ninox ersetzen. Der neue Build enthält die Freischaltung. Details: [Ninox Builds](/docs/faq/wie-man-builds-einbaut-und-updatet). Preise: [Wie berechnen sich die Preise?](/docs/faq/wie-sich-preise-zusammensetzen). # Was bedeutet der Lizenzhinweis (Watermark) in der Produktion? Wenn du Widgets in ein Projekt legst, sind die Funktionen sofort da. Du kannst Premium-Widgets (Table, Kanban, Calendar, …) vollständig testen, bevor du Ultimate kaufst. Ohne Ultimate erscheint in einer **Produktionsumgebung** ein Hinweis — unten in der Oberfläche und einmal in der Browser-Konsole. Die Widgets bleiben bedienbar. Der Hinweis ist ein Toast unten in der App und dieselbe Meldung einmal in der Browser-Konsole hinter dem gelben Badge **Arc Widgets**. ## Wann erscheint der Hinweis? - Du nutzt **Premium-Widgets** und das Projekt hat kein **Ultimate-Paket**. - Die App läuft auf einer **Produktions-Umgebung**, die nicht im Universe-Projekt hinterlegt ist. Bei Vue zählt die hinterlegte Live-Domain (inkl. Subdomains). Mini-Widgets zeigen den Hinweis nicht. Auf `localhost` und in Test-Stages erscheint er ebenfalls nicht. ## Was wird blockiert? **Nichts.** Der Hinweis ist eine Lizenzanzeige: - Widgets frieren nicht ein. - Daten werden weiter geladen und gespeichert. - Bedienung und Performance bleiben erhalten. In der Konsole steht dieselbe Information hinter dem gelben Badge **Arc Widgets**. Das hilft, wenn jemand die Seite prüft und den Toast schon weggeklickt hat. ## Wie entferne ich den Hinweis? Sobald Ultimate für das Projekt aktiv ist, verschwindet er: Produktions-Domain im Universe-Projekt hinterlegen. Preise: [Wie berechnen sich die Preise?](/docs/faq/wie-sich-preise-zusammensetzen). --- # Was ist ein Widget-Partner und welche Vorteile habe ich davon? Source: https://www.arc-rider.com/docs/faq/was-ist-ein-widget-partner-und-welche-vorteile-habe-ich-davon # Was ist ein Widget-Partner und welche Vorteile habe ich davon? Partner sind Agenturen und Integratoren, die Arc Rider Widgets **in Kundenlösungen** einsetzen. Arc Rider stuft Organisationen als Partner ein. Für den Einsatz beim Endkunden gilt: - **Ein Universe-Projekt je Endkunde.** Jeder Lizenzkontext hat ein eigenes Package. Den Preis gegenüber deinem Kunden setzt du selbst. - Du trittst **im eigenen Namen** auf. Vertragspartner deiner Mandanten bist du, nicht Arc Rider. - Builds, Umgebungen und Rechnungen bleiben pro Kundenprojekt getrennt. - Lizenz, Produktions-Domains und Rechnungen bleiben pro Kundenprojekt getrennt. - Lizenz, Produktions-Domains und Rechnungen bleiben pro Kundenprojekt getrennt. Konditionen (Rabattcode, Angebot, Freischaltung) vereinbaren wir individuell. Zusätzlich gibt es das **Ambassador-Programm**: öffentlich geteilte Arc-Rider-Erfahrung bringt Credits, die du gegen Gutscheine für neue Projekte einlösen kannst. Überblick auf der [Preisseite](/pricing). Partner-Einstufung: [support@arc-rider.com](mailto:support@arc-rider.com). --- # Wie berechnen sich die Preise für Widgets? Source: https://www.arc-rider.com/docs/faq/wie-sich-preise-zusammensetzen # Wie berechnen sich die Preise für Widgets? Zwei Stufen, beide **pro Universe-Projekt** und für genau eine Plattform (Ninox oder React). | Paket | Preis (netto) | Enthalten | |---|---|---| | Free | 0 € | Alle Mini-Widgets, produktiv nutzbar | | Ultimate | 750 €, einmalig | Alle Premium-Widgets und Mini-Widgets, inklusive Widgets, die nach dem Kauf dazukommen | Ultimate ist ein Einmalkauf. Du erhältst **Lifetime-Updates**: neue Versionen, Fixes und künftige Premium-Widgets ohne weitere Zahlung. Der erzeugte Build-Code bleibt in deinem Ninox-Projekt nutzbar. Das npm-Paket und die Lifetime-Updates bleiben in deiner React-App nutzbar. Das npm-Paket und die Lifetime-Updates bleiben in deiner Vue-App nutzbar. Im Paket steckt **eine Produktions-Umgebung**. Entwicklung (`localhost`) und nicht-produktive Stages sind frei. Weitere Produktions-URLs oder Team-IDs buchst du als Add-on oder lässt sie von uns freischalten. Weitere Produktions-Domains buchst du als Add-on oder lässt sie von uns freischalten. Weitere Produktions-Domains buchst du als Add-on oder lässt sie von uns freischalten. Du kannst den gesamten Katalog vorher nutzen. Ohne Ultimate erscheint in der Produktion bei Premium-Widgets ein Lizenzhinweis — die Oberfläche bleibt bedienbar. Ninox und React sind zwei Projekte. Wer beide Plattformen produktiv ohne Hinweis nutzen will, kauft Ultimate zweimal. Aktueller Preis und Checkout: [Preisseite](/pricing). Wie Projekt und Umgebung zusammenhängen: [Buchung und Teamstruktur](/docs/faq/wie-funktioniert-die-buchung-pro-team-und-was-muss-ich-zur-teamstruktur-wissen). --- # Wie funktioniert die Buchung pro Team und was muss ich zur Teamstruktur wissen? Source: https://www.arc-rider.com/docs/faq/wie-funktioniert-die-buchung-pro-team-und-was-muss-ich-zur-teamstruktur-wissen # Wie funktioniert die Buchung pro Team und was muss ich zur Teamstruktur wissen? Du buchst **pro Universe-Projekt**. Ein Projekt gehört einer Plattform. Darin liegen Builds, Lizenz und die Produktions-Umgebungen. Darin liegen Lizenz und die Produktions-Domains. Darin liegen Lizenz und die Produktions-Domains. Ultimate enthält **eine Produktions-Umgebung**. Entwicklung und Staging sind frei. Weitere Produktions-Umgebungen sind ein Add-on. ## Ninox: was als Produktions-Umgebung zählt - **Private Cloud:** die genaue URL, zum Beispiel `https://firma.ninoxdb.de`. - **On-Premise:** eure Installations-URL, inklusive Subdomains. - **Public Cloud:** die **Team-ID**. Sie steht in der Ninox-URL nach `/teams/` oder kommt aus der Formel `teamId()`. Ohne hinterlegte Team-ID erscheint der Lizenzhinweis. Innerhalb einer hinterlegten Umgebung darfst du die Widgets in allen Datenbanken dieses Teams nutzen. ### Eigenes Team Ein Universe-Projekt, Ultimate einmal, Team-ID oder Private-Cloud-URL hinterlegen. ### Kundenprojekte Pro Endkunde ein eigenes Universe-Projekt mit eigenem Package. So bleiben Lizenz und Build getrennt. Siehe [Widget-Partner](/docs/faq/was-ist-ein-widget-partner-und-welche-vorteile-habe-ich-davon). ### Dieselbe Lösung für mehrere Mandanten Jeder produktive Lizenzkontext braucht ein eigenes Projekt — oder zusätzliche Produktions-URLs bzw. Team-IDs im selben Projekt. ## React Eine hinterlegte Domain gilt für die Domain selbst und alle Subdomains (`kunde.de` schließt `app.kunde.de` ein). Eine Produktions-Domain steckt im Ultimate-Paket. Für Kundenprojekte gilt: pro Endkunde ein eigenes Universe-Projekt. Siehe [Widget-Partner](/docs/faq/was-ist-ein-widget-partner-und-welche-vorteile-habe-ich-davon). ## Vue Eine hinterlegte Domain gilt für die Domain selbst und alle Subdomains (`kunde.de` schließt `app.kunde.de` ein). Eine Produktions-Domain steckt im Ultimate-Paket. Für Kundenprojekte gilt: pro Endkunde ein eigenes Universe-Projekt. Siehe [Widget-Partner](/docs/faq/was-ist-ein-widget-partner-und-welche-vorteile-habe-ich-davon). Preise: [Wie berechnen sich die Preise?](/docs/faq/wie-sich-preise-zusammensetzen). Fragen zur Struktur: [support@arc-rider.com](mailto:support@arc-rider.com). --- # Wie verwalte ich mehrere Umgebungen oder URLs? Source: https://www.arc-rider.com/docs/faq/mehrere-umgebungen-und-urls # Mehrere React-Umgebungen Das Ultimate-Modell gilt pro Universe-Projekt mit einer Produktions-Umgebung; weitere produktive Umgebungen können als Add-on zugeordnet werden. Für React wird die Live-Domain im Projekt hinterlegt. ## Domain und technische Freischaltung Die Domain-Zuordnung im Portal und die Aktivierung im npm-Paket müssen zusammenpassen. Das Paket liest diese Zuordnung nicht automatisch aus dem Portal. Prüfe Hauptdomain, verwendete Subdomains und öffentlich erreichbare Test-Domains mit der bereitgestellten Aktivierung. ## Entwicklung und Staging Lokale Entwicklung auf `localhost` oder `127.0.0.1` zeigt keinen Hinweis. Eine im Portal als Entwicklung gekennzeichnete öffentliche Domain wird im npm-Paket nicht automatisch als solche erkannt. [React-Paket und Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung) · [Vertragsbedingungen](/terms). # Wie verwalte ich mehrere Umgebungen oder URLs? Jedes Ultimate-Paket gilt pro Universe-Projekt und schaltet standardmäßig **eine Produktions-Umgebung** frei. ## Welche Umgebungen sind immer kostenlos? Entwicklungs- und Testumgebungen verbrauchen **kein** Kontingent: - Lokale Adressen wie `localhost`, `127.0.0.1` oder `::1`. - Nicht-produktive Staging- und Test-Umgebungen (in Universe als "dev" gekennzeichnet). ## Was zählt als eine Produktions-Umgebung? Die Lizenz-Einheit hängt vom Ninox-Hosting ab: - **Private Cloud:** die exakte Subdomain (z. B. `firma.ninoxdb.de`). - **On-Premise:** die Installations-URL des Servers (inklusive aller Subdomains). - **Public Cloud:** die **Team-ID** deines Ninox-Teams. Innerhalb dieses Teams können Widgets in beliebig vielen Datenbanken genutzt werden. ## Wie füge ich weitere Produktions-Umgebungen hinzu? Wenn die Anwendung auf mehreren getrennten Live-Systemen laufen soll: 1. **Add-ons:** zusätzliche Produktions-Umgebungen im Universe-Portal gegen eine Einmalzahlung. 2. **Freischaltung (Kulanz):** wir können zusätzliche Umgebungen im Administrationsbereich kostenlos freischalten, zum Beispiel für interne Testsysteme oder Partner-Szenarien. Mehr Details zum Lizenzmodell findest du in unseren [AGB](/terms) unter Teil III. # Wie verwalte ich mehrere Umgebungen oder URLs? Jedes Ultimate-Paket gilt pro Universe-Projekt und schaltet standardmäßig **eine Produktions-Umgebung** frei. ## Welche Umgebungen sind immer kostenlos? Entwicklungs- und Testumgebungen verbrauchen **kein** Kontingent: - Lokale Adressen wie `localhost`, `127.0.0.1` oder `::1`. - Nicht-produktive Staging- und Test-Umgebungen (in Universe als "dev" gekennzeichnet). ## Was zählt als eine Produktions-Umgebung? **Vue:** Eine hinterlegte Live-Domain. Sie deckt die Hauptdomain und **sämtliche Subdomains** ab (`firma.de` lizenziert auch `app.firma.de`). ## Wie füge ich weitere Produktions-Umgebungen hinzu? Wenn die Anwendung auf mehreren getrennten Live-Systemen laufen soll: 1. **Add-ons:** zusätzliche Produktions-Umgebungen im Universe-Portal gegen eine Einmalzahlung. 2. **Freischaltung (Kulanz):** wir können zusätzliche Umgebungen im Administrationsbereich kostenlos freischalten, zum Beispiel für interne Testsysteme oder Partner-Szenarien. Mehr Details zum Lizenzmodell findest du in unseren [AGB](/terms) unter Teil III. --- # Action Polling Source: https://www.arc-rider.com/docs/patterns/action-polling # Action Polling (`polling` auf `update`) ## AI Defaults (read first) - **`polling` nur auf `type: "update"`** — wiederholt dasselbe Feld-Update in Intervallen (typisch: Trigger-Feld). - **API-Calls gehören in den Ninox-Trigger** („Nach Änderung“), nicht ins Widget. Kein CORS-Thema. - **Stopp:** Formel-Boolean `active` (Live über Widget-Reload) **und** `maxAttempts` als Sicherheitsnetz. - **Loading** (Button/Layout) bleibt an, solange `perform` läuft — inkl. Poll-Dauer. Status-Texte: `loading.overlay.sequences` + `when:`. - **Trigger-Feld zurücksetzen** am Ende des Triggers (`false` / `null`), sonst feuert „Nach Änderung“ bei erneutem `true` oft nicht. --- ## Problem Nach einem Klick soll Ninox eine API prüfen oder ein Skript erneut ausführen, **solange** ein Job noch läuft — ohne dass der Browser die API direkt aufruft. ## Lösung `update` mit `polling`: das Widget schreibt das Trigger-Feld wiederholt; der Trigger macht `http(...)` / Logik und schreibt den Status (z. B. in ein Data-Bag-Feld). Die Formel setzt `active` aus diesem Status. ```javascript let state := parseJSON(text(helper_uiState)); let pollActive := state.jobStatus = "running"; actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "trigger_checkApi"), value: true, polling: { intervalMs: 2000, maxAttempts: 30, active: pollActive } }] ``` | Feld | Default | Bedeutung | |------|---------|-----------| | `intervalMs` | `2000` | Pause zwischen Updates (ms) | | `maxAttempts` | `30` | Max. Updates inkl. dem ersten | | `active` | `true` | Poll weiter, solange `true`; Formel-Boolean (z. B. aus Data Bag) | **Ablauf:** erstes Update → warten → wenn Live-`active` noch `true` und Attempts nicht erreicht → erneut Update → … → Stopp → Loading aus. ## Live-`active` (Button / Layout) Bei Formel-Reload nach dem Trigger rufen Button und Layout `syncPollingGates` auf. So sieht die laufende Poll-Schleife den neuen Boolean — nicht nur den Snapshot vom Klick. Ohne Sync (andere Widgets): Snapshot + `maxAttempts` gelten weiterhin. ## Zusammenspiel mit Loading / Sequences ```javascript loading: { show: true, overlay: { title: "Bitte warten…", sequences: [ { after: 0, title: "Starte…", priority: 5 }, { when: pollActive, title: "API prüft…", priority: 40 } ] } } ``` `when` und `polling.active` können denselben Boolean nutzen. ## Trigger-Muster (Ninox) 1. Ja/Nein-Feld z. B. `trigger_checkApi` 2. Trigger „Nach Änderung“: wenn `true` → HTTP / Logik → Status ins Bag schreiben → `trigger_checkApi := false` 3. Widget pollt mit `value: true` und `active` aus dem Status ## Lokaler Smoke-Test (Interactive) 1. `npm start` im Widgets-Repo 2. Sidebar → **Button** → Variante **★ Polling Test (Console)** (oder P1–P3) 3. DevTools-Console filtern: `[arc-polling]` 4. Erwartung: - **Poll until active=false** — 3× `database.update`, dann Log `active=false`, Poll stoppt - **maxAttempts=3** — genau 3 Updates - **Single update** — genau 1 Update ## Grenzen - Läuft nur solange der Tab / die Ansicht offen ist (kein echter Cron). - v1: nur `type: "update"`; Live-Gate-Sync: arcCustomButton + arcCustomLayout. --- # Data Bag Actions Source: https://www.arc-rider.com/docs/patterns/data-bag-actions # Data Bag Actions (partielle Updates in JSON-Töpfen) ## AI Defaults (read first) - **`createNew.dataBag`** (Calendar Week) = nach erfolgreichem Anlegen, inkl. `##newRecordId##`. Siehe [`custom-calendar-week`](../widgets/custom-calendar-week.md). - **Gilt für alle Widgets mit `actions`**, die den Ninox Action Performer nutzen (Button, Layout, Table, KPI Bar, Calendar, Drawing, …). - **Syntax:** `dataBag: { encoding, base, patch }` — `patch` folgt JSON Merge Patch (RFC 7386): Key setzen/überschreiben; `null` / `''` löscht den Key. - **`encoding`:** `"raw"` (Default) — Plain-JSON im Textfeld via `JSON.stringify` / `parseJSON`. **`encoding` weglassen**, wenn nicht Legacy-Daten. - **`base`:** aktueller Feldwert (Rohstring aus dem Textfeld oder bereits geparstes Objekt). - **Lesen in Formeln:** `parseJSON(text(helper_uiState))` bzw. `parseJSON(helper_data)` — **ohne** `urlDecode`. - **Alternative:** plain `value` auf der Action, wenn das Zielfeld nur einen einzelnen Wert hält. - **Legacy:** `"urlEncoded"` nur bei bestehenden url-kodierten Feldern (`parseJSON(urlDecode(...))`). Siehe Abschnitt unten. - **Nicht verwechseln:** Widget-Prop `dataBag: { key, encoding }` in **arcCustomDrawing** — nur für Shape-Geometrie in verschachtelten JSON-Töpfen. Siehe [`custom-drawing`](../widgets/custom-drawing.md#-databag). --- ## Problem Viele UI-Zustände (Tab, Filter, ausgewählte ID) liegen in **einem** JSON-Textfeld (State-Bag). Beim Klick soll nur **ein Key** geändert werden — ohne den Rest zu löschen. Ohne `dataBag` musst du in Ninox den Topf manuell lesen, `setItem` aufrufen und wieder serialisieren (siehe [State-Bag in einem Textfeld](/docs/templates/tips_statefield)). Mit **`dataBag` auf der Action** übernimmt der Action Performer Read → Patch → Write. ## Action-Syntax ### Partiell patchen (empfohlen für State-Bags) ```javascript { type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_mainContentState"), dataBag: { base: helper_mainContentState, patch: { selectedShape: "123", activeTab: "details" } } } ``` - **`base`:** Rohwert aus dem Feld (String oder bereits geparstes Objekt — der Helfer normalisiert). - **`patch`:** nur die Keys, die sich ändern sollen. - Key löschen: `patch: { statusFilter: null }` oder `""`. - **`encoding`:** optional; Default `"raw"` — Plain-JSON im Textfeld. ### Einfacher Wert (dediziertes Feld) ```javascript { type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_selectedId"), value: "123" } ``` ## Wo funktioniert das? | Kontext | `dataBag` auf Action | Widget-`dataBag` (`key`) | |--------|----------------------|---------------------------| | arcCustomButton, Table, KPI Bar, Calendar, Input, … | ja (`update` + `create`) | — | | arcCustomCalendarWeek — `createNew` | ja (`create`, inkl. `##newRecordId##`) | — | | arcCustomLayout — Block-`actions` (Klick) | ja (`update` + `create`) | — | | arcCustomLayout — Widget-`actions` (Klick auf gesamtes Layout) | ja (`update` + `create`) | — | | arcCustomDrawing — Shape-`actions` (Klick) | ja (`update`) | — | | arcCustomDrawing — Geometrie speichern | — | ja (`key` + optional `encoding`) | Alle Widgets teilen sich dieselbe Action-Engine für `update` und `create` (inklusive `dataBag`). > **Hinweis für arcCustomDrawing Create:** Das Drawing-Widget legt neue Shapes über einen eigenen Create-Pfad an, nicht über die üblichen `actions`. Dort gilt weiterhin `setNewRecordId`. `dataBag` auf `type: "create"` gilt nur für explizit definierte Create-Actions in anderen Widgets (z. B. Button, Layout). **React / Vue / WeWeb:** Actions werden an den Host durchgereicht (`onAction` / `emit('action')`). `dataBag`-Patching ist dort nicht automatisch — der Host muss den Patch selbst anwenden oder den Klartext-`value` nutzen. ## `dataBag` auf `create`-Actions Neu: `dataBag` funktioniert auch auf `type: "create"`. Nach dem Anlegen des Datensatzes wird der Platzhalter `##newRecordId##` durch die numerische ID des neuen Records ersetzt — dann wird der Bag gepatch und ins Zielfeld geschrieben. ### Platzhalter `##newRecordId##` Ersetzt in beliebiger Tiefe (Strings, Objekte, Arrays) die neue numerische Record-ID nach dem Create. ```javascript { type: "create", tableId: tableId("Aufgaben"), changeFieldValues: [{ fieldId: fieldId("Aufgaben", "Projekt"), value: Nr }], dataBag: { recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), base: helper_uiState, patch: { selectedTaskId: "##newRecordId##" } } } ``` **Ablauf:** Create → neue ID → `##newRecordId##` auflösen → `readBag(base)` → `applyPatch(bag, patch)` → `writeBag` → `database.update` ### Mehrere Bags nach Create (Array) ```javascript dataBag: [ { recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), base: helper_uiState, patch: { selectedTaskId: "##newRecordId##", activeTab: "details" } }, { recordId: parentNr, fieldId: fieldId(parentNr, "helper_dashState"), base: helper_dashState, patch: { lastCreatedId: "##newRecordId##" } } ] ``` ### Kombiniert mit `setNewRecordId` Beide Mechanismen können zusammen genutzt werden: | | `setNewRecordId` | `dataBag` auf Create | |---|---|---| | **Zielfeld** | Ninox-Referenzfeld (Verknüpfung) | JSON-Textfeld (State-Bag) | | **Schreibt** | numerische ID direkt | ID als Patch-Key in JSON | | **Wann** | Parent-Datensatz verlinken | UI-State nach Create setzen | ```javascript { type: "create", tableId: tableId("Aufgaben"), changeFieldValues: [...], setNewRecordId: [{ recordId: Nr, fieldId: fieldId(Nr, "LetzteAufgabe") }], dataBag: { recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), base: helper_uiState, patch: { selectedTaskId: "##newRecordId##" } } } ``` ### Kalender `createNew` (nach erfolgreichem Anlegen) `createNew.dataBag` hängt direkt am Kalender-Create, nicht an einer Button-Action. Nach dem Anlegen (Aufziehen, Ganztags oder Ghost-Klick) läuft derselbe Create-Pfad. Typisch: Auswahl leeren, damit der Placement-Ghost verschwindet. ```javascript createNew: { tableId: tableId("Termine"), dateFrom: fieldId(first(Termine), "Datum von"), timeFrom: fieldId(first(Termine), "Von"), dateTo: fieldId(first(Termine), "Datum bis"), timeTo: fieldId(first(Termine), "Bis"), dataBag: { recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), base: helper_uiState, patch: { auswahlId: null, lastTerminId: "##newRecordId##" } } } ``` ## Beispiele ### Tab wechseln (Button) ```javascript arcCustomButton({ uniqueId: "tab-details-" + Nr, title: "Details", actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), dataBag: { base: helper_uiState, patch: { activeTab: "details" } } }] }) ``` ### Mobile Dimmer schließen (Layout-Block) ```javascript blocks: [{ width: "100%", height: "100%", styles: "position: absolute; z-index: 100;", value: "", actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_mainContentState"), dataBag: { base: helper_mainContentState, patch: { sidebarOpen: "", selectedShape: "" } } }] }] ``` ### Shape auswählen → Sidebar (Drawing) ```javascript actions: [{ type: "update", recordId: stateRecordId, fieldId: stateField, dataBag: { base: helper_mainContentState, patch: { selectedShape: Nr } } }] ``` Ausführlich: [`custom-drawing`](../widgets/custom-drawing.md) (Shape-`actions`, Klick vs. Drag). ## Legacy: `urlEncoded` Früher wurde JSON oft mit `urlEncode(formatJSON(...))` geschrieben, weil Ninox-Actions mit `{}` in `value` problematisch waren. Mit **dataBag** (Patch im Bundle) ist das in **neuen Projekten nicht mehr nötig**. | | `raw` (Default) | `urlEncoded` (Legacy) | |---|---|---| | Im Feld | `{"activeTab":"details"}` | `%7B%22activeTab%22%3A...` | | Formel lesen | `parseJSON(text(feld))` | `parseJSON(urlDecode(text(feld)))` | | Action schreiben | `dataBag: { base, patch }` | zusätzlich `encoding: "urlEncoded"` | | Wann | neue Demos, Seeds, Trigger | bestehende Kundendaten | ## Siehe auch - [State-Bag in einem Textfeld](/docs/templates/tips_statefield) — State-Bag manuell mit Ninox `setItem` / `item` - [`custom-drawing`](../widgets/custom-drawing.md) — Widget-`dataBag` für Pin-Geometrie + Shape-Actions --- # Datum und Zeit in React Source: https://www.arc-rider.com/docs/patterns/react-dates # Datum und Zeit in React Die Kalender verwenden getrennte Werte für Kalendertage und Uhrzeiten. Das verhindert, dass eine Anzeige versehentlich einen Unix-Zeitstempel als Uhrzeit interpretiert. ## Kalendertag `dateFrom`, `dateTo` und ein datumsbasierter `startDate` erhalten Millisekunden seit dem Unix-Epoch. Die Beispiele erzeugen den Tageswert mit `new Date(2026, 8, 17).getTime()`: September hat im JavaScript-Konstruktor den Index `8`. Für Calendar Grid ist `dateSettings.month` dagegen einsbasiert: September ist `9`. ## Uhrzeit und Dauer `timeFrom` und `timeTo` sind Millisekunden seit Tagesbeginn: `9 * 60 * 60 * 1000` steht für 09:00 Uhr. Eine Stunde Dauer ist `60 * 60 * 1000`. Calendar Week verwendet für den sichtbaren Bereich `timeSettings.timeStart` und `timeSettings.duration` in derselben Einheit. `dateSettings.duration` zählt dagegen Tage. ## Zeitzonen Lege in deiner Anwendung fest, ob deine Daten lokale Kalendertage oder globale Zeitpunkte darstellen. Konvertiere Backend-Zeitpunkte vor der Übergabe in die gewünschte Anzeigezone. Die Beispiele verwenden lokal erzeugte Tageswerte und `timeZoneBalance: 0`. Eine feste Stundenkorrektur bildet keine vollständige Zeitzone mit Sommerzeitwechseln ab. Teste besonders Termine um Mitternacht und an Zeitumstellungen. Lokale Datumsstrings wie `17.09.2026` eignen sich zur Anzeige, nicht als Datenaustauschformat. ## Bearbeiten und speichern Für verlässliche Speicherung benutzt das [Kalender-Beispiel](/docs/react/examples/individuelles-design-in-der-calendarweek) ein Formular und neue State-Werte. Die vorhandenen Drag-Callbacks melden den ursprünglichen Stand, aber nicht alle neuen Zielzeiten. Deshalb bleiben die dokumentierten Kalenderbeispiele ohne Drag & Drop. --- # Logbook to Billing Source: https://www.arc-rider.com/docs/patterns/logbook-to-billing # Logbook to Billing (Zeiterfassung → Aufwandsberechnung) ## AI Defaults (read first) - **Zwei getrennte Konzepte:** `TimeEntry` (einzelner Zeiteintrag) und `BillingPeriod` (Abrechnungszeitraum mit Soll-Stunden/Budget). Nicht vermischen. - **`billingMode` pro Zeiteintrag** entscheidet den Ausgabepfad: `contingent` (zählt auf ein Kontingent), `billable_extra` (wird separat abgerechnet), `non_billable` (zählt nirgends). - **Zwei Ausgabepfade, kein Automatismus dazwischen:** Stunden-Abrechnung (→ Rechnungsposition) und Budget/Kontingent-Nachweis (→ Aufwandsposition) sind unabhängige Auswertungen auf denselben `TimeEntry`-Daten — nicht ein einziger kombinierter Bericht. - **Kennzahlen sind reine Aggregationen** (`sum`, `min`, `round`) auf gefilterten `TimeEntry`-Listen — kein separates Aggregat-Objekt in der Datenbank nötig, kann pro Abfrage berechnet werden. - **Texte sind Config, kein Hardcoding:** Rechnungs- und Aufwandszeilen entstehen aus Platzhalter-Templates (`invoiceLineTemplate`, `effortLineTemplate`), nicht aus fest verdrahteten Sätzen im Formelcode. - **Kein lauffähiges Space-Beispiel in diesem Dokument** — nur Konzept, Datenmodell und illustrative Formeln. Konkrete Umsetzung ist projektspezifisch. --- ## Problem Viele Dienstleistungsverträge laufen nach demselben Muster: Mitarbeitende erfassen Zeiteinträge (Logbuch), und am Ende einer Periode muss daraus entweder 1. eine **Rechnungsposition** werden (Stunden × Satz), oder 2. ein **Aufwandsnachweis** ohne Rechnungsbetrag (wie viel von einem Kontingent/Budget wurde verbraucht). Ohne ein gemeinsames Datenmodell entsteht das für jedes Projekt neu — mit leicht unterschiedlichen Feldnamen und Formeln, die schwer wiederverwendbar sind. ## Datenmodell ### `TimeEntry` (Logbuch-Zeile) | Feld | Typ | Bedeutung | |------|-----|-----------| | `start` / `end` | Datum/Zeit | Beginn/Ende des Zeiteintrags | | `durationHours` | Zahl (berechnet) | `(end - start) / 3600000` | | `employeeRef` | Referenz | Mitarbeiter:in | | `contractRef` | Referenz | Zugehöriger Vertrag/Kunde | | `locationType` | Enum | `onsite` \| `office` \| `remote` | | `activityLabel` | Text | Freitext oder Kategorie der Tätigkeit | | `billingMode` | Enum | `contingent` \| `billable_extra` \| `non_billable` | | `billedAt` | Datum (optional) | Gesetzt, sobald der Eintrag abgerechnet wurde | ### `BillingPeriod` (Abrechnungszeitraum) | Feld | Typ | Bedeutung | |------|-----|-----------| | `contractRef` | Referenz | Zugehöriger Vertrag/Kunde | | `periodStart` / `periodEnd` | Datum | Grenzen des Zeitraums (z. B. Kalenderjahr, Quartal, Vertragslaufzeit) | | `targetHours` | Zahl | Soll-Stunden für den Zeitraum | | `hourlyRate` | Zahl | Stundensatz | | `budgetAmount` | Zahl (optional) | Alternativ zu `targetHours`, wenn in Geld statt Stunden budgetiert wird | ## Formeln (illustrativ) ```javascript let actualHours := sum( (select TimeEntry where contractRef = period.contractRef and billingMode = "contingent" and start >= period.periodStart and start <= period.periodEnd ).durationHours ); let remainingHours := period.targetHours - actualHours; let progressPercent := min(100, round(actualHours / period.targetHours * 100)); let billableQueue := (select TimeEntry where contractRef = period.contractRef and billingMode = "billable_extra" and billedAt = null ); ``` ## Zwei Ausgabepfade ### 1. Stunden-Abrechnung → Rechnungsposition Für Zeiteinträge mit `billingMode = "billable_extra"` und `billedAt = null`: 1. `billableQueue` gruppieren (z. B. nach Periode + `activityLabel`) 2. Pro Gruppe: `lineAmount := sum(durationHours) * hourlyRate` 3. Zeilentext aus `invoiceLineTemplate` (siehe Textbaustein-Mechanismus) 4. Nach Übernahme in die Rechnung: `billedAt` auf allen enthaltenen Zeiteinträgen setzen ### 2. Budget/Kontingent → Aufwandsposition Kein Rechnungsbetrag, sondern ein **Statusnachweis** pro `BillingPeriod`: - `actualHours`, `targetHours`, `remainingHours`, `progressPercent` - Zeilentext aus `effortLineTemplate` - Typische Darstellung: `arcCustomProgressBar` oder `arcCustomKpiBar` für den Fortschritt, `arcCustomTable` für die Liste der Perioden/Verträge ## Textbaustein-Mechanismus Statt fixer Sätze im Formelcode ein Config-Objekt mit Platzhaltern — der Wortlaut bleibt pro Projekt austauschbar, ohne die Formeln anzufassen: ```javascript let cfg := { labels: { onsite: "Vor Ort", office: "Büro", remote: "Remote" }, invoiceLineTemplate: "{hours} Std. {activityLabel} ({period}) à {rate} €", effortLineTemplate: "{actualHours} von {targetHours} Std. genutzt ({progressPercent} %), Rest {remainingHours} Std." }; ``` Platzhalter werden vor der Ausgabe per einfachem String-Replace aufgelöst (z. B. `replace(cfg.invoiceLineTemplate, "{hours}", text(hours))` verkettet für jeden Platzhalter). ## Beispiele ### Fortschrittsanzeige für ein Kontingent ```javascript arcCustomProgressBar({ uniqueId: "contingent-progress-" + period.Nr, value: progressPercent, label: replace( replace(cfg.effortLineTemplate, "{actualHours}", text(actualHours)), "{targetHours}", text(period.targetHours) ) }) ``` ### Offene abrechenbare Stunden als Tabelle ```javascript arcCustomTable({ uniqueId: "billable-queue-" + contractRef.Nr, table: [ { columns: [ { value: activityLabel }, { value: text(durationHours) + " Std." }, { value: text(durationHours * hourlyRate) + " €" } ] } // ... eine Zeile pro Eintrag aus billableQueue, gruppiert ] }) ``` ## Siehe auch - [`data-bag-actions`](./data-bag-actions.md) — partielle Updates, falls Filter/Auswahl-Zustand pro Periode zwischengespeichert wird - `docs/widgets/custom-kpi-bar.md`, `docs/widgets/custom-progress-bar.md` — Fortschritts-/Kennzahlen-Darstellung - `docs/widgets/custom-table.md` — Listendarstellung für Zeiteinträge/Rechnungspositionen --- # Nested Widget Descriptors Source: https://www.arc-rider.com/docs/patterns/nested-widget-descriptors # Inhalte und Widgets in React kombinieren In unterstützten `ArcWidgetSlot`-Feldern kannst du Text, ein JSX-Element oder einen Widget-Deskriptor verwenden. Für React-Anwendungen ist JSX praktisch: Props, Typprüfung und Event-Callbacks bleiben direkt an der Komponente. ## JSX und eigene Callbacks ```tsx import { ArcWidgetBadge, ArcWidgetInput, ArcWidgetLayout, } from '@arcrider/arcwidgets-react'; const row = (number: string, title: string) => ({ value: ({message}
> ); } ``` Der Deskriptor nutzt hier den geerbten Event-Fallback des Layouts. Wenn mehrere interaktive Kinder unterschiedliche Aktionen auslösen sollen, verwende JSX mit eigenen Callback-Props. ## Inhalte aus fremden Quellen String-Slots können HTML interpretieren. Gib fremde Texte daher über JSX aus, etwa als Textinhalt eines `span`. Verwende HTML nur aus eigener oder zuvor bereinigter Quelle. Deskriptor-Konfigurationen sind kein Ersatz für die Validierung fremder Daten. [Table mit Mini-Widgets](/docs/react/examples/custom-table-mit-mini-widgets) · [Dashboard](/docs/react/examples/dashboardansicht-mit-grid). # Nested Widget Descriptors (Object-Notation) ## AI Defaults (read first) - **Top-level** Ninox formula: use **`arcCustomLayout({ uniqueId: … })`** (function call) — not a descriptor object. - **Nested** inside another widget's content slot (`blocks[].value`, table cells, drawing sidebar, kanban cards, …): use **`{ widget, uid, data }`**. - **`widget`**: internal kebab-case ID (e.g. `"arc-widget-layout"`), **not** `arcCustomLayout`. - **`uid`**: same rules as `uniqueId` — unique, no spaces/special characters. - **`data`**: same properties as the function call; use `uid` at the top level instead of `uniqueId` inside `data`. - **Next-Gen only**: parent **and** child must come from the current **`bundle.js` / profile build** (`arcWidgetManager` v3). **Legacy Ninox-formula widgets do not support this.** - When unsure whether a widget is Next-Gen: if it only exists as an old Ninox template formula (not in the bundle), use the function call or plain HTML — **never** a descriptor object. --- # Verschachtelte Widgets als JSON-Objekt Mini-Widgets und andere Widgets sitzen in Slots als **reines JSON-Objekt** — nicht als vorgerendertes HTML. Das ist nützlich in Layout-Blöcken, Tabellen-Zellen, Drawing-Sidebars und tief verschachtelten Oberflächen. ## Zwei Schreibweisen ### Top-Level (Ninox-Formelfeld) Der Einstieg bleibt ein **Funktionsaufruf**: ```javascript arcCustomLayout({ uniqueId: "dashboard-" + Nr, embedded: false, fullscreen: true, height: "100%", direction: "vertical", blocks: [ … ] }) ``` ### Verschachtelt (Inhalt in einem Slot) In `blocks[].value` (und vergleichbaren Slots) nutzt du die **Widget-Descriptor-Notation**: ```javascript { widget: "arc-widget-button", uniqueId: "open-btn-" + Nr, // oder kurz: uid: "open-btn-" + Nr data: { title: "Öffnen", height: "20px", width: "auto", actions: [{ type: "popup", recordId: "42" }] } } ``` Für die ID akzeptiert der Descriptor **beide Schreibweisen** gleichwertig: - `uniqueId` — konsistent mit der Root-Config - `uid` — Kurzform, ebenfalls gültig ### Mapping | Funktionsaufruf | Descriptor | |-----------------|------------| | `arcCustomLayout({ … })` | `widget: "arc-widget-layout"` | | `uniqueId: "…"` | `uniqueId: "…"` oder `uid: "…"` (top-level, **nicht** in `data`) | | Alle anderen Props | In `data: { … }` | ## Vollständiges Beispiel (Layout → Button) Siehe auch [Custom Layout](/docs/widgets/custom-layout) für verschachtelte Layout-Blöcke. ```javascript { widget: "arc-widget-layout", uniqueId: "sidebar-item-" + Nr, // uid: "sidebar-item-" + Nr wäre gleichwertig data: { embedded: true, direction: "horizontal", gap: "10px", blocks: [{ width: "fraction", height: "auto", value: "Mangel ID" }, { width: "auto", height: "auto", value: { widget: "arc-widget-button", uniqueId: "open-btn-" + Nr, // uid: "open-btn-" + Nr wäre gleichwertig data: { title: "Öffnen", height: "20px", actions: [{ type: "popup", recordId: "42" }] } } }] } } ``` In Ninox bleibt `uniqueId: "sidebar-item-" + Nr` üblich, und `recordId: Nr` in Actions. ## Voraussetzung: Next-Gen-Bundle Die Engine erkennt Descriptor-Objekte in Slots über **`append_content`**: Ist `value` ein Objekt mit Feld `widget`, ruft sie `arcWidgetManager.js(widget, uid, data)` auf. Das funktioniert **nur**, wenn: 1. Das **Parent-Widget** aus dem aktuellen Bundle kommt und den Slot über `append_content` rendert. 2. Das **Child-Widget** im selben Bundle **registriert** ist. ### Next-Gen (funktioniert) - Widgets, die im aktuellen Bundle / Ninox-Template registriert sind ### Legacy (funktioniert **nicht**) - Widget existiert **nur** als alte **NinoxScript-Formel** im Template / in der Datenbank (HTML-String, kein `arcWidgetManager`) - Slot erwartet fertiges HTML oder ein Formel-Ergebnis — `{ widget: … }` wird nicht interpretiert (leer, Konsole-Fehler oder kaputtes Markup) **Noch ohne Descriptor-Unterstützung:** `arcCheckBox`, `shortNumbers` — diese **können weder Parent noch Child** in der Descriptor-Notation sein. ## Parent-Widgets mit Descriptor-Slots Diese Widgets mounten verschachtelte Descriptors (haben `append_content`): | Widget-ID | Typische Slots | |-----------|----------------| | `arc-widget-layout` | `blocks[].value` | | `arc-widget-button` | `icon`, `title`, `subtitle`, `prefix`, `suffix` | | `arc-widget-table` | `table[].columns[].value` | | `arc-widget-drawing` | `sidebarItem`, `rightSideContent`, Shape-Inhalte | | `arc-widget-kanban` | Karten-`value`, Swimlane-Inhalte | | `arc-widget-select` | `currentValue`, `items[].title` | | `arc-widget-input` | Label-/Prefix-Slots | | `arc-widget-badge` | Badge-Inhalt | | `arc-widget-upload` | `container.value` | | `arc-widget-calendar-week` | Entry-Inhalte | | `arc-widget-calendar-timeline` | Zellen-/Entry-Inhalte | | `arc-widget-calendar-grid` | Zellen-Inhalte | ## Child-Widgets (häufig verschachtelt) Alle im Bundle registrierten Widgets können als **`widget`-Wert** erscheinen, z. B.: | `widget`-Wert | Ninox-Funktion | |---------------|----------------| | `arc-widget-layout` | `arcCustomLayout` | | `arc-widget-button` | `arcCustomButton` | | `arc-widget-badge` | `arcCustomBadge` | | `arc-widget-icon` | `arcCustomIcon` | | `arc-widget-select` | `arcCustomSelect` | | `arc-widget-input` | `arcCustomInput` | | `arc-widget-image` | `arcCustomImage` | | `arc-widget-grid` | `arcCustomGrid` | | `arc-widget-text` | (Mini-Widget / Text) | Die **`widget`-ID** steht in der Tabelle oben (`arc-widget-button`, `arc-widget-layout`, …). ## Wann welches Format? | Situation | Format | |-----------|--------| | Root-Widget in Ninox-Formelfeld | `arcCustomXxx({ uniqueId, … })` | | Inhalt in Layout-Block, Table-Zelle, Drawing-Sidebar, … | `{ widget, uniqueId, data }` oder `{ widget, uid, data }` | | Legacy-Widget noch nicht im Bundle | Funktionsaufruf oder HTML — **kein** Descriptor | | Plain Text in Block | String (`value: "Text"`) | ## Fehler vermeiden - ❌ `widget: "arcCustomLayout"` oder `widget: "ArcWidgetLayout"` — falsche ID (CamelCase) - ❌ `uniqueId` oder `uid` **innerhalb** von `data` setzen — immer top-level (auf gleicher Ebene wie `widget` und `data`) - ❌ Descriptor in Top-Level-Formelfeld ohne Parent — use function call - ❌ Descriptor als Child eines Legacy-Layouts aus altem Ninox-Template ## Siehe auch - [Custom Layout](/docs/widgets/custom-layout) — Layout-Blocks und `value`-Slots - [Button](/docs/mini-widgets/buttons) — `prefix` / `suffix` als nested widgets # Nested Widget Descriptors (Object-Notation) ## AI Defaults (read first) - **Root** in Vue: the `ArcWidget…` component with `:data` and `@action`. - **Nested** in a slot: `{ widget, uid, data }` inside the parent's `data`. - **`widget`**: kebab-case ID (`"arc-widget-button"`), not the Vue component name. --- # Verschachtelte Widgets als JSON-Objekt Mini-Widgets und andere Widgets sitzen in Slots als **reines JSON-Objekt** — nicht als vorgerendertes HTML. Das ist nützlich in Layout-Blöcken, Tabellen-Zellen, Drawing-Sidebars und tief verschachtelten Oberflächen. ## Zwei Schreibweisen ### Root (Vue-Komponente) Der Einstieg ist die Komponente. Nested Descriptors leben **innen** in `data`. ### Verschachtelt (Inhalt in einem Slot) In `blocks[].value` (und vergleichbaren Slots) nutzt du die **Widget-Descriptor-Notation**: ```javascript { widget: "arc-widget-button", uniqueId: "open-btn-" + Nr, // oder kurz: uid: "open-btn-" + Nr data: { title: "Öffnen", height: "20px", width: "auto", actions: [{ type: "popup", recordId: "42" }] } } ``` Für die ID akzeptiert der Descriptor **beide Schreibweisen** gleichwertig: - `uniqueId` — konsistent mit der Root-Config - `uid` — Kurzform, ebenfalls gültig ### Mapping | Vue-Komponente | Descriptor | |----------------|------------| | `Shapes (Untertabelle)
Shapes (Untertabelle)
Pläne (Untertabelle) - Mängel (Untertabelle)
Shapes (Untertabelle)
| Feldname | Typ |
|---|---|
| `Bild` | Dokumenten-/Bild-Feld |
| `shareUrl` | URL-Feld |
| `sortId` | Zahlenfeld (zur Sortierung) |
| `Beschreibung` | Text-Feld |
| `currentImage` | Hilfsfeld (Text), z. B. im Layout |
| Tabellen | |
|---|---|
|  | - Kunden
- Mitarbeiter
|
Erstelle zunächst eine Hilfstabelle, in der die verschiedenen Statuswerte in einem Textfeld definiert werden. - **Status auf der Zieltabelle als dynamisches Feld einblenden (Ninox)**
Blende die Statuswerte in der Zieltabelle als dynamisches Feld ein, sodass sie flexibel genutzt werden können. - **Den Anwendungscode auf deiner Oberfläche einfügen**
Füge den erforderlichen Anwendungscode in deine Oberfläche ein. Das kann beispielsweise in einer Custom Table in einer Spalte sein oder aber blank auf deinem Ninox Record. - **Anwendungscode auf deinen Use Case anpassen: Deine eigene Statusliste auswählen**
Wähle die entsprechende Hilfstabelle, die du bei Schritt 1 angelegt hast, bei Blocks unter „select ….“ aus, um sicherzustellen, dass die richtigen Statuswerte verwendet werden. - **Anwendungscode auf deinen Use Case anpassen: Field bei Actions mit der fieldID deines Statusfeldes austauschen**
Tausche in den Actions das Feld mit der fieldID deines Statusfeldes, dass du bei Schritt 2 angelegt hast, aus, damit die Änderungen korrekt übernommen werden.
Mit diesen Schritten kannst du eine intuitive und benutzerfreundliche Oberfläche gestalten, auf der der Status schnell und einfach angepasst werden kann.
## Anwendungscode
```javascript
arcCustomLayout({
uniqueId: "statusTermin " + Nr,
direction: "horizontal",
alignX: "center",
alignY: "center",
width: "100%",
height: "100%",
gap: "10px",
backgroundColor: "",
paddingY: "10px",
paddingX: "10px",
blocks: let id := Nr;
(select 'Status Termin').[{
align: "center",
styles: "",
width: "fraction",
clickAction: "",
value: arcCustomButton({
uniqueId: "Termin_Status_" + id + Nr,
title: Bezeichung,
width: "100%",
icon: "",
fontSize: "",
fontColor: if id.Status = number(Nr) then
"#fff"
else
"#9ba3af"
end,
iconColor: "",
backgroundColor: if id.Status = number(Nr) then
Farbe
else
"#9ba3af33"
end,
borderColor: if id.Status = number(Nr) then
Farbe
else
"#9ba3af33"
end,
borderRadius: "5px",
actions: [{
recordId: id,
type: "update",
field: "Q1",
value: if id.Status = number(Nr) then
null
else
number(Nr)
end
}]
})
}]
})
```
---
# Vertriebs-Cockpit
Source: https://www.arc-rider.com/docs/examples/react-sales-app
# Vertriebs-Cockpit
Opportunities vom Erstgespräch bis zum Abschluss begleiten. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand.
## Ansichten und Workflows
**Übersicht · Pipeline · Opportunities · Aktivitäten**
- Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle.
- Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich.
- Navigiere in den Kalenderansichten wochenweise vor und zurück.
- Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar.
- Ziehe eine Karte zwischen den Spalten. Status, Tabellenzeile, Detailbereich und Kennzahlen verwenden danach den gleichen aktualisierten Eintrag.
## Live-App und vollständiger Code
Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen.
## Zusammenspiel der Widgets
Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten.
## Grenzen der Demo
Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten.
---
# Projekt-Cockpit
Source: https://www.arc-rider.com/docs/examples/react-projects-app
# Projekt-Cockpit
Aufgaben, Fortschritt und Termine an einem Ort. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand.
## Ansichten und Workflows
**Übersicht · Kanban · Aufgaben · Kalender**
- Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle.
- Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich.
- Navigiere in den Kalenderansichten wochenweise vor und zurück.
- Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar.
- Ziehe eine Karte zwischen den Spalten. Status, Tabellenzeile, Detailbereich und Kennzahlen verwenden danach den gleichen aktualisierten Eintrag.
## Live-App und vollständiger Code
Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen.
## Zusammenspiel der Widgets
Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten.
## Grenzen der Demo
Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten.
---
# Ressourcenplanung
Source: https://www.arc-rider.com/docs/examples/react-resources-app
# Ressourcenplanung
Teams, Einsätze und Verfügbarkeit gemeinsam planen. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand.
## Ansichten und Workflows
**Übersicht · Ressourcen · Wochenplan · Einsatzliste**
- Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle.
- Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich.
- Navigiere in den Kalenderansichten wochenweise vor und zurück.
- Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar.
- Wähle in der Ressourcenansicht ein Team oder einen Balken aus. Weise den Einsatz im Detailbereich einem anderen Team zu und prüfe seine neue Zeile.
## Live-App und vollständiger Code
Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen.
## Zusammenspiel der Widgets
Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten.
## Grenzen der Demo
Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten.
---
# Cyber Switch
Source: https://www.arc-rider.com/docs/components/component-cyberswitch
# Cyber Switch
# Cyber Switch Komponente

Mit `arcCustomComponentCyberSwitch` erstellst du ganz einfach einen modernen Umschalter zwischen zwei (oder mehr) Zuständen – perfekt für Filter, Views oder einfache Statuswechsel in deiner App. Die Komponente basiert auf `arcCustomLayout` und kombiniert UI-Design und Interaktion in einem vorgefertigten, reaktiven Block.
Der Cyber Switch kommt ohne komplizierte Setup-Schritte aus: Du definierst lediglich die Items, die aktiven und inaktiven Styles – und schon ist dein Schalter einsatzbereit. Dabei kannst du das Design vollständig an dein Farbschema anpassen.
**💡 Besonders hilfreich:**
- Ideal für visuelle Filter wie "Aktiv / Inaktiv", "Heute / Morgen", "Liste / Karte" etc.
- Nutzt `clickAction`, um z. B. Felder direkt zu aktualisieren.
- Jeder Button passt sich automatisch an das übergeordnete Layout an.
Im folgenden Abschnitt findest du den Anwendungscode sowie ein Beispiel zur Anwendung in Ninox.
## Gesamter Anwendungscode
Im folgenden leeren Anwendungscode findest du ein Beispiel mit zwei Items, die manuell definiert werden – inklusive individueller `clickAction` für jeden Schalter.
```javascript
arcCustomComponentCyberSwitch({
basicSettings: {
backgroundColor: ""
},
inactiveSettings: {
color: "",
backgroundColor: ""
},
activeSettings: {
color: "",
backgroundColor: ""
},
items: [{
title: "",
active: "",
clickAction: {
recordId: Nr,
type: "",
field: fieldId(current, "),
value: ""
}
}, {
title: "",
active: "",
clickAction: {
recordId: Nr,
type: "",
field: fieldId(current, "),
value: ""
}
}]
})
```
## Einzelne Parameter
### basicSettings Block
Der `basicSettings`-Block definiert die grundlegenden Design-Eigenschaften der gesamten Schalter-Komponente. Aktuell gibt es nur einen konfigurierbaren Parameter:
Parameter
Beschreibung
Default-Wert
`backgroundColor`
Hintergrund der gesamten Komponente
`#F8FAFC`
Parameter
Beschreibung
Default-Wert
`color`
Textfarbe für inaktive Buttons
`#727c90`
`backgroundColor`
Hintergrundfarbe für inaktive Buttons
`#F8FAFC`
Parameter
Beschreibung
Default-Wert
`color`
Textfarbe für aktiven Button
`#ffffff`
`backgroundColor`
Hintergrundfarbe für aktiven Button
`#3b82f6`
Parameter
Beschreibung
Beispiel
`title`
Text, der im Button angezeigt wird.
```javascript
title: "Menupunkt 1",
```
`active`
Bedingung, wann der Button als aktiv dargestellt wird.
`clickAction`
Aktion, die beim Klick auf den Button ausgeführt wird (z. B. Feld aktualisieren).
Widget
Beschreibung
`arcCustomLayout`
Grundstruktur zur Darstellung der Buttons
`arcCustomText` (optional & kostenfrei)
Nur nötig, wenn du Texte innerhalb der Switch-Items selbst gestalten willst
Parameter
Typ
Pflicht
Beschreibung
Beispiel
`id`
Zahl
✅
Eindeutige ID des KPI-Elements. Wird zur Unterscheidung der einzelnen KPIs genutzt.
`1`
`title`
Text
❌
Titel, der oberhalb des Wertes angezeigt wird.
`"Lagerbestand"`
`iconName`
Text
✅
Name des Icons aus der integrierten Icon-Bibliothek.
`"buildings"`
`value`
Text
✅
Der Hauptwert, der in der KPI-Box angezeigt wird.
`text(sum(lager.sum_lagerbestand))`
`subtitle`
Objekt
❌
Zusätzlicher Bereich unterhalb des Hauptwerts mit Detailinformationen. Siehe unten.
Siehe `subtitle`-Struktur
Parameter
Typ
Pflicht
Beschreibung
Beispiel
`color`
Text
❌
Legt die Text- und Icon-Farbe für den Subtitle fest. Unterstützt Hex- oder CSS-Farben.
`"#3b82f6"`
`iconName`
Text
❌
Icon für den Subtitle. Wird links neben dem Wert angezeigt. Wird aus dem aktiven Icon-Set aus dem User Cockpit gezogen.
`"truck"`
`value`
Text
❌
Der zusätzliche Wert, der im Subtitle angezeigt wird.
`cnt(lagerbuchungen[Buchungsart = 1])`
`description`
Text
❌
Eine kurze Beschreibung oder Erläuterung für den Wert.
`"neue Lieferungen"`
Parameter
Typ
Beschreibung
Beispiel
`uniqueId`
Text
Eindeutige ID für die Komponente (für Styling und Wiederverwendung wichtig)
```javascript
uniqueId: "card element Lagerkapazitäten" + Nr,
```
`title`
Text
Überschrift der Karte
```javascript
title: "Lagebestände",
```
`subtitle`
Text
Untertitel oder Zusatzinfo unter dem Titel
```javascript
subtitle: "Lagebestände aller Lager",
```
`switchItems`
Liste
Liste mit den Umschaltoptionen (siehe unten für Details)
```javascript
switchItems: [ {…}, {…} ]
```
Parameter
Typ
Beschreibung
Beispiel
`id`
Zahl o. Text
Eindeutige ID für das Item (kann z. B. für dynamisches Mapping genutzt werden)
```javascript
id: 1,
```
`title`
Text
Titel des Buttons im Switch (z. B. "Bestände")
```javascript
title: "Bestände",
```
`recordId`
ID/Ref
ID des aktuellen Datensatzes, auf den sich die Aktion bezieht
```javascript
recordId: Nr,
```
`fieldId`
Funktion o. Text (ID)
Referenz auf das Feld, das aktualisiert werden soll
```javascript
fieldId: fieldId(Nr, "filter_lagerkapazitaet"),
```
`value`
beliebig
Wert, der gesetzt werden soll, wenn der Button geklickt wird
```javascript
value: 2,
```
`active`
bool./Expr.
Ausdruck, wann das Item aktiv ist
```javascript
active: filter_lagerkapazitaet = "2",
```
`content`
Text
Der eigentliche Inhalt, der angezeigt wird, wenn dieses Item aktiv ist
```javascript
content: 'arcProgressBarComponent _lager'
```
Inhaltstyp
Beschreibung
Beispiel
Widget-Referenz
Aufruf eines anderen Widgets (z. B. eine Fortschrittsanzeige, ein Donut-Chart, ein eigenes Layout, etc.)
```javascript
content: helper_donuts_lager
```
Textausgabe
Einfacher Text (z. B. als Platzhalter oder zur schnellen Anzeige)
Widget
Beschreibung
`arcCustomCard`
Grundstruktur der Karte (Layout, Padding, Hintergrund)
`arcCustomLayout`
Flexible Verschachtelung von Inhalten und Layouts
`arcCustomText` (kostenfrei)
Darstellung von Titel und Untertitel
`arcCustomComponentCyberSwitch` (kostenfrei)
Umschalter zur Auswahl der Inhalte innerhalb der Karte