Skip to content

update opened_at to be nullable #7767

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 19 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6f5ab25
allow opened_at to be nullable for workflows that the user has never …
Mar 11, 2025
a40e5f4
update recent workflows UI
Mar 11, 2025
e5f99a3
fix(ui): rebase broke things
psychedelicious Mar 11, 2025
74a4953
fix(ui): other stuff borked by rebase
psychedelicious Mar 11, 2025
d174c14
perf(ui): memoize workflow library components
psychedelicious Mar 11, 2025
262aaf8
tweak(ui): align new and upload workflow buttons
psychedelicious Mar 11, 2025
7e63cdf
feat(app): drop and recreate index on opened_at
psychedelicious Mar 11, 2025
09f2794
fix(ui): memo WorkflowLibraryModal
psychedelicious Mar 11, 2025
1d7491e
tweak(app): 'is_recent' -> 'has_been_opened'
psychedelicious Mar 11, 2025
fe5ed74
chore(ui): typegen
psychedelicious Mar 11, 2025
932e9b6
tweak(ui): 'is_recent' -> 'has_been_opened'
psychedelicious Mar 11, 2025
0b5210d
tidy(app): remove unused method in workflow records service
psychedelicious Mar 11, 2025
3273615
feat(app): add method and route to get workflow library counts by cat…
psychedelicious Mar 11, 2025
9574bba
chore(ui): typegen
psychedelicious Mar 11, 2025
7cc0d6b
feat(ui): on first load, if the selected library view has no workflow…
psychedelicious Mar 11, 2025
a15235c
chore(ui): lint
psychedelicious Mar 12, 2025
d21a497
feat(db): drop the opened_at column instead of marking deprecated
psychedelicious Mar 12, 2025
1280cc6
fix(ui): default categories for oss
psychedelicious Mar 12, 2025
e1f1aaf
fix(ui): mark workflow as opened when creating a new workflow
psychedelicious Mar 12, 2025
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
21 changes: 19 additions & 2 deletions invokeai/app/api/routers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ async def list_workflows(
categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories of workflow to get"),
tags: Optional[list[str]] = Query(default=None, description="The tags of workflow to get"),
query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]:
"""Gets a page of workflows"""
workflows_with_thumbnails: list[WorkflowRecordListItemWithThumbnailDTO] = []
Expand All @@ -116,6 +117,7 @@ async def list_workflows(
query=query,
categories=categories,
tags=tags,
has_been_opened=has_been_opened,
)
for workflow in workflows.items:
workflows_with_thumbnails.append(
Expand Down Expand Up @@ -225,10 +227,25 @@ async def get_workflow_thumbnail(
async def get_counts_by_tag(
tags: list[str] = Query(description="The tags to get counts for"),
categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories to include"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> dict[str, int]:
"""Gets tag counts with a filter"""
"""Counts workflows by tag"""

return ApiDependencies.invoker.services.workflow_records.counts_by_tag(tags=tags, categories=categories)
return ApiDependencies.invoker.services.workflow_records.counts_by_tag(
tags=tags, categories=categories, has_been_opened=has_been_opened
)


@workflows_router.get("/counts_by_category", operation_id="counts_by_category")
async def counts_by_category(
categories: list[WorkflowCategory] = Query(description="The categories to include"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
) -> dict[str, int]:
"""Counts workflows by category"""

return ApiDependencies.invoker.services.workflow_records.counts_by_category(
categories=categories, has_been_opened=has_been_opened
)


@workflows_router.put(
Expand Down
2 changes: 2 additions & 0 deletions invokeai/app/services/shared/sqlite/sqlite_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_16 import build_migration_16
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_17 import build_migration_17
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_18 import build_migration_18
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator


Expand Down Expand Up @@ -57,6 +58,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_15())
migrator.register_migration(build_migration_16())
migrator.register_migration(build_migration_17())
migrator.register_migration(build_migration_18())
migrator.run_migrations()

return db
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import sqlite3

from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration


class Migration18Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._make_workflow_opened_at_nullable(cursor)

def _make_workflow_opened_at_nullable(self, cursor: sqlite3.Cursor) -> None:
"""
Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
- Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
- Dropping the existing `opened_at` column
- Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
- Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
"""
# For index renaming in SQLite, we need to drop and recreate
cursor.execute("DROP INDEX IF EXISTS idx_workflow_library_opened_at;")
# Rename existing column to deprecated
cursor.execute("ALTER TABLE workflow_library DROP COLUMN opened_at;")
# Add new nullable column - all values will be NULL - no migration of data needed
cursor.execute("ALTER TABLE workflow_library ADD COLUMN opened_at DATETIME;")
# Create new index on the new column
cursor.execute(
"CREATE INDEX idx_workflow_library_opened_at ON workflow_library(opened_at);",
)


def build_migration_18() -> Migration:
"""
Build the migration from database version 17 to 18.

This migration does the following:
- Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
- Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
- Dropping the existing `opened_at` column
- Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
- Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
"""
migration_18 = Migration(
from_version=17,
to_version=18,
callback=Migration18Callback(),
)

return migration_18
11 changes: 11 additions & 0 deletions invokeai/app/services/workflow_records/workflow_records_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,26 @@ def get_many(
per_page: Optional[int],
query: Optional[str],
tags: Optional[list[str]],
has_been_opened: Optional[bool],
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets many workflows."""
pass

@abstractmethod
def counts_by_category(
self,
categories: list[WorkflowCategory],
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
"""Gets a dictionary of counts for each of the provided categories."""
pass

@abstractmethod
def counts_by_tag(
self,
tags: list[str],
categories: Optional[list[WorkflowCategory]] = None,
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
"""Gets a dictionary of counts for each of the provided tags."""
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from enum import Enum
from typing import Any, Union
from typing import Any, Optional, Union

import semver
from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, field_validator
Expand Down Expand Up @@ -98,7 +98,7 @@ class WorkflowRecordDTOBase(BaseModel):
name: str = Field(description="The name of the workflow.")
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
opened_at: Optional[Union[datetime.datetime, str]] = Field(description="The opened timestamp of the workflow.")


class WorkflowRecordDTO(WorkflowRecordDTOBase):
Expand Down
46 changes: 26 additions & 20 deletions invokeai/app/services/workflow_records/workflow_records_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def get_many(
per_page: Optional[int] = None,
query: Optional[str] = None,
tags: Optional[list[str]] = None,
has_been_opened: Optional[bool] = None,
) -> PaginatedResults[WorkflowRecordListItemDTO]:
# sanitize!
assert order_by in WorkflowRecordOrderBy
Expand Down Expand Up @@ -175,6 +176,11 @@ def get_many(
conditions.append(tags_condition)
params.extend(tags_params)

if has_been_opened:
conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
conditions.append("opened_at IS NULL")

# Ignore whitespace in the query
stripped_query = query.strip() if query else None
if stripped_query:
Expand Down Expand Up @@ -234,6 +240,7 @@ def counts_by_tag(
self,
tags: list[str],
categories: Optional[list[WorkflowCategory]] = None,
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
if not tags:
return {}
Expand All @@ -251,6 +258,11 @@ def counts_by_tag(
base_conditions.append(f"category IN ({placeholders})")
base_params.extend([category.value for category in categories])

if has_been_opened:
base_conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
base_conditions.append("opened_at IS NULL")

# For each tag to count, run a separate query
for tag in tags:
# Start with the base conditions
Expand All @@ -276,20 +288,14 @@ def counts_by_tag(

return result

def get_tag_counts_with_filter(
def counts_by_category(
self,
tags_to_count: list[str],
selected_tags: Optional[list[str]] = None,
categories: Optional[list[WorkflowCategory]] = None,
categories: list[WorkflowCategory],
has_been_opened: Optional[bool] = None,
) -> dict[str, int]:
if not tags_to_count:
return {}

cursor = self._conn.cursor()
result: dict[str, int] = {}
selected_tags = selected_tags or []

# Base conditions for categories and selected tags
# Base conditions for categories
base_conditions: list[str] = []
base_params: list[str | int] = []

Expand All @@ -300,20 +306,20 @@ def get_tag_counts_with_filter(
base_conditions.append(f"category IN ({placeholders})")
base_params.extend([category.value for category in categories])

# Add selected tags conditions (AND logic)
for tag in selected_tags:
base_conditions.append("tags LIKE ?")
base_params.append(f"%{tag.strip()}%")
if has_been_opened:
base_conditions.append("opened_at IS NOT NULL")
elif has_been_opened is False:
base_conditions.append("opened_at IS NULL")

# For each tag to count, run a separate query
for tag in tags_to_count:
# For each category to count, run a separate query
for category in categories:
# Start with the base conditions
conditions = base_conditions.copy()
params = base_params.copy()

# Add this specific tag condition
conditions.append("tags LIKE ?")
params.append(f"%{tag.strip()}%")
# Add this specific category condition
conditions.append("category = ?")
params.append(category.value)

# Construct the full query
stmt = """--sql
Expand All @@ -326,7 +332,7 @@ def get_tag_counts_with_filter(

cursor.execute(stmt, params)
count = cursor.fetchone()[0]
result[tag] = count
result[category.value] = count

return result

Expand Down
2 changes: 2 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,7 @@
"filterByTags": "Filter by Tags",
"yourWorkflows": "Your Workflows",
"recentlyOpened": "Recently Opened",
"noRecentWorkflows": "No Recent Workflows",
"private": "Private",
"shared": "Shared",
"browseWorkflows": "Browse Workflows",
Expand Down Expand Up @@ -1726,6 +1727,7 @@
"loadWorkflow": "$t(common.load) Workflow",
"autoLayout": "Auto Layout",
"edit": "Edit",
"view": "View",
"download": "Download",
"copyShareLink": "Copy Share Link",
"copyShareLinkForWorkflow": "Copy Share Link for Workflow",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export const ViewWorkflow = ({ workflowId }: { workflowId: string }) => {
);

return (
<Tooltip label={t('workflows.edit')} closeOnScroll>
<Tooltip label={t('workflows.view')} closeOnScroll>
<IconButton
size="sm"
variant="ghost"
aria-label={t('workflows.edit')}
aria-label={t('workflows.view')}
onClick={handleClickLoad}
icon={<PiEyeBold />}
/>
Expand Down
Loading