diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
index 926c0f7fd22..d5b1acb5144 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -5,7 +5,7 @@
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
-from invokeai.app.services.board_records.board_records_common import BoardChanges
+from invokeai.app.services.board_records.board_records_common import BoardChanges, UncategorizedImageCounts
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -146,3 +146,14 @@ async def list_all_board_image_names(
board_id,
)
return image_names
+
+
+@boards_router.get(
+ "/uncategorized/counts",
+ operation_id="get_uncategorized_image_counts",
+ response_model=UncategorizedImageCounts,
+)
+async def get_uncategorized_image_counts() -> UncategorizedImageCounts:
+ """Gets count of images and assets for uncategorized images (images with no board assocation)"""
+
+ return ApiDependencies.invoker.services.board_records.get_uncategorized_image_counts()
diff --git a/invokeai/app/services/board_records/board_records_base.py b/invokeai/app/services/board_records/board_records_base.py
index 9d16dacf60b..7bfe6ada6fd 100644
--- a/invokeai/app/services/board_records/board_records_base.py
+++ b/invokeai/app/services/board_records/board_records_base.py
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
-from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord, UncategorizedImageCounts
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -48,3 +48,8 @@ def get_many(
def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
"""Gets all board records."""
pass
+
+ @abstractmethod
+ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
+ """Gets count of images and assets for uncategorized images (images with no board assocation)."""
+ pass
diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py
index 0dda8a8b6b6..3478746536f 100644
--- a/invokeai/app/services/board_records/board_records_common.py
+++ b/invokeai/app/services/board_records/board_records_common.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import Optional, Union
+from typing import Any, Optional, Union
from pydantic import BaseModel, Field
@@ -26,21 +26,25 @@ class BoardRecord(BaseModelExcludeNull):
"""Whether or not the board is archived."""
is_private: Optional[bool] = Field(default=None, description="Whether the board is private.")
"""Whether the board is private."""
+ image_count: int = Field(description="The number of images in the board.")
+ asset_count: int = Field(description="The number of assets in the board.")
-def deserialize_board_record(board_dict: dict) -> BoardRecord:
+def deserialize_board_record(board_dict: dict[str, Any]) -> BoardRecord:
"""Deserializes a board record."""
# Retrieve all the values, setting "reasonable" defaults if they are not present.
board_id = board_dict.get("board_id", "unknown")
board_name = board_dict.get("board_name", "unknown")
- cover_image_name = board_dict.get("cover_image_name", "unknown")
+ cover_image_name = board_dict.get("cover_image_name", None)
created_at = board_dict.get("created_at", get_iso_timestamp())
updated_at = board_dict.get("updated_at", get_iso_timestamp())
deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
archived = board_dict.get("archived", False)
is_private = board_dict.get("is_private", False)
+ image_count = board_dict.get("image_count", 0)
+ asset_count = board_dict.get("asset_count", 0)
return BoardRecord(
board_id=board_id,
@@ -51,6 +55,8 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
deleted_at=deleted_at,
archived=archived,
is_private=is_private,
+ image_count=image_count,
+ asset_count=asset_count,
)
@@ -63,19 +69,24 @@ class BoardChanges(BaseModel, extra="forbid"):
class BoardRecordNotFoundException(Exception):
"""Raised when an board record is not found."""
- def __init__(self, message="Board record not found"):
+ def __init__(self, message: str = "Board record not found"):
super().__init__(message)
class BoardRecordSaveException(Exception):
"""Raised when an board record cannot be saved."""
- def __init__(self, message="Board record not saved"):
+ def __init__(self, message: str = "Board record not saved"):
super().__init__(message)
class BoardRecordDeleteException(Exception):
"""Raised when an board record cannot be deleted."""
- def __init__(self, message="Board record not deleted"):
+ def __init__(self, message: str = "Board record not deleted"):
super().__init__(message)
+
+
+class UncategorizedImageCounts(BaseModel):
+ image_count: int = Field(description="The number of uncategorized images.")
+ asset_count: int = Field(description="The number of uncategorized assets.")
diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py
index c64e060b953..ea02d3e4b21 100644
--- a/invokeai/app/services/board_records/board_records_sqlite.py
+++ b/invokeai/app/services/board_records/board_records_sqlite.py
@@ -9,12 +9,121 @@
BoardRecordDeleteException,
BoardRecordNotFoundException,
BoardRecordSaveException,
+ UncategorizedImageCounts,
deserialize_board_record,
)
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.util.misc import uuid_string
+_BASE_BOARD_RECORD_QUERY = """
+ -- This query retrieves board records, joining with the board_images and images tables to get image counts and cover image names.
+ -- It is not a complete query, as it is missing a GROUP BY or WHERE clause (and is unterminated).
+ SELECT b.board_id,
+ b.board_name,
+ b.created_at,
+ b.updated_at,
+ b.archived,
+ -- Count the number of images in the board, alias image_count
+ COUNT(
+ CASE
+ WHEN i.image_category in ('general') -- "Images" are images in the 'general' category
+ AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
+ END
+ ) AS image_count,
+ -- Count the number of assets in the board, alias asset_count
+ COUNT(
+ CASE
+ WHEN i.image_category in ('control', 'mask', 'user', 'other') -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
+ AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
+ END
+ ) AS asset_count,
+ -- Get the name of the the most recent image in the board, alias cover_image_name
+ (
+ SELECT bi.image_name
+ FROM board_images bi
+ JOIN images i ON bi.image_name = i.image_name
+ WHERE bi.board_id = b.board_id
+ AND i.is_intermediate = 0 -- Intermediates cannot be cover images
+ ORDER BY i.created_at DESC -- Sort by created_at to get the most recent image
+ LIMIT 1
+ ) AS cover_image_name
+ FROM boards b
+ LEFT JOIN board_images bi ON b.board_id = bi.board_id
+ LEFT JOIN images i ON bi.image_name = i.image_name
+ """
+
+
+def get_paginated_list_board_records_queries(include_archived: bool) -> str:
+ """Gets a query to retrieve a paginated list of board records. The query has placeholders for limit and offset.
+
+ Args:
+ include_archived: Whether to include archived board records in the results.
+
+ Returns:
+ A query to retrieve a paginated list of board records.
+ """
+
+ archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+ # The GROUP BY must be added _after_ the WHERE clause!
+ query = f"""
+ {_BASE_BOARD_RECORD_QUERY}
+ {archived_condition}
+ GROUP BY b.board_id,
+ b.board_name,
+ b.created_at,
+ b.updated_at
+ ORDER BY b.created_at DESC
+ LIMIT ? OFFSET ?;
+ """
+
+ return query
+
+
+def get_total_boards_count_query(include_archived: bool) -> str:
+ """Gets a query to retrieve the total count of board records.
+
+ Args:
+ include_archived: Whether to include archived board records in the count.
+
+ Returns:
+ A query to retrieve the total count of board records.
+ """
+
+ archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+ return f"SELECT COUNT(*) FROM boards {archived_condition};"
+
+
+def get_list_all_board_records_query(include_archived: bool) -> str:
+ """Gets a query to retrieve all board records.
+
+ Args:
+ include_archived: Whether to include archived board records in the results.
+
+ Returns:
+ A query to retrieve all board records.
+ """
+
+ archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+ return f"""
+ {_BASE_BOARD_RECORD_QUERY}
+ {archived_condition}
+ GROUP BY b.board_id,
+ b.board_name,
+ b.created_at,
+ b.updated_at
+ ORDER BY b.created_at DESC;
+ """
+
+
+def get_board_record_query() -> str:
+ """Gets a query to retrieve a board record. The query has a placeholder for the board_id."""
+
+ return f"{_BASE_BOARD_RECORD_QUERY} WHERE b.board_id = ?;"
+
class SqliteBoardRecordStorage(BoardRecordStorageBase):
_conn: sqlite3.Connection
@@ -76,11 +185,7 @@ def get(
try:
self._lock.acquire()
self._cursor.execute(
- """--sql
- SELECT *
- FROM boards
- WHERE board_id = ?;
- """,
+ get_board_record_query(),
(board_id,),
)
@@ -92,7 +197,7 @@ def get(
self._lock.release()
if result is None:
raise BoardRecordNotFoundException
- return BoardRecord(**dict(result))
+ return deserialize_board_record(dict(result))
def update(
self,
@@ -149,45 +254,15 @@ def get_many(
try:
self._lock.acquire()
- # Build base query
- base_query = """
- SELECT *
- FROM boards
- {archived_filter}
- ORDER BY created_at DESC
- LIMIT ? OFFSET ?;
- """
-
- # Determine archived filter condition
- if include_archived:
- archived_filter = ""
- else:
- archived_filter = "WHERE archived = 0"
-
- final_query = base_query.format(archived_filter=archived_filter)
+ main_query = get_paginated_list_board_records_queries(include_archived=include_archived)
- # Execute query to fetch boards
- self._cursor.execute(final_query, (limit, offset))
+ self._cursor.execute(main_query, (limit, offset))
result = cast(list[sqlite3.Row], self._cursor.fetchall())
boards = [deserialize_board_record(dict(r)) for r in result]
- # Determine count query
- if include_archived:
- count_query = """
- SELECT COUNT(*)
- FROM boards;
- """
- else:
- count_query = """
- SELECT COUNT(*)
- FROM boards
- WHERE archived = 0;
- """
-
- # Execute count query
- self._cursor.execute(count_query)
-
+ total_query = get_total_boards_count_query(include_archived=include_archived)
+ self._cursor.execute(total_query)
count = cast(int, self._cursor.fetchone()[0])
return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count)
@@ -201,26 +276,10 @@ def get_many(
def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
try:
self._lock.acquire()
-
- base_query = """
- SELECT *
- FROM boards
- {archived_filter}
- ORDER BY created_at DESC
- """
-
- if include_archived:
- archived_filter = ""
- else:
- archived_filter = "WHERE archived = 0"
-
- final_query = base_query.format(archived_filter=archived_filter)
-
- self._cursor.execute(final_query)
-
+ query = get_list_all_board_records_query(include_archived=include_archived)
+ self._cursor.execute(query)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
boards = [deserialize_board_record(dict(r)) for r in result]
-
return boards
except sqlite3.Error as e:
@@ -228,3 +287,28 @@ def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
raise e
finally:
self._lock.release()
+
+ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
+ try:
+ self._lock.acquire()
+ query = """
+ -- Get the count of uncategorized images and assets.
+ SELECT
+ CASE
+ WHEN i.image_category = 'general' THEN 'image_count' -- "Images" are images in the 'general' category
+ ELSE 'asset_count' -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
+ END AS category_type,
+ COUNT(*) AS unassigned_count
+ FROM images i
+ LEFT JOIN board_images bi ON i.image_name = bi.image_name
+ WHERE bi.board_id IS NULL -- Uncategorized images have no board association
+ AND i.is_intermediate = 0 -- Omit intermediates from the counts
+ GROUP BY category_type; -- Group by category_type alias, as derived from the image_category column earlier
+ """
+ self._cursor.execute(query)
+ results = self._cursor.fetchall()
+ image_count = dict(results)["image_count"]
+ asset_count = dict(results)["asset_count"]
+ return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count)
+ finally:
+ self._lock.release()
diff --git a/invokeai/app/services/boards/boards_common.py b/invokeai/app/services/boards/boards_common.py
index 15d0b3c37f5..1e9337a3edf 100644
--- a/invokeai/app/services/boards/boards_common.py
+++ b/invokeai/app/services/boards/boards_common.py
@@ -1,23 +1,8 @@
-from typing import Optional
-
-from pydantic import Field
-
from invokeai.app.services.board_records.board_records_common import BoardRecord
+# TODO(psyche): BoardDTO is now identical to BoardRecord. We should consider removing it.
class BoardDTO(BoardRecord):
- """Deserialized board record with cover image URL and image count."""
-
- cover_image_name: Optional[str] = Field(description="The name of the board's cover image.")
- """The URL of the thumbnail of the most recent image in the board."""
- image_count: int = Field(description="The number of images in the board.")
- """The number of images in the board."""
-
+ """Deserialized board record."""
-def board_record_to_dto(board_record: BoardRecord, cover_image_name: Optional[str], image_count: int) -> BoardDTO:
- """Converts a board record to a board DTO."""
- return BoardDTO(
- **board_record.model_dump(exclude={"cover_image_name"}),
- cover_image_name=cover_image_name,
- image_count=image_count,
- )
+ pass
diff --git a/invokeai/app/services/boards/boards_default.py b/invokeai/app/services/boards/boards_default.py
index 97fd3059a93..abf38e8ea71 100644
--- a/invokeai/app/services/boards/boards_default.py
+++ b/invokeai/app/services/boards/boards_default.py
@@ -1,6 +1,6 @@
from invokeai.app.services.board_records.board_records_common import BoardChanges
from invokeai.app.services.boards.boards_base import BoardServiceABC
-from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto
+from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -16,17 +16,11 @@ def create(
board_name: str,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.save(board_name)
- return board_record_to_dto(board_record, None, 0)
+ return BoardDTO.model_validate(board_record.model_dump())
def get_dto(self, board_id: str) -> BoardDTO:
board_record = self.__invoker.services.board_records.get(board_id)
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
- return board_record_to_dto(board_record, cover_image_name, image_count)
+ return BoardDTO.model_validate(board_record.model_dump())
def update(
self,
@@ -34,14 +28,7 @@ def update(
changes: BoardChanges,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.update(board_id, changes)
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
- return board_record_to_dto(board_record, cover_image_name, image_count)
+ return BoardDTO.model_validate(board_record.model_dump())
def delete(self, board_id: str) -> None:
self.__invoker.services.board_records.delete(board_id)
@@ -50,30 +37,10 @@ def get_many(
self, offset: int = 0, limit: int = 10, include_archived: bool = False
) -> OffsetPaginatedResults[BoardDTO]:
board_records = self.__invoker.services.board_records.get_many(offset, limit, include_archived)
- board_dtos = []
- for r in board_records.items:
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
- board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
-
+ board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records.items]
return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
def get_all(self, include_archived: bool = False) -> list[BoardDTO]:
board_records = self.__invoker.services.board_records.get_all(include_archived)
- board_dtos = []
- for r in board_records:
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
- board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
-
+ board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records]
return board_dtos
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
index 63ba2991cfc..e3b30666b0e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
@@ -1,27 +1,23 @@
import { Flex, Image, Text } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
+import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-import type { BoardDTO } from 'services/api/types';
type Props = {
- board: BoardDTO | null;
+ imageCount: number;
+ assetCount: number;
+ isArchived: boolean;
+ coverImageName?: string | null;
};
-export const BoardTooltip = ({ board }: Props) => {
+export const BoardTooltip = ({ imageCount, assetCount, isArchived, coverImageName }: Props) => {
const { t } = useTranslation();
- const { imagesTotal } = useGetBoardImagesTotalQuery(board?.board_id || 'none', {
- selectFromResult: ({ data }) => {
- return { imagesTotal: data?.total ?? 0 };
- },
- });
- const { assetsTotal } = useGetBoardAssetsTotalQuery(board?.board_id || 'none', {
- selectFromResult: ({ data }) => {
- return { assetsTotal: data?.total ?? 0 };
- },
- });
- const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
+ const { currentData: coverImage } = useGetImageDTOQuery(coverImageName ?? skipToken);
+
+ const totalString = useMemo(() => {
+ return `${t('boards.imagesWithCount', { count: imageCount })}, ${t('boards.assetsWithCount', { count: assetCount })}${isArchived ? ` (${t('boards.archived')})` : ''}`;
+ }, [assetCount, imageCount, isArchived, t]);
return (
@@ -34,13 +30,11 @@ export const BoardTooltip = ({ board }: Props) => {
aspectRatio="1/1"
borderRadius="base"
borderBottomRadius="lg"
+ mt={1}
/>
)}
-
- {t('boards.imagesWithCount', { count: imagesTotal })}, {t('boards.assetsWithCount', { count: assetsTotal })}
-
- {board?.archived && ({t('boards.archived')})}
+ {totalString}
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
index 2bd07ef39cc..75ea1410214 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
@@ -119,7 +119,18 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
return (
{(ref) => (
- } openDelay={1000} placement="left" closeOnScroll p={2}>
+
+ }
+ placement="left"
+ closeOnScroll
+ >
{
{autoAddBoardId === board.board_id && !editingDisclosure.isOpen && }
{board.archived && !editingDisclosure.isOpen && }
- {!editingDisclosure.isOpen && {board.image_count}}
+ {!editingDisclosure.isOpen && {board.image_count + board.asset_count}}
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
index 7719c79866f..41d90a30c95 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
@@ -14,7 +14,7 @@ import {
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
+import { useGetUncategorizedImageCountsQuery } from 'services/api/endpoints/boards';
import { useBoardName } from 'services/api/hooks/useBoardName';
interface Props {
@@ -27,11 +27,7 @@ const _hover: SystemStyleObject = {
const NoBoardBoard = memo(({ isSelected }: Props) => {
const dispatch = useAppDispatch();
- const { imagesTotal } = useGetBoardImagesTotalQuery('none', {
- selectFromResult: ({ data }) => {
- return { imagesTotal: data?.total ?? 0 };
- },
- });
+ const { data } = useGetUncategorizedImageCountsQuery();
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
const boardSearchText = useAppSelector(selectBoardSearchText);
@@ -60,7 +56,13 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
return (
{(ref) => (
- } openDelay={1000} placement="left" closeOnScroll>
+
+ }
+ placement="left"
+ closeOnScroll
+ >
{
{boardName}
{autoAddBoardId === 'none' && }
- {imagesTotal}
+ {(data?.image_count ?? 0) + (data?.asset_count ?? 0)}
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index 55ebeab3188..2b33a0a603f 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
@@ -1,12 +1,4 @@
-import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
-import type {
- BoardDTO,
- CreateBoardArg,
- ListBoardsArgs,
- OffsetPaginatedResults_ImageDTO_,
- UpdateBoardArg,
-} from 'services/api/types';
-import { getListImagesUrl } from 'services/api/util';
+import type { BoardDTO, CreateBoardArg, ListBoardsArgs, S, UpdateBoardArg } from 'services/api/types';
import type { ApiTagDescription } from '..';
import { api, buildV1Url, LIST_TAG } from '..';
@@ -55,38 +47,11 @@ export const boardsApi = api.injectEndpoints({
keepUnusedDataFor: 0,
}),
- getBoardImagesTotal: build.query<{ total: number }, string | undefined>({
- query: (board_id) => ({
- url: getListImagesUrl({
- board_id: board_id ?? 'none',
- categories: IMAGE_CATEGORIES,
- is_intermediate: false,
- limit: 0,
- offset: 0,
- }),
- method: 'GET',
+ getUncategorizedImageCounts: build.query({
+ query: () => ({
+ url: buildBoardsUrl('uncategorized/counts'),
}),
- providesTags: (result, error, arg) => [{ type: 'BoardImagesTotal', id: arg ?? 'none' }, 'FetchOnReconnect'],
- transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
- return { total: response.total };
- },
- }),
-
- getBoardAssetsTotal: build.query<{ total: number }, string | undefined>({
- query: (board_id) => ({
- url: getListImagesUrl({
- board_id: board_id ?? 'none',
- categories: ASSETS_CATEGORIES,
- is_intermediate: false,
- limit: 0,
- offset: 0,
- }),
- method: 'GET',
- }),
- providesTags: (result, error, arg) => [{ type: 'BoardAssetsTotal', id: arg ?? 'none' }, 'FetchOnReconnect'],
- transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
- return { total: response.total };
- },
+ providesTags: ['UncategorizedImageCounts', { type: 'Board', id: LIST_TAG }, { type: 'Board', id: 'none' }],
}),
/**
@@ -124,9 +89,8 @@ export const boardsApi = api.injectEndpoints({
export const {
useListAllBoardsQuery,
- useGetBoardImagesTotalQuery,
- useGetBoardAssetsTotalQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useListAllImageNamesForBoardQuery,
+ useGetUncategorizedImageCountsQuery,
} = boardsApi;
diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts
index 0b82714d94c..1fa78cc4d7e 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -46,6 +46,7 @@ const tagTypes = [
// This is invalidated on reconnect. It should be used for queries that have changing data,
// especially related to the queue and generation.
'FetchOnReconnect',
+ 'UncategorizedImageCounts',
] as const;
export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>;
export const LIST_TAG = 'LIST';
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index b34e9589a4c..e483d678759 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -785,6 +785,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/boards/uncategorized/counts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Uncategorized Image Counts
+ * @description Gets count of images and assets for uncategorized images (images with no board assocation)
+ */
+ get: operations["get_uncategorized_image_counts"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/board_images/": {
parameters: {
query?: never;
@@ -1973,7 +1993,7 @@ export type components = {
};
/**
* BoardDTO
- * @description Deserialized board record with cover image URL and image count.
+ * @description Deserialized board record.
*/
BoardDTO: {
/**
@@ -2003,9 +2023,9 @@ export type components = {
deleted_at?: string | null;
/**
* Cover Image Name
- * @description The name of the board's cover image.
+ * @description The name of the cover image of the board.
*/
- cover_image_name: string | null;
+ cover_image_name?: string | null;
/**
* Archived
* @description Whether or not the board is archived.
@@ -2021,6 +2041,11 @@ export type components = {
* @description The number of images in the board.
*/
image_count: number;
+ /**
+ * Asset Count
+ * @description The number of assets in the board.
+ */
+ asset_count: number;
};
/**
* BoardField
@@ -4345,7 +4370,7 @@ export type components = {
};
/**
* Core Metadata
- * @description Collects core generation metadata into a MetadataField
+ * @description Used internally by Invoke to collect metadata for generations.
*/
CoreMetadataInvocation: {
/**
@@ -16391,6 +16416,19 @@ export type components = {
*/
type: "url";
};
+ /** UncategorizedImageCounts */
+ UncategorizedImageCounts: {
+ /**
+ * Image Count
+ * @description The number of uncategorized images.
+ */
+ image_count: number;
+ /**
+ * Asset Count
+ * @description The number of uncategorized assets.
+ */
+ asset_count: number;
+ };
/**
* Unsharp Mask
* @description Applies an unsharp mask filter to an image
@@ -18845,6 +18883,26 @@ export interface operations {
};
};
};
+ get_uncategorized_image_counts: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UncategorizedImageCounts"];
+ };
+ };
+ };
+ };
add_image_to_board: {
parameters: {
query?: never;
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 647f141f4f6..b0860d6678c 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -37,7 +37,6 @@ export type AppDependencyVersions = S['AppDependencyVersions'];
export type ImageDTO = S['ImageDTO'];
export type BoardDTO = S['BoardDTO'];
export type ImageCategory = S['ImageCategory'];
-export type OffsetPaginatedResults_ImageDTO_ = S['OffsetPaginatedResults_ImageDTO_'];
// Models
export type ModelType = S['ModelType'];
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index f6fae1ec4a8..fc16c01cdc8 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -6,7 +6,6 @@ import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagi
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
-import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { getCategories, getListImagesUrl } from 'services/api/util';
@@ -31,13 +30,6 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
return;
}
- // update the total images for the board
- dispatch(
- boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
- draft.total += 1;
- })
- );
-
dispatch(
imagesApi.util.invalidateTags([
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py
index 223ecc88632..48842d9a4bd 100644
--- a/tests/app/services/bulk_download/test_bulk_download.py
+++ b/tests/app/services/bulk_download/test_bulk_download.py
@@ -127,7 +127,16 @@ def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker):
def mock_board_get(*args, **kwargs):
return BoardRecord(
- board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False
+ board_id="12345",
+ board_name="test_board_name",
+ created_at="None",
+ updated_at="None",
+ archived=False,
+ asset_count=0,
+ image_count=0,
+ cover_image_name="asdf.png",
+ deleted_at=None,
+ is_private=False,
)
monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get)
@@ -156,7 +165,16 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag
def mock_board_get(*args, **kwargs):
return BoardRecord(
- board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False
+ board_id="12345",
+ board_name="test_board_name",
+ created_at="None",
+ updated_at="None",
+ archived=False,
+ asset_count=0,
+ image_count=0,
+ cover_image_name="asdf.png",
+ deleted_at=None,
+ is_private=False,
)
monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get)