wcz-layout
Version:
582 lines (494 loc) • 28.3 kB
Markdown
# Migrate wcz-layout v9 to v10
Two headline changes:
1. Login moves to a `/login` page where the user picks a provider. AWS Cognito joins
Entra. Entra-only apps behave as before, apart from the new page.
2. Every paid MUI X package is gone. `LayoutTable` (TanStack Table v9) replaces
`DataGridPremium`, pickers move to the free `@mui/x-date-pickers`, and there is no
licence key any more.
Work through the steps in order.
Commit the migration edits before running `vp check --fix`. On a repo not already
formatted to the current config it rewrites hundreds of unrelated files, and the
middleware arrays you need to eyeball vanish into the churn.
## 1. Dependencies
| Action | Package | Version |
| ------- | ---------------------------------- | ----------- |
| Install | `@tanstack/react-table` | 9.x |
| Install | `@tanstack/react-virtual` | 3.x |
| Install | `@tanstack/react-hotkeys` | 0.x |
| Install | `@mui/x-date-pickers` | 9.x |
| Upgrade | `@tanstack/react-query` | **≥ 5.102** |
| Upgrade | `@tanstack/react-router-ssr-query` | latest 1.x |
| Delete | `@mui/x-data-grid-premium` | — |
| Delete | `@mui/x-date-pickers-pro` | — |
```bash
npm install @tanstack/react-table@9 @tanstack/react-virtual@3 @tanstack/react-hotkeys@0 @mui/x-date-pickers@9
npm install @tanstack/react-query@latest @tanstack/react-router-ssr-query@latest
npm uninstall @mui/x-data-grid-premium @mui/x-date-pickers-pro
```
**Both query upgrades are mandatory** and neither failure names a version:
- react-query below 5.102 lacks `QueryKeyWithDataTag`, which v10's `.d.mts` reference.
Every `wcz-layout/data/client` export then resolves to `unknown`, surfacing as
`TS18046: 'x' is of type 'unknown'` scattered through your own components.
- ssr-query below 1.167.2 hydrates against the pre-5.102 shape and throws
`Cannot read properties of undefined (reading 'mutations')` in the browser. No type
error, so `vp check` passes and the page silently falls back to client rendering.
If `@mui/material` is pinned to an exact version — common, to match the pinned MUI X
Pro packages — loosen it to a caret range, or the install fails `ERESOLVE` against a
newer `@mui/icons-material`.
Then: rewrite every `@mui/x-date-pickers-pro` reference to `@mui/x-date-pickers`,
including the `/// <reference types="@mui/x-date-pickers-pro/themeAugmentation" />`
line atop `src/hooks/useTheme.ts` — not an import, so grepping for one misses it.
Delete every `@mui/x-license` import from `src/router.tsx` and what it feeds —
`LicenseInfo.setLicenseKey`, plus `muiXTelemetrySettings.disableTelemetry()` where it
is used. Drop `VITE_MUI_LICENSE_KEY` from `src/env.ts` and `.env*`, and remove it
from Vault. If that empties the `client` block, delete `clientEnv` and its
import in `src/router.tsx`; delete any `.env*` file left empty.
The range pickers were Pro-only, so `DateRangePicker`, `TimeRangePicker` and
`DateTimeRangePicker` are gone from `useLayoutForm`. Replace each with two adjacent
single-value fields, validate the ordering in the form schema, and delete the
`MuiDateRangePicker` / `MuiDateTimeRangePicker` entries from `useTheme.ts`.
## 2. Renamed auth exports
| v9 | v10 | From |
| ---------------------------------------------- | ----------------------- | ----------------------- |
| `requirePermission("key")` | `requireAuth("key")` | `wcz-layout/utils` |
| `requirePermission("all")` | `requireAuth()` | `wcz-layout/utils` |
| `authorizationMiddleware("key")` | `authMiddleware("key")` | `wcz-layout/middleware` |
| `authorizationMiddleware("all")` | `authMiddleware()` | `wcz-layout/middleware` |
| `authenticationMiddleware()` | `authMiddleware()` | `wcz-layout/middleware` |
| `authenticationMiddleware({ optional: true })` | dropped, see step 6 | |
**The "any employee" key must go.** v9 apps carried a key listing the company-wide
employee groups (`wcz-all-employees`, `wscz-all-employees`) to mean "anyone signed
in". v10 says that with no key at all. Delete the entry from `permissions.ts` —
leaving it breaks AWS, whose sessions carry no groups, so a key-checked guard rejects
every Cognito user.
**Find it by its groups, not its name.** It is usually called `all`, but not always —
one app named it `tester`. Read `permissions.ts` and pick the key whose value is the
company-wide employee groups; that is the one to translate and delete, whatever it is
called. Keys naming a real group (`admin` and friends) stay. Replace any
`hasPermission(user, <that key>)` with `!!user`.
New: `LoginForm` (`wcz-layout/components`), `loginProvidersQueryOptions`
(`wcz-layout`), `Permissions` and `Scopes` types (`wcz-layout/models`), and
`userMiddleware` (`wcz-layout/middleware`) — puts the caller in `context.user`,
`null` when anonymous, requires nobody.
Unchanged: `hasPermission`, `getUser`, `getAccessToken`, `getAppToken`,
`handleLogin`, `handleCallback`, `handleLogout`, `csrfMiddleware`,
`validationMiddleware`. `User` gains an additive `provider` field.
Removed from `wcz-layout/components`: `RouterGridActionsCellItem` and
`EditableColumnHeader` (grid wrappers — row actions now live in a `display` column),
and `TypographyWithIcon` (inline a `Stack direction="row"` with the icon and a
`Typography`).
## 3. src/lib/auth/permissions.ts
```ts
import type { Permissions } from "wcz-layout/models";
export const permissions = {
admin: ["wcz-developers"],
} as const satisfies Permissions;
```
## 4. src/lib/auth/scopes.ts
Arrays become strings, one scope per key. The object form is only for an API an AWS
session must reach.
```ts
import type { Scopes } from "wcz-layout/models";
export const scopes = {
graph: "User.Read",
api: "api://wistron.com/project-name/access_as_user",
// reachable by both providers:
// file: { entra: "api://…/access_as_user", aws: "https://…/access_as_user" },
} as const satisfies Scopes;
```
Keep `as const satisfies` on both files — `as const` is what keeps
`requireAuth("admin")` and `getAccessToken("file")` type-checked.
## 5. src/routes/login.tsx, new and required
`requireAuth` redirects unauthenticated users to `/login`; without this file they 404.
```tsx
import { createFileRoute } from "@tanstack/react-router";
import { loginProvidersQueryOptions } from "wcz-layout";
import { LoginForm } from "wcz-layout/components";
export const Route = createFileRoute("/login")({
loader: ({ context }) => context.queryClient.query(loginProvidersQueryOptions),
component: LoginForm,
});
```
The loader makes the buttons server-render. `LoginForm` takes no props; it reads
`?returnTo` itself and shows one button per configured provider.
`tsc` will report `'"/login"' is not assignable to 'keyof FileRoutesByPath'` until the
route tree regenerates — run `vp check` before chasing it.
## 6. src/server/middleware/databaseMiddleware.ts
Only if the app has one. The change is one line — swap its auth dependency:
```diff
- .middleware([authenticationMiddleware({ optional: true })]) // or authenticationMiddleware()
+ .middleware([userMiddleware])
```
Keep the shape the app already has. If it is a plain exported middleware object, it
stays one. If it is a factory that existed only to carry an `optional` flag, the
generic factory, the pre-built instances and the overloads all go — there is one
signature now.
### Did it authenticate on its own?
Check what the v9 middleware composed before touching any call site:
- `authenticationMiddleware({ optional: true })` — required nobody. Nothing opens up.
- `authenticationMiddleware()` — **required a signed-in user**, and every call site
that stood alone inherited that. `userMiddleware` requires nobody, so each of those
now serves anonymous callers unless you add `authMiddleware()` yourself.
| v9 call site | v9 meaning | v10 |
| ---------------------------------------------------------- | ------------------- | -------------------------------------------------- |
| `databaseMiddleware({ optional: true })` | anonymous allowed | `[databaseMiddleware()]` |
| `databaseMiddleware()` over `authenticationMiddleware()` | user required | `[databaseMiddleware(), authMiddleware()]` |
| `[authorizationMiddleware("admin"), databaseMiddleware()]` | permission required | `[databaseMiddleware(), authMiddleware("admin")]` |
Grep the middleware name, not `createServerFn` — route `server.middleware` arrays
count too, and job, cron and webhook endpoints are where a lone `databaseMiddleware`
hides. Those are the worst ones to leave anonymous.
### Order: `databaseMiddleware` first
Middleware context merges left to right and the later entry wins, so the order
decides whether `context.user` is nullable in the handler. `databaseMiddleware`
declares `userMiddleware` and contributes `user: User | null`; `authMiddleware`
contributes `user: User`. Put the database one first and the narrowed type survives:
```ts
.middleware([databaseMiddleware(), authMiddleware("admin")])
.handler(({ context }) => context.user.name) // User, no narrowing needed
```
v9 arrays are usually written the other way round, so reorder them as you go. Any
`validationMiddleware` in a REST route still goes **after** `authMiddleware`, or an
unauthenticated caller gets their body parsed and your validation errors back:
`[databaseMiddleware(), authMiddleware("admin"), validationMiddleware(Schema)]`.
## 7. src/start.ts
v10 apps need no `src/start.ts` just for CSRF. When the file is absent, TanStack Start
installs its own default request middleware,
`createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" })` — exactly
what `csrfMiddleware` from `wcz-layout/middleware` is.
Open the file and read what `createStart` returns:
- **Only `requestMiddleware: [csrfMiddleware]`**, with `csrfMiddleware` imported from
`wcz-layout/middleware` and no other option → delete `src/start.ts` completely.
- **Anything else** — a second entry in `requestMiddleware`, a `functionMiddleware`
array, any other option, or a `createCsrfMiddleware` of the app's own with a
different filter → keep the file as it is.
After deleting, `src/routeTree.gen.ts` still imports `startInstance` from `./start.ts`
until the route tree regenerates — run `vp check` before chasing it. Nothing else
should import `startInstance`; grep to confirm.
## 8. Locales
The app owns every string — the vite plugin builds i18next resources from the app's
`src/lib/locales/*.json`, so a key the library renders but the app does not define
comes out raw. The package does not ship its locale files: the complete v10 `Layout`
block for `en` and `cs` is at the end of this guide, under
[Appendix: v10 Layout locales](#appendix-v10-layout-locales).
v10 adds the whole `Layout.Table` namespace plus `Layout.ContinueWith` and
`Layout.TestEnvironment`. Take only the `Layout` key; nothing else in the app's
locale files is the library's.
**Merge it into the app's `Layout`, do not replace the block.** Apps add their own
keys inside that namespace (`Layout.Loading`, `Layout.NotProvided` in one app), and a
wholesale replace silently deletes them — which *does* break the build, because
`src/types/i18next.d.ts` types `t()` from `en.json` and the app's own
`t("Layout.Loading")` calls stop compiling. So: overlay the library's keys on top of
the app's, then look at what is left over. A leftover key the app's own code
references is the app's — keep it. One nothing references is a v9 library key v10
dropped — remove it, or it lingers forever because it passes every check.
**Use the block from the guide of the major you are migrating to.** Do this step
again in every major's pass: `Layout` keys change between majors, and each guide's
appendix holds its own major's set. A v9 → v11 upgrade runs the v10 pass with this
appendix and the v11 pass with the v11 guide's. Carrying v10's block into a v11 app
leaves the wrong key set.
**A library key the app never calls can go missing without failing the build.**
`t()` is typed from the app's `en.json`, so it catches the app's own calls and nothing
else — the library's are compiled already. A `Layout.Table.*` key the app never
references therefore survives `vp check` and surfaces only as a raw key string on
screen. Verify deliberately: compare the app's `Layout` key set against the appendix,
and look at the login card and the table toolbar, where a miss shows first.
**Every language the app ships needs the new keys, not just the two.** The appendix
covers `en` and `cs`; an app may have any number of files in `src/lib/locales/`. For
every other file (`de.json`, `pl.json`, `zh.json`, …), take the **English** block and
translate it into that file's language yourself, as part of this step — do not ask,
do not leave English values in place, do not skip the file. Nobody else will, and the
gap is invisible until someone switches language.
- Merge the same way as above: keys the app already translated keep their existing
values; translate only the keys the file is missing.
- Translate values only. Keys, `{{provider}}` / `{{name}}` / `{{count}}`
interpolations and the `<env>` tag stay byte-for-byte.
- Emit the plural suffixes the target language needs, not English's. English has
`_one` / `_other`, Czech adds `_few`, Polish needs `_one` / `_few` / `_many` /
`_other`, and Chinese or Japanese only `_other`. When unsure, check
`new Intl.PluralRules("<lang>").resolvedOptions().pluralCategories`; a category
that only applies to fractions (Czech `many`) can be left out, `count` is whole.
- Match the tone of the file's existing strings (formal vs. informal address).
`Layout.TestEnvironment` holds an `<env>` tag the mascot renders through `Trans` —
keep the tag in every language, it colours the word.
## 9. Replace the grid
**Only where `DataGridPremium` is used.** A table already built on TanStack Table
stays exactly as written — do not convert it to `LayoutTable`. This step exists
because the paid grid is gone, not because `LayoutTable` is the house table for
everything. Grep for `DataGridPremium`; anything else is out of scope.
Read the `table` skill before writing any columns — it documents the API. This is
only the mapping.
Delete `src/components/DataGridToolbar.tsx`; `LayoutTable` renders the toolbar,
quick filter, columns panel and filter panel itself. In `useTheme.ts`, drop
`import { csCZ, enUS } from "@mui/x-data-grid/locales"` and its two entries.
| DataGridPremium | LayoutTable |
| --------------------------------------------------- | ------------------------------------------------------------- |
| `Array<GridColDef<Row>>` | `columnHelper.columns([...])` from `createLayoutColumnHelper` |
| `field` / `headerName` | the accessor key / `header` |
| `width`, `flex`, `minWidth` | `size` |
| `renderCell` | `cell: ({ cell }) => <cell.ValueCell />` |
| `valueGetter: (_v, row) => row.x.y` | `columnHelper.accessor((row) => row.x.y, { id: "..." })` |
| `align` / `headerAlign` | `meta: { align }` |
| `initialState.pinnedColumns.left` / `.right` | `initialState.columnPinning.start` / `.end` |
| `initialState.sorting.sortModel` `{field, sort}` | `initialState.sorting` `[{ id, desc }]` |
| `valueFormatter` on a date column | `cell.DateCell` or `cell.DateTimeCell` |
| `valueFormatter` with an `Intl.NumberFormat` | `cell.NumberCell options={...}`, same options object |
| `type: "singleSelect"` with `valueOptions` | `meta: { variant: "select", options }` |
| `type: "number"` / `"date"` / `"boolean"` | `meta: { variant: "number" \| "date" \| "boolean" }` |
| `rows` and `columns` props | `useLayoutTable({ data, columns })`, then `table={table}` |
| `showToolbar` with `slots.toolbar` | `showToolbar` with `title` and `actions` on `LayoutTable` |
| `ignoreDiacritics` | always on |
| `rowSelectionModel` state | `table.getSelectedRowIds()`, `table.resetRowSelection()` |
| `cellSelection` | always on, `Mod+C` copies the range as TSV |
| `GridActionsCellItem` in a `type: "actions"` column | a `display` column rendering `cell.ActionsCell` |
**Every row needs an `id`.** `getRowId` is not an option — the library hardcodes
`row.id`. A table over a view or a natural-key table without an `id` column must add
one where the rows are built (`id: row.cpn`), and the row type gains `id: string`.
**Dropped with no replacement**, so decide per case whether to rebuild or drop:
`getRowClassName` and `cellClassName` (style inside the cell instead — a `sx` on
`cell.NumberCell` colours the text, nothing reaches the row), `rowHeight`, the
Excel export button, and `localeText` overrides such as `footerTotalRows`. Any `sx`
block targeting `.MuiDataGrid-*` classes is dead: delete it rather than translating
it, LayoutTable's DOM is unrelated.
**State persistence changes shape.** `apiRef.exportState()` / `restoreState()` and the
per-change callbacks (`onSortModelChange`, `onPinnedColumnsChange`, …) are gone.
Restore by passing `initialState` read from storage before first render, and persist
with one effect on `table.state`. There is no `onStateChange` option.
**Shared generic column factories do not port.** A v9 helper like
`actionColumn<T>(): GridColDef<T>` reused across pages cannot be rewritten as
`(helper: ColumnHelper<T>) => ...` — `createLayoutColumnHelper` is invariant in its row
type, so a concrete helper will not satisfy a generic parameter. Share the *contents*
instead: a function returning `Array<RowAction<T>>` (that type is generic-friendly) or
a plain predicate, and let each page write its own five-line `display` column.
**Inline editing has no equivalent.** No `editMode`, no `processRowUpdate`, no
editable columns, and `EditableColumnHeader` is gone. A page built on
`editMode="row"` is a redesign, not a column mapping: move create and edit into a
dialog with a `useLayoutForm` and leave the table read-only. Usually the largest item
in the whole migration. It also retires `rowModesModel`, the `isNew` row flag and any
"turn the Zod error into a string for the grid" helper — delete them, the form renders
field errors itself.
**Bulk actions need a selection column.** A v9 page driving actions off
`Object.keys(cellSelectionModel)` has nothing to map onto: cell selection is always on
in v10 but exists for `Mod+C`. `table.getSelectedRowIds()` reads *row* selection,
which renders only if the columns start with a `select` display column
(`header.SelectAllCell` / `cell.SelectCell`, `size: 50`, `enableResizing: false`,
`enableCellSelection: false`). Ids come back as plain strings, so the `id.toString()`
that `GridRowId` forced can go.
Two behaviours that catch people out. `meta.variant: "select"` and `"multi-select"`
read options from the column's faceted unique values, so pass `meta.options` only when
the label differs from the stored value — a translated database enum, derived from
`enumValues`. And grouping shows a value only for the grouping column and columns that
resolve an aggregation function (a number or `Date`, unless the column sets
`aggregationFn`); others stay blank in a group row.
## 10. useNotification
`useNotification` is gone, along with its snackbar and the `snackbarOrigin` layout
option. Replace every call with something the page renders itself — a success
animation, a non-dismissing alert dialog for errors. Match whatever the app already
does for feedback rather than introducing a new pattern.
Two things to check:
- A `notify(..., { severity: "error" })` sitting next to an `alert(error.message)`
from `useDialogs` is redundant. Delete the `notify`, keep the `alert`.
- A success `notify` that fires **after** a `navigate` cannot become a component on
the page it was called from — that page has already unmounted. Invert it: render
the feedback, and navigate when it finishes.
## 11. AWS Cognito, optional
Skip for Entra-only apps. To offer AWS, add to the `server` block of `src/env.ts`:
```ts
AWS_ISSUER: z.url().optional(), // https://cognito-idp.<region>.amazonaws.com/<poolId>
AWS_DOMAIN: z.url().optional(), // https://<prefix>.auth.<region>.amazoncognito.com
AWS_CLIENT_ID: z.string().min(1).optional(),
AWS_CLIENT_SECRET: z.string().min(1).optional(),
```
Set all four in `.env.local`; the second button appears only when all are present.
The Cognito app client must be confidential, use the authorization-code grant, and
list `<origin>/auth/callback` as callback and `<origin>/` as sign-out URL.
An AWS session requests OIDC scopes only, so it cannot mint a delegated API token.
Cognito cannot widen scopes after login, so adding a downstream API means requesting
every `aws` scope in `scopes.ts` at login, and an unregistered one fails the whole
sign-in — register them all on the app client first. Downstream APIs must also accept
the Cognito JWKS and authorize on the token's `scope` claim, since Cognito access
tokens carry no `aud`.
## Appendix: v10 Layout locales
The complete `Layout` block wcz-layout v10 renders, for step 8. Merge it into the
app's `src/lib/locales/en.json` and `cs.json` as that step describes — do not paste
it over the app's block. For any other language, translate the English block.
### en.json
```json
{
"Layout": {
"LogIn": "Log In",
"ContinueWith": "Continue with {{provider}}",
"Logout": "Logout",
"Language": "Language",
"Appearance": "Appearance",
"Light": "Light",
"Dark": "Dark",
"System": "System",
"ThisPageCouldNotBeFound": "This page could not be found.",
"Settings": "Settings",
"Unauthorized": "Unauthorized",
"TestEnvironment": "You’re in the <env>TEST</env> environment",
"Dialog": {
"Confirm": "Confirm",
"Cancel": "Cancel",
"Alert": "Alert"
},
"File": {
"AreYouSureYouWantToDelete": "Are you sure you want to delete {{name}}?",
"DragSomeFilesHereOrClickToSelectThem": "Drag some files here or click to select them",
"Delete": "Delete",
"Download": "Download"
},
"Table": {
"Search": "Search",
"ClearSearch": "Clear search",
"ColumnMenu": "Column menu",
"Filters": "Filters",
"AddFilter": "Add filter",
"RemoveFilter": "Remove filter",
"ClearFilters": "Clear filters",
"And": "and",
"Or": "or",
"Field": "Field",
"Operator": "Operator",
"Value": "Value",
"NoValueRequired": "No value required",
"From": "From",
"To": "To",
"DaysFromToday": "Days from today",
"NoRows": "No rows",
"SortAscending": "Sort ascending",
"SortDescending": "Sort descending",
"ClearSort": "Clear sort",
"PinStart": "Pin to start",
"PinEnd": "Pin to end",
"Unpin": "Unpin",
"GroupBy": "Group by this column",
"Ungroup": "Stop grouping",
"RowActions": "Row actions",
"SelectedCount_one": "{{count}} selected",
"SelectedCount_other": "{{count}} selected",
"MoveLeft": "Move left",
"MoveRight": "Move right",
"All": "All",
"Columns": "Columns",
"ShowHideAll": "Show/Hide all",
"HideColumn": "Hide column",
"Operators": {
"includesString": "contains",
"notIncludesString": "does not contain",
"equalsString": "is",
"notEqualsString": "is not",
"startsWith": "starts with",
"endsWith": "ends with",
"isEmpty": "is empty",
"isNotEmpty": "is not empty",
"equals": "is",
"notEquals": "is not",
"greaterThan": "is greater than",
"greaterThanOrEqualTo": "is greater than or equal to",
"lessThan": "is less than",
"lessThanOrEqualTo": "is less than or equal to",
"inRange": "is between",
"isRelativeToToday": "is relative to today",
"Date": {
"lessThan": "is before",
"lessThanOrEqualTo": "is on or before",
"greaterThan": "is after",
"greaterThanOrEqualTo": "is on or after"
}
}
}
}
}
```
### cs.json
```json
{
"Layout": {
"LogIn": "Přihlásit se",
"ContinueWith": "Pokračovat přes {{provider}}",
"Logout": "Odhlásit se",
"Language": "Jazyk",
"Appearance": "Vzhled",
"Light": "Světlý",
"Dark": "Tmavý",
"System": "Systém",
"ThisPageCouldNotBeFound": "Tato stránka nebyla nalezena.",
"Settings": "Nastavení",
"Unauthorized": "Neoprávněný",
"TestEnvironment": "Jste v <env>TESTOVACÍM</env> prostředí",
"Dialog": {
"Confirm": "Potvrdit",
"Cancel": "Zrušit",
"Alert": "Upozornění"
},
"File": {
"AreYouSureYouWantToDelete": "Opravdu chcete smazat {{name}}?",
"DragSomeFilesHereOrClickToSelectThem": "Přetáhněte sem některé soubory nebo je vyberte kliknutím",
"Delete": "Smazat",
"Download": "Stáhnout"
},
"Table": {
"Search": "Hledat",
"ClearSearch": "Zrušit hledání",
"ColumnMenu": "Nabídka sloupce",
"Filters": "Filtry",
"AddFilter": "Přidat filtr",
"RemoveFilter": "Odebrat filtr",
"ClearFilters": "Zrušit filtry",
"And": "a",
"Or": "nebo",
"Field": "Pole",
"Operator": "Operátor",
"Value": "Hodnota",
"NoValueRequired": "Hodnota není potřeba",
"From": "Od",
"To": "Do",
"DaysFromToday": "Dnů od dneška",
"NoRows": "Žádné řádky",
"SortAscending": "Seřadit vzestupně",
"SortDescending": "Seřadit sestupně",
"ClearSort": "Zrušit řazení",
"PinStart": "Připnout na začátek",
"PinEnd": "Připnout na konec",
"Unpin": "Odepnout",
"GroupBy": "Seskupit podle sloupce",
"Ungroup": "Zrušit seskupení",
"RowActions": "Akce řádku",
"SelectedCount_one": "{{count}} vybraný",
"SelectedCount_few": "{{count}} vybrané",
"SelectedCount_other": "{{count}} vybraných",
"MoveLeft": "Posunout doleva",
"MoveRight": "Posunout doprava",
"All": "Vše",
"Columns": "Sloupce",
"ShowHideAll": "Zobrazit/skrýt vše",
"HideColumn": "Skrýt sloupec",
"Operators": {
"includesString": "obsahuje",
"notIncludesString": "neobsahuje",
"equalsString": "je",
"notEqualsString": "není",
"startsWith": "začíná na",
"endsWith": "končí na",
"isEmpty": "je prázdné",
"isNotEmpty": "není prázdné",
"equals": "je",
"notEquals": "není",
"greaterThan": "je větší než",
"greaterThanOrEqualTo": "je větší nebo rovno",
"lessThan": "je menší než",
"lessThanOrEqualTo": "je menší nebo rovno",
"inRange": "je mezi",
"isRelativeToToday": "je relativní ke dnešku",
"Date": {
"lessThan": "je před",
"lessThanOrEqualTo": "je nejpozději",
"greaterThan": "je po",
"greaterThanOrEqualTo": "je nejdříve"
}
}
}
}
}
```