Skip to content

Commit 6965930

Browse files
committed
Fin
1 parent 26079a8 commit 6965930

5 files changed

Lines changed: 125 additions & 68 deletions

File tree

fastapi_forge/frontend/components/model_row.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ def _build(self) -> None:
3636
)
3737
self.name_label.bind_visibility_from(self, "is_editing", lambda x: not x)
3838

39-
with ui.row().classes("gap-2"):
39+
with ui.row().classes("flex-nowrap gap-2 min-w-fit"):
4040
self.edit_button = (
4141
ui.button(
4242
icon="edit",
4343
)
4444
.on("click.stop", self._toggle_edit)
4545
.bind_visibility_from(self, "is_editing", lambda x: not x)
46+
.classes("min-w-fit")
4647
)
4748

4849
self.save_button = (
@@ -51,11 +52,14 @@ def _build(self) -> None:
5152
)
5253
.on("click.stop", self._save_model)
5354
.bind_visibility_from(self, "is_editing")
55+
.classes("min-w-fit")
5456
)
5557

5658
ui.button(
5759
icon="delete",
58-
).on("click.stop", lambda: state.delete_model(self.model))
60+
).on("click.stop", lambda: state.delete_model(self.model)).classes(
61+
"min-w-fit"
62+
)
5963

6064
def _toggle_edit(self) -> None:
6165
self.is_editing = not self.is_editing

fastapi_forge/frontend/panels/project_config_panel.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,41 @@ def _handle_builtin_auth_change(self, event: ValueChangeEventArguments) -> None:
200200
state.render_models_fn()
201201
ui.notify("The 'auth_user' model has been deleted.", type="positive")
202202

203+
async def _warn_override(self) -> bool:
204+
"""Show a confirmation dialog if the project already exists."""
205+
dialog = ui.dialog()
206+
with dialog, ui.card().classes("w-full max-w-md p-6 text-center"):
207+
ui.icon("warning", color="orange-500").classes("text-4xl self-center")
208+
ui.markdown(
209+
f"Project '{state.project_name}' already exists!\n\n"
210+
"This will **permanently overwrite** the existing project directory.\n"
211+
"Are you sure you want to continue?"
212+
).classes("text-center")
213+
214+
with ui.row().classes("w-full justify-center gap-4 mt-4"):
215+
ui.button("Cancel", color="primary", on_click=dialog.close)
216+
ui.button(
217+
"Overwrite", color="negative", on_click=lambda: dialog.submit(True)
218+
)
219+
220+
return await dialog
221+
203222
async def _create_project(self) -> None:
204-
"""Generate the project based on current state"""
223+
"""Generate the project based on the current state."""
224+
project_path = Path(state.project_name)
225+
226+
if project_path.exists():
227+
try:
228+
override = await self._warn_override()
229+
if not override:
230+
ui.notify("Project generation cancelled.", type="warning")
231+
return
232+
except Exception as e:
233+
ui.notify(f"Error displaying confirmation: {e}", type="negative")
234+
return
235+
205236
self.create_button.classes("hidden")
206237
self.loading_spinner.classes(remove="hidden")
207-
208238
ongoing_notification = ui.notification("Generating project...")
209239

210240
try:

fastapi_forge/frontend/state.py

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,24 @@
1010
ProjectSpec,
1111
)
1212
from fastapi_forge.enums import FieldDataType
13-
from fastapi_forge.frontend import notifications as n
13+
from fastapi_forge.frontend.notifications import (
14+
notify_model_exists,
15+
notify_something_went_wrong,
16+
notify_validation_error,
17+
)
1418

1519

1620
class ProjectState(BaseModel):
21+
"""Central state management for the project configuration."""
22+
1723
models: list[Model] = []
1824
selected_model: Model | None = None
1925
selected_field: ModelField | None = None
2026
selected_relation: ModelRelationship | None = None
2127

2228
render_models_fn: Callable | None = None
2329
render_model_editor_fn: Callable | None = None
24-
select_model_fn: Callable | None = None
30+
select_model_fn: Callable[[Model], None] | None = None
2531
deselect_model_fn: Callable | None = None
2632

2733
project_name: str = ""
@@ -32,7 +38,7 @@ class ProjectState(BaseModel):
3238
use_rabbitmq: bool = False
3339

3440
def initialize_from_project(self, project: ProjectSpec) -> None:
35-
"""Initialize the state from an existing project specification"""
41+
"""Initialize state from an existing project specification."""
3642
self.project_name = project.project_name
3743
self.use_postgres = project.use_postgres
3844
self.use_alembic = project.use_alembic
@@ -41,90 +47,62 @@ def initialize_from_project(self, project: ProjectSpec) -> None:
4147
self.use_rabbitmq = project.use_rabbitmq
4248
self.models = project.models.copy()
4349

44-
if self.render_models_fn:
45-
self.render_models_fn()
50+
self._trigger_ui_refresh()
4651

4752
def add_model(self, model_name: str) -> None:
48-
if self.render_models_fn is None:
49-
n.notify_something_went_wrong()
53+
"""Add a new model to the project."""
54+
if not self._validate_ui_callbacks():
5055
return
5156

52-
if any(model.name == model_name for model in self.models):
53-
n.notify_model_exists(model_name)
57+
if self._model_exists(model_name):
58+
notify_model_exists(model_name)
5459
return
5560

5661
try:
57-
default_id_field = ModelField(
58-
name="id",
59-
type=FieldDataType.UUID,
60-
primary_key=True,
61-
nullable=False,
62-
unique=True,
63-
index=True,
64-
)
65-
66-
new_model = Model(name=model_name, fields=[default_id_field])
67-
self.models.append(new_model)
68-
69-
self.render_models_fn()
70-
62+
self.models.append(self._create_default_model(model_name))
63+
self._trigger_ui_refresh()
7164
except ValidationError as exc:
72-
n.notify_validation_error(exc)
65+
notify_validation_error(exc)
7366

7467
def delete_model(self, model: Model) -> None:
75-
if (
76-
model not in self.models
77-
or self.deselect_model_fn is None
78-
or self.render_models_fn is None
79-
):
80-
ui.notify("Something went wrong...", type="warning")
68+
"""Remove a model from the project."""
69+
if not self._validate_model_operation(model):
8170
return
8271

8372
self.models.remove(model)
84-
self.deselect_model_fn()
85-
self.render_models_fn()
73+
self._cleanup_relationships_for_deleted_model(model.name)
74+
self._deselect_current_model()
75+
self._trigger_ui_refresh()
8676

8777
def update_model_name(self, model: Model, new_name: str) -> None:
78+
"""Rename an existing model."""
8879
if model.name == new_name:
8980
return
9081

91-
if any(m.name == new_name for m in self.models if m != model):
92-
n.notify_model_exists(new_name)
82+
if self._model_exists(new_name, exclude=model):
83+
notify_model_exists(new_name)
9384
return
9485

9586
old_name = model.name
9687
model.name = new_name
9788
self._update_relationships_for_rename(old_name, new_name)
9889

99-
if self.select_model_fn and model == self.selected_model:
90+
if model == self.selected_model and self.select_model_fn:
10091
self.select_model_fn(model)
10192

102-
if self.render_models_fn:
103-
self.render_models_fn()
104-
105-
self.render_model_editor_fn()
93+
self._trigger_ui_refresh()
10694

10795
def select_model(self, model: Model) -> None:
108-
if self.selected_model == model:
96+
"""Set the currently selected model."""
97+
if self.selected_model == model or not self._validate_ui_callbacks():
10998
return
110-
# print("selecting")
11199

112-
if (
113-
self.select_model_fn is None
114-
or self.render_models_fn is None
115-
or self.deselect_model_fn is None
116-
):
117-
n.notify_something_went_wrong()
118-
return
119100
self.selected_model = model
120-
self.select_model_fn(model)
121-
122-
if model not in self.models:
123-
self.deselect_model_fn()
124-
125-
self.render_models_fn()
101+
self.select_model_fn(model) # type: ignore
102+
self._trigger_ui_refresh()
126103

127104
def get_project_spec(self) -> ProjectSpec:
105+
"""Generate a ProjectSpec from the current state."""
128106
return ProjectSpec(
129107
project_name=self.project_name,
130108
use_postgres=self.use_postgres,
@@ -135,26 +113,70 @@ def get_project_spec(self) -> ProjectSpec:
135113
models=self.models,
136114
)
137115

138-
def _cleanup_relationships_for_deleted_model(
139-
self,
140-
deleted_model_name: str,
141-
) -> None:
116+
def _create_default_model(self, name: str) -> Model:
117+
"""Create a new model with default fields."""
118+
return Model(
119+
name=name,
120+
fields=[
121+
ModelField(
122+
name="id",
123+
type=FieldDataType.UUID,
124+
primary_key=True,
125+
nullable=False,
126+
unique=True,
127+
index=True,
128+
)
129+
],
130+
)
131+
132+
def _cleanup_relationships_for_deleted_model(self, deleted_model_name: str) -> None:
133+
"""Remove relationships pointing to deleted models."""
142134
for model in self.models:
143135
model.relationships = [
144136
rel
145137
for rel in model.relationships
146138
if rel.target_model != deleted_model_name
147139
]
148140

149-
def _update_relationships_for_rename(
150-
self,
151-
old_name: str,
152-
new_name: str,
153-
) -> None:
141+
def _update_relationships_for_rename(self, old_name: str, new_name: str) -> None:
142+
"""Update relationships when a model is renamed."""
154143
for model in self.models:
155144
for relationship in model.relationships:
156145
if relationship.target_model == old_name:
157146
relationship.target_model = new_name
158147

148+
def _model_exists(self, name: str, exclude: Model | None = None) -> bool:
149+
"""Check if a model with the given name already exists."""
150+
return any(model.name == name for model in self.models if model != exclude)
151+
152+
def _validate_ui_callbacks(self) -> bool:
153+
"""Verify required UI callbacks are set."""
154+
if not all([self.render_models_fn, self.select_model_fn]):
155+
notify_something_went_wrong()
156+
return False
157+
return True
158+
159+
def _validate_model_operation(self, model: Model) -> bool:
160+
"""Validate conditions for model operations."""
161+
if model not in self.models or not all(
162+
[self.deselect_model_fn, self.render_models_fn]
163+
):
164+
ui.notify("Something went wrong...", type="warning")
165+
return False
166+
return True
167+
168+
def _deselect_current_model(self) -> None:
169+
"""Clear current model selection."""
170+
if self.deselect_model_fn:
171+
self.deselect_model_fn()
172+
self.selected_model = None
173+
174+
def _trigger_ui_refresh(self) -> None:
175+
"""Refresh all relevant UI components."""
176+
if self.render_models_fn:
177+
self.render_models_fn()
178+
if self.render_model_editor_fn:
179+
self.render_model_editor_fn()
180+
159181

160182
state: ProjectState = ProjectState()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ ignore = [
7070
"TD003",
7171
"FIX002",
7272
"PLR0913",
73+
"PGH003",
7374
"S701",
7475
]
7576

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)