- Fix delete_proj not persisting cleared current_project to config: page reload after deleting active project restored deleted name, silently breaking all DB sync - Fix sync_to_db crash on non-dict batch_data items: add isinstance guard matching import_json_file - Fix output_types ignored in load_dynamic: parse declared types and use to_int()/to_float() to coerce values, so downstream ComfyUI nodes receive correct types even when API returns strings - Fix backward-compat comma-split for types not trimming whitespace: legacy workflows with "STRING, INT" got types " INT" breaking ComfyUI connection type-matching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
7.2 KiB
Python
166 lines
7.2 KiB
Python
import logging
|
|
from pathlib import Path
|
|
|
|
from nicegui import ui
|
|
|
|
from state import AppState
|
|
from db import ProjectDB
|
|
from utils import save_config, sync_to_db, KEY_BATCH_DATA
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def render_projects_tab(state: AppState):
|
|
"""Render the Projects management tab."""
|
|
|
|
# --- DB toggle ---
|
|
def on_db_toggle(e):
|
|
state.db_enabled = e.value
|
|
state.config['db_enabled'] = e.value
|
|
save_config(state.current_dir, state.config.get('favorites', []), state.config)
|
|
render_project_content.refresh()
|
|
|
|
ui.switch('Enable Project Database', value=state.db_enabled,
|
|
on_change=on_db_toggle).classes('q-mb-md')
|
|
|
|
@ui.refreshable
|
|
def render_project_content():
|
|
if not state.db_enabled:
|
|
ui.label('Project database is disabled. Enable it above to manage projects.').classes(
|
|
'text-caption q-pa-md')
|
|
return
|
|
|
|
if not state.db:
|
|
ui.label('Database not initialized.').classes('text-warning q-pa-md')
|
|
return
|
|
|
|
# --- Create project form ---
|
|
with ui.card().classes('w-full q-pa-md q-mb-md'):
|
|
ui.label('Create New Project').classes('section-header')
|
|
name_input = ui.input('Project Name', placeholder='my_project').classes('w-full')
|
|
desc_input = ui.input('Description (optional)', placeholder='A short description').classes('w-full')
|
|
|
|
def create_project():
|
|
name = name_input.value.strip()
|
|
if not name:
|
|
ui.notify('Please enter a project name', type='warning')
|
|
return
|
|
try:
|
|
state.db.create_project(name, str(state.current_dir), desc_input.value.strip())
|
|
name_input.set_value('')
|
|
desc_input.set_value('')
|
|
ui.notify(f'Created project "{name}"', type='positive')
|
|
render_project_list.refresh()
|
|
except Exception as e:
|
|
ui.notify(f'Error: {e}', type='negative')
|
|
|
|
ui.button('Create Project', icon='add', on_click=create_project).classes('w-full')
|
|
|
|
# --- Active project indicator ---
|
|
if state.current_project:
|
|
ui.label(f'Active Project: {state.current_project}').classes(
|
|
'text-bold text-primary q-pa-sm')
|
|
|
|
# --- Project list ---
|
|
@ui.refreshable
|
|
def render_project_list():
|
|
projects = state.db.list_projects()
|
|
if not projects:
|
|
ui.label('No projects yet. Create one above.').classes('text-caption q-pa-md')
|
|
return
|
|
|
|
for proj in projects:
|
|
is_active = proj['name'] == state.current_project
|
|
card_style = 'border-left: 3px solid var(--accent);' if is_active else ''
|
|
|
|
with ui.card().classes('w-full q-pa-sm q-mb-sm').style(card_style):
|
|
with ui.row().classes('w-full items-center'):
|
|
with ui.column().classes('col'):
|
|
ui.label(proj['name']).classes('text-bold')
|
|
if proj['description']:
|
|
ui.label(proj['description']).classes('text-caption')
|
|
ui.label(f'Path: {proj["folder_path"]}').classes('text-caption')
|
|
files = state.db.list_data_files(proj['id'])
|
|
ui.label(f'{len(files)} data file(s)').classes('text-caption')
|
|
|
|
with ui.row().classes('q-gutter-xs'):
|
|
if not is_active:
|
|
def activate(name=proj['name']):
|
|
state.current_project = name
|
|
state.config['current_project'] = name
|
|
save_config(state.current_dir,
|
|
state.config.get('favorites', []),
|
|
state.config)
|
|
ui.notify(f'Activated project "{name}"', type='positive')
|
|
render_project_list.refresh()
|
|
|
|
ui.button('Activate', icon='check_circle',
|
|
on_click=activate).props('flat dense color=primary')
|
|
else:
|
|
def deactivate():
|
|
state.current_project = ''
|
|
state.config['current_project'] = ''
|
|
save_config(state.current_dir,
|
|
state.config.get('favorites', []),
|
|
state.config)
|
|
ui.notify('Deactivated project', type='info')
|
|
render_project_list.refresh()
|
|
|
|
ui.button('Deactivate', icon='cancel',
|
|
on_click=deactivate).props('flat dense')
|
|
|
|
def import_folder(pid=proj['id'], pname=proj['name']):
|
|
_import_folder(state, pid, pname, render_project_list)
|
|
|
|
ui.button('Import Folder', icon='folder_open',
|
|
on_click=import_folder).props('flat dense')
|
|
|
|
def delete_proj(name=proj['name']):
|
|
state.db.delete_project(name)
|
|
if state.current_project == name:
|
|
state.current_project = ''
|
|
state.config['current_project'] = ''
|
|
save_config(state.current_dir,
|
|
state.config.get('favorites', []),
|
|
state.config)
|
|
ui.notify(f'Deleted project "{name}"', type='positive')
|
|
render_project_list.refresh()
|
|
|
|
ui.button(icon='delete',
|
|
on_click=delete_proj).props('flat dense color=negative')
|
|
|
|
render_project_list()
|
|
|
|
render_project_content()
|
|
|
|
|
|
def _import_folder(state: AppState, project_id: int, project_name: str, refresh_fn):
|
|
"""Bulk import all .json files from current directory into a project."""
|
|
json_files = sorted(state.current_dir.glob('*.json'))
|
|
json_files = [f for f in json_files if f.name not in (
|
|
'.editor_config.json', '.editor_snippets.json')]
|
|
|
|
if not json_files:
|
|
ui.notify('No JSON files in current directory', type='warning')
|
|
return
|
|
|
|
imported = 0
|
|
skipped = 0
|
|
for jf in json_files:
|
|
file_name = jf.stem
|
|
existing = state.db.get_data_file(project_id, file_name)
|
|
if existing:
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
state.db.import_json_file(project_id, jf)
|
|
imported += 1
|
|
except Exception as e:
|
|
logger.warning(f"Failed to import {jf}: {e}")
|
|
|
|
msg = f'Imported {imported} file(s)'
|
|
if skipped:
|
|
msg += f', skipped {skipped} existing'
|
|
ui.notify(msg, type='positive')
|
|
refresh_fn.refresh()
|