Fully functional implementation of the foreignKey

This commit is contained in:
2025-01-21 18:14:31 +01:00
committed by ewandor
parent 829d16c1c4
commit 38c5a69130
8 changed files with 41 additions and 33 deletions

View File

@@ -1,10 +1,10 @@
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from enum import Enum from enum import Enum
from sqlmodel import Field, SQLModel, select from sqlmodel import Field, SQLModel, select, Relationship
from pydantic import Field as PydField from pydantic import Field as PydField
from category.models import CategoryRead from category.models import CategoryRead, Category
class AccountType(Enum): class AccountType(Enum):
@@ -38,6 +38,7 @@ class AccountBaseId(AccountBase):
id: UUID | None = Field(default_factory=uuid4, primary_key=True) id: UUID | None = Field(default_factory=uuid4, primary_key=True)
class Account(AccountBaseId, table=True): class Account(AccountBaseId, table=True):
default_category: Category | None = Relationship()
@classmethod @classmethod
def create(cls, account, session): def create(cls, account, session):
@@ -50,7 +51,7 @@ class Account(AccountBaseId, table=True):
@classmethod @classmethod
def list(cls): def list(cls):
return select(Account) return select(Account).join(Category)
@classmethod @classmethod
def get(cls, session, account_id): def get(cls, session, account_id):
@@ -70,8 +71,7 @@ class Account(AccountBaseId, table=True):
session.commit() session.commit()
class AccountRead(AccountBaseId): class AccountRead(AccountBaseId):
# default_category: CategoryRead default_category: CategoryRead
pass
class AccountWrite(AccountBase): class AccountWrite(AccountBase):
default_category_id: UUID = PydField(default=None, json_schema_extra={ default_category_id: UUID = PydField(default=None, json_schema_extra={

View File

@@ -12,8 +12,8 @@ router = APIRouter()
@router.post("") @router.post("")
def create_account(account: AccountCreate, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead: def create_account(account: AccountCreate, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead:
Account.create(account, session) result = Account.create(account, session)
return account return result
@router.get("") @router.get("")
def read_accounts(session: SessionDep, current_user=Depends(get_current_user)) -> Page[AccountRead]: def read_accounts(session: SessionDep, current_user=Depends(get_current_user)) -> Page[AccountRead]:

View File

@@ -4,7 +4,6 @@ from fastapi import APIRouter, HTTPException, Depends
from fastapi_filter import FilterDepends from fastapi_filter import FilterDepends
from fastapi_pagination import Page from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate from fastapi_pagination.ext.sqlmodel import paginate
from pydantic import BaseModel
from category.models import Category, CategoryCreate, CategoryRead, CategoryUpdate, CategoryFilters from category.models import Category, CategoryCreate, CategoryRead, CategoryUpdate, CategoryFilters
from db import SessionDep from db import SessionDep

View File

@@ -6,7 +6,7 @@ from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models, ex
from fastapi_users.authentication import BearerTransport, AuthenticationBackend from fastapi_users.authentication import BearerTransport, AuthenticationBackend
from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy
from .models import User, get_user_db, AccessToken, get_access_token_db, UserRead, UserUpdate, UserCreate from user.models import User, get_user_db, AccessToken, get_access_token_db, UserRead, UserUpdate, UserCreate
from db import get_session from db import get_session
SECRET = "SECRET" SECRET = "SECRET"

View File

@@ -2,8 +2,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from .models import User, UserCreate, UserRead, UserUpdate from user.models import User, UserCreate, UserRead, UserUpdate
from .manager import get_user_manager, get_current_user, get_current_superuser from user.manager import get_user_manager, get_current_user, get_current_superuser
from fastapi_pagination import Page from fastapi_pagination import Page
from fastapi_pagination.ext.sqlmodel import paginate from fastapi_pagination.ext.sqlmodel import paginate

View File

@@ -1,8 +1,7 @@
import TextWidget from "@rjsf/core/lib/components/widgets/TextWidget"; import TextWidget from "@rjsf/core/lib/components/widgets/TextWidget";
import {FormContextType, getTemplate, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils"; import { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from "@rjsf/utils";
import {ForeignKeyWidget} from "./foreign-key";
import { ForeignKeyWidget } from "./foreign-key";
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>( export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F> props: WidgetProps<T, S, F>

View File

@@ -1,45 +1,55 @@
import { RJSFSchema, UiSchema, WidgetProps, RegistryWidgetsType } from '@rjsf/utils'; import { WidgetProps } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8'; import { Autocomplete } from "@mui/material";
import {Autocomplete, AutocompleteRenderInputParams, debounce} from "@mui/material"; import { useState, useEffect } from "react";
import {useAutocomplete} from "@refinedev/mui";
import { Controller } from "react-hook-form";
import {useState, useEffect, useCallback} from "react";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {axiosInstance} from "@refinedev/simple-rest"; import { useList, useOne } from "@refinedev/core";
import {useList} from "@refinedev/core";
export const ForeignKeyWidget = (props: WidgetProps) => { export const ForeignKeyWidget = (props: WidgetProps) => {
const [inputValue, setInputValue] = useState("");
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const [debouncedInputValue, setDebouncedInputValue] = useState(inputValue);
const resource = props.schema.foreign_key.reference.resource const resource = props.schema.foreign_key.reference.resource
const labelField = props.schema.foreign_key.reference.label
const valueResult = useOne({
resource: resource,
id: props.value
});
const [inputValue, setInputValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
return () => clearTimeout(handler); return () => clearTimeout(handler);
}, [inputValue]); }, [inputValue]);
const { data, isLoading } = useList({ const listResult = useList({
resource: resource, resource: resource,
pagination: { current: 1, pageSize: 10 }, pagination: { current: 1, pageSize: 10 },
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }], filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "name", order: "asc" }], sorters: [{ field: "name", order: "asc" }],
}); });
const options = data?.data || []; const options = listResult.data?.data || [];
const isLoading = listResult.isLoading || valueResult.isLoading;
if(! selectedValue && valueResult.data) {
setSelectedValue(valueResult.data?.data)
}
return ( return (
<Autocomplete <Autocomplete
value={selectedValue} value={selectedValue}
onChange={(event, newValue) => setSelectedValue(newValue)} onChange={(event, newValue) => {
inputValue={inputValue} setSelectedValue(newValue)
props.onChange(newValue ? newValue.id : "")
}}
//inputValue={inputValue}
onInputChange={(event, newInputValue) => setInputValue(newInputValue)} onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
options={options} options={options}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option ? option[labelField] : ""}
loading={isLoading} loading={isLoading}
renderInput={(params) => ( renderInput={(params) => (
<TextField {...params} label="Search" variant="outlined" /> <TextField {...params} label={ props.label } variant="outlined" />
)} )}
/> />
); );

View File

@@ -55,7 +55,7 @@ export const dataProvider: DataProvider = {
if (filters && filters.length > 0) { if (filters && filters.length > 0) {
filters.forEach((filter) => { filters.forEach((filter) => {
if ("field" in filter && filter.value.length > 0 && filter.operator === "contains") { if ("field" in filter && filter.value && filter.operator === "contains") {
params.append(filter.field + "__like", "%" + filter.value + "%"); params.append(filter.field + "__like", "%" + filter.value + "%");
} }
}); });