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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

All notable changes to FastAdmin are documented in this file.

## Unreleased

### Features

- **Column labels**: new `list_display_labels` ModelAdmin attribute
(`{"field": "Header"}`) overrides or translates auto-generated list-page
column titles. Sorting, filtering and data binding stay keyed by the field
name.
- **Actions without selection**: `@action(requires_selection=False)` enables
the Apply button without selecting rows; the action receives an empty `ids`
list and should treat it as "all objects".

### Frontend

- **Apple-like design refresh**: frosted-glass sticky header with hairline
borders (the configured `primary_color` now serves as the accent instead of
the header background), larger continuous corner radii, softer diffuse
shadows, `#f5f5f7`/`#161617` layout backgrounds, tighter letter spacing and
transparent table headers. The rich-text editor accent follows
`primary_color` instead of a hardcoded teal.
- **Dependencies**: antd 6.5.1, i18next 26, react-i18next 17, vite 8, jsdom 29,
TypeScript 6.0, vitest 4.1.10, eslint 10.7; `quill-delta` added to satisfy a
peer dependency and `baseUrl` removed from `tsconfig.json` (deprecated in
TypeScript 6).

## 0.8.3

Correctness and security fixes from a full-codebase review. The only public API
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Overridable hooks (shared by model and inline admins):
| Export | Description |
| --- | --- |
| `@register(*models, **kwargs)` | Register a `ModelAdmin` subclass for the given model(s). For SQLAlchemy pass `sqlalchemy_sessionmaker=...`. |
| `@action(description=None)` | Mark an admin method as a bulk action (list it in `actions`). |
| `@action(description=None, requires_selection=True)` | Mark an admin method as a bulk action (list it in `actions`). With `requires_selection=False` the action can be applied without selecting rows and receives an empty `ids` list. |
| `@display(sorter=False)` | Mark an admin method as a computed list column (include it in `list_display`). |
| `@widget_action(...)` | Mark an admin method as a dashboard widget (list it in `widget_actions`). See [Dashboard widgets](guides/dashboard-widgets.md). |

Expand All @@ -94,7 +94,7 @@ Overridable hooks (shared by model and inline admins):

| Export | Description |
| --- | --- |
| `ActionInputSchema` | Input to `@action` handlers: `ids` of the selected objects. |
| `ActionInputSchema` | Input to `@action` handlers: `ids` of the selected objects (empty for actions with `requires_selection=False`). |
| `ActionResponseSchema` | Action result: `type` (`ActionResponseType`), `data`, optional `file_name`. |
| `WidgetActionInputSchema` | Input to `@widget_action` handlers: `query` — a list of `WidgetActionQuerySchema`. |
| `WidgetActionQuerySchema` | One filter/argument value: `field_name`, `widget_type`, `value`. |
Expand Down
12 changes: 12 additions & 0 deletions docs/guides/model-admins.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ attributes are optional and mirror Django Admin where possible.
| `list_display` | `()` | Fields shown as columns on the list page. Without it, a single `__str__` column is shown. |
| `list_display_links` | `()` | Which `list_display` fields link to the change page. |
| `list_display_widths` | `{}` | Column widths per field (`{"id": "100px"}`). |
| `list_display_labels` | `{}` | Column headers per field (`{"id": "Идентификатор"}`) — use to override or translate the auto-generated titles. |
| `list_filter` | `()` | Fields that get a filter in the table columns. |
| `list_per_page` | `10` | Items per paginated page. |
| `list_max_show_all` | `200` | Max total count for which a "Show all" link is displayed. |
Expand Down Expand Up @@ -75,6 +76,17 @@ class StoryAdmin(TortoiseModelAdmin):
An action can return `None`, a `MESSAGE` response, or a `DOWNLOAD_BASE64`
response (`data` is base64 file content, `file_name` names the download).

By default the Apply button is disabled until at least one row is selected.
Pass `requires_selection=False` to allow running an action without selecting
rows — the action then receives an empty `ids` list and should treat it as
"all objects":

```python
@action(description="Delete all stories", requires_selection=False)
async def delete_all(self, ids: list) -> None:
await Story.all().delete()
```

## Display fields

Add computed, read-only columns with the `@display` decorator and include
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export ADMIN_SECRET_KEY=secret_key
| `ADMIN_SITE_SIGN_IN_LOGO` | `/admin/static/images/sign-in-logo.svg` | Logo path on the sign-in page. |
| `ADMIN_SITE_HEADER_LOGO` | `/admin/static/images/header-logo.svg` | Logo path in the header. |
| `ADMIN_SITE_FAVICON` | `/admin/static/images/favicon.png` | Favicon path. |
| `ADMIN_PRIMARY_COLOR` | `#009485` | Primary UI color. |
| `ADMIN_PRIMARY_COLOR` | `#009485` | Accent color used for buttons, links, menu selection and other controls. |
| `ADMIN_SESSION_ID_KEY` | `admin_session_id` | Cookie key for the session id (HTTP-only). |
| `ADMIN_SESSION_EXPIRED_AT` | `144000` | Session lifetime in seconds. |
| `ADMIN_SESSION_COOKIE_SECURE` | `true` | Set the `Secure` flag so the session cookie is only sent over HTTPS. Set to `false` for local HTTP development. |
Expand Down
9 changes: 9 additions & 0 deletions fastadmin/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ class BaseModelAdmin:
# }
list_display_widths: dict[str, str] = {} # noqa: RUF012

# A dictionary containing the field names and the corresponding column labels (headers)
# for the list view. Use it to override or translate the auto-generated column titles.
# Example of usage:
# list_display_labels = {
# "id": "Идентификатор",
# "created_at": "Создано",
# }
list_display_labels: dict[str, str] = {} # noqa: RUF012

# Set list_filter to activate filters in the tabel columns of the list page of the admin.
# Example of usage: list_filter = ("is_superuser", "is_active", "created_at")
list_filter: Sequence[str] = ()
Expand Down
4 changes: 4 additions & 0 deletions fastadmin/models/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def action(
function=None,
*,
description: str | None = None,
requires_selection: bool = True,
):
"""Conveniently add attributes to an action function:

Expand All @@ -42,12 +43,15 @@ async def make_published(self, objs: list[Any]) -> ActionResponseSchema | None:

:param function: A function to decorate.
:param description: A string value to set the function's short_description
:param requires_selection: If False, the action can be applied without selecting rows
(the action function receives an empty ids list in that case).
"""

def decorator(func):
wrapped = _wrap_callable(func)

wrapped.is_action = True
wrapped.requires_selection = requires_selection
if description is not None:
wrapped.short_description = description
return wrapped
Expand Down
3 changes: 3 additions & 0 deletions fastadmin/models/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ async def generate_models_schema(
filter_widget_type=filter_widget_type,
filter_widget_props=filter_widget_props,
width=admin_model_obj.list_display_widths.get(field_name, None),
label=admin_model_obj.list_display_labels.get(field_name, None),
)

add_configuration = None
Expand Down Expand Up @@ -206,6 +207,7 @@ async def generate_models_schema(
filter_widget_type=None,
filter_widget_props=None,
width=admin_model_obj.list_display_widths.get(field_name, None),
label=admin_model_obj.list_display_labels.get(field_name, None),
),
add_configuration=None,
change_configuration=None,
Expand All @@ -231,6 +233,7 @@ async def generate_models_schema(
ModelAction(
name=action,
description=getattr(action_function, "short_description", None),
requires_selection=getattr(action_function, "requires_selection", True),
),
)

Expand Down
2 changes: 2 additions & 0 deletions fastadmin/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class ModelAction:

name: str
description: str | None
requires_selection: bool = True


@dataclass
Expand Down Expand Up @@ -143,6 +144,7 @@ class ListConfigurationFieldSchema:
empty_value_display: str
filter_widget_type: WidgetType | None
filter_widget_props: dict | None
label: str | None = None


@dataclass
Expand Down
3 changes: 2 additions & 1 deletion fastadmin/static/index.min.css

Large diffs are not rendered by default.

520 changes: 258 additions & 262 deletions fastadmin/static/index.min.js

Large diffs are not rendered by default.

31 changes: 16 additions & 15 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,49 @@
"dependencies": {
"@ant-design/charts": "^2.6.7",
"@tanstack/react-query": "^5.101.2",
"antd": "^6.5.0",
"antd": "^6.5.1",
"antd-img-crop": "^4.30.0",
"axios": "^1.18.1",
"getbase64data": "^1.0.8",
"i18next": "^25.10.10",
"i18next": "^26.3.6",
"js-file-download": "^0.4.12",
"lodash-es": "^4.18.1",
"lodash.debounce": "^4.0.8",
"query-string": "^9.4.1",
"quill-delta": "^5.1.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-i18next": "^16.6.6",
"react-i18next": "^17.0.10",
"react-phone-input-2": "^2.15.1",
"react-quill-new": "^3.8.3",
"react-router-dom": "^7.18.1",
"slugify": "^1.6.9"
},
"devDependencies": {
"@biomejs/biome": "^2.5.2",
"@biomejs/biome": "^2.5.4",
"@eslint/compat": "^2.1.0",
"@eslint/eslintrc": "^3.3.5",
"@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^10.0.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/lodash.debounce": "^4.0.9",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/parser": "^8.64.0",
"@vitejs/plugin-react-swc": "^4.3.1",
"@vitest/coverage-v8": "^4.1.9",
"eslint": "^10.6.0",
"globals": "^17.7.0",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.7.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"jsdom": "^28.1.0",
"typescript": "^5.9.3",
"vite": "^7.3.6",
"globals": "^17.7.0",
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
"vite": "^8.1.5",
"vite-plugin-environment": "^1.1.3",
"vitest": "^4.1.9"
"vitest": "^4.1.10"
},
"resolutions": {
"vite": "^7.3.6"
"vite": "^8.1.5"
}
}
51 changes: 38 additions & 13 deletions frontend/src/components/crud-container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
const { t: _t } = useTranslation("CrudContainer");

const {
token: { colorBgContainer, colorPrimary },
token: { colorBgContainer, colorText },
} = theme.useToken();

const hairline =
mode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.08)";

useEffect(() => {
if (!signedIn) {
navigate(ROUTES.SIGN_IN);
Expand Down Expand Up @@ -218,11 +221,19 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
<Layout style={{ minHeight: "100vh" }}>
<Header
style={{
background: colorPrimary,
background:
mode === "dark"
? "rgba(22, 22, 23, 0.8)"
: "rgba(255, 255, 255, 0.72)",
backdropFilter: "saturate(180%) blur(20px)",
WebkitBackdropFilter: "saturate(180%) blur(20px)",
borderBottom: `1px solid ${hairline}`,
position: "sticky",
top: 0,
zIndex: 100,
paddingInline: isMobile ? 0 : 20,
height: isMobile ? 48 : 56,
lineHeight: isMobile ? "48px" : "56px",
boxShadow: "0 1px 4px rgba(0, 0, 0, 0.08)",
}}
>
<Row
Expand Down Expand Up @@ -254,7 +265,7 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
icon: (
<BarsOutlined
style={{
color: mode === "dark" ? "white" : colorBgContainer,
color: colorText,
}}
/>
),
Expand Down Expand Up @@ -285,7 +296,7 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
/>
<span
style={{
color: mode === "dark" ? "white" : colorBgContainer,
color: colorText,
fontSize: 18,
fontWeight: 600,
letterSpacing: "-0.01em",
Expand Down Expand Up @@ -314,7 +325,7 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
icon: (
<SettingOutlined
style={{
color: mode === "dark" ? "white" : colorBgContainer,
color: colorText,
}}
/>
),
Expand Down Expand Up @@ -359,7 +370,7 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
style={{
background: colorBgContainer,
borderRadius: 0,
boxShadow: "1px 0 0 rgba(0, 0, 0, 0.06)",
borderRight: `1px solid ${hairline}`,
}}
>
<div style={{ padding: 16 }}>
Expand All @@ -368,10 +379,16 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
onChange={onSearch}
placeholder={_t("Search By Menu") as string}
prefix={
<SearchOutlined style={{ color: "rgba(0,0,0,0.25)" }} />
<SearchOutlined
style={{
color:
mode === "dark"
? "rgba(255, 255, 255, 0.3)"
: "rgba(0, 0, 0, 0.25)",
}}
/>
}
allowClear
style={{ borderRadius: 6 }}
/>
</div>
<Menu
Expand Down Expand Up @@ -420,7 +437,14 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
title={
<Row justify="space-between" align="middle">
<Col>
<Title style={{ margin: 0 }} level={5}>
<Title
style={{
margin: 0,
fontWeight: 600,
letterSpacing: "-0.02em",
}}
level={5}
>
{title}
</Title>
</Col>
Expand All @@ -429,10 +453,11 @@ export const CrudContainer: React.FC<ICrudContainer> = ({
}
style={{
marginTop: isMobile ? 4 : 0,
borderRadius: isMobile ? 6 : 8,
borderRadius: isMobile ? 12 : 16,
border: `1px solid ${hairline}`,
boxShadow: isMobile
? "0 1px 3px rgba(0, 0, 0, 0.05)"
: "0 1px 4px rgba(0, 0, 0, 0.06)",
? "0 1px 2px rgba(0, 0, 0, 0.03)"
: "0 1px 2px rgba(0, 0, 0, 0.03), 0 8px 24px rgba(0, 0, 0, 0.04)",
}}
>
<Skeleton loading={isLoading} active={true}>
Expand Down
Loading
Loading