Skip to content

Favorite renaming#30

Open
Lyinlyon wants to merge 6 commits into
certfrom
Favorite-Renaming
Open

Favorite renaming#30
Lyinlyon wants to merge 6 commits into
certfrom
Favorite-Renaming

Conversation

@Lyinlyon

Copy link
Copy Markdown
Collaborator

Favorites can now be renamed inline. The favorite search now searches by the favorite id and name rather than the insertable. The changes to packages and the addition of the coverage folder were done automatically and I have no idea what they do.

Lyinlyon added 4 commits July 8, 2026 13:51
	new file:   drizzle/0003_favorites_add_name.sql
	modified:   package-lock.json
	modified:   package.json
	modified:   src/__test_utils__/seed.ts
	modified:   src/backend/routes/favorites.test.ts
	modified:   src/backend/routes/favorites.ts
	modified:   src/frontend/cards/card-components.tsx
	modified:   src/frontend/favorites/favorite-button.tsx
	modified:   src/frontend/favorites/favorite-card.tsx
	modified:   src/frontend/favorites/favorite-menu.tsx
	modified:   src/shared/api-models.ts
	modified:   src/shared/schema.ts
	new file:   src/frontend/favorites/favorite-search.test.ts
	new file:   src/frontend/favorites/favorite-search.ts
	modified:   src/frontend/favorites/favorites-list.tsx
	modified:   src/shared/search.ts
	modified:   src/frontend/favorites/favorite-menu.tsx
	modified:   src/backend/routes/favorites.test.ts
	modified:   src/backend/routes/favorites.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| -> ??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the create endpoint we can simplify the sortOrder logic to:

    const existingFavorites = await db.$count(
        favorites,
        and(eq(favorites.userId, userId), eq(favorites.libraryId, libraryId))
    );

    await db
        .insert(favorites)
        .values({
            id: favoriteId,
            userId,
            libraryId,
            insertableId,
            sortOrder: existingFavorites + 1
        })
        .onConflictDoNothing();

Which just sets the sortOrder to the number of existing favorites+1 more directly

@AlexKempen AlexKempen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking good, you can see my comments above.

I also realized our search logic was pretty scuffed and/or scattered, so I added a typed SearchResultDocument helper and renamed/re-organized our existing search implementation so the organization is a bit clearer, so make sure you run git pull the next time you work on it so you get my changes/don't run into conflicts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an existing pattern we use for updating a row

    const body = await c.req.json<{ theme?: Theme; libraryId?: LibraryId }>();

    const db = getDb(c.env.DB);

    await db.insert(users).values({ id: userId }).onConflictDoNothing();

    if (Object.keys(body).length > 0) {
        await db.update(users).set(body).where(eq(users.id, userId));
    }

});
};

const isSearchActive = Boolean(uiState.searchQuery && searchHit);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&& is already a boolean, so Boolean() is redundant


const isSearchActive = Boolean(uiState.searchQuery && searchHit);
const titleComponent =
isSearchActive && searchHit ? (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking searchHit (already checked in isSearchActive)

isSearchActive && searchHit ? (
<SearchHitTitle title={favoriteName} searchHit={searchHit} />
) : (
<TextInput

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TextInput (and related hooks and whatnot) feels like a separate EditTitle component that takes an onEditComplete callback

});
}

function useUpdateFavoriteNameMutation() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like how this is a seperate custom hook to encapsulate the mutation logic!

const disabled = props.disabled ?? false;
const isHidden = props.showHiddenTag ?? false;

let cardTitle: ReactNode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be best to rename this component CardTitleGroup and have it wrap a different CardTitle component that composes a standard title, SearchHitTitle, and EditTitle components. See https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children for info on how you can type/use the component, the end pattern you're looking for is:

<CardTitleGroup thumbnailUrls={thumbnailUrls} buildStatusBadge={badge}>
    <CardTitle title={title} searchHit={searchHit} isEditing={isEditing} />
<CardTitleGroup />

The EditTitle component can come from your editable title logic below.

interface FavoriteSearchDocument {
id: string;
name: string;
favoriteId: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to also include vendors: Vendors[] in here so we can easily display the number of favorites filtered out (the pattern should match the existing pattern). I also added a filter block to the search function you can use to implement

Comment thread src/shared/schema.ts
defaultConfiguration: text("default_configuration", {
mode: "json"
}).$type<Configuration | null>(),
name: text("name").notNull().default(""),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favorite name should be nullable and null by default, meaning "use the name of the insertable since the user hasn't set one", since otherwise admin renames won't update people's favorites by default

}

const FAVORITE_SEARCH_OPTIONS: Options<FavoriteSearchDocument> = {
fields: ["name", "favoriteId"],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't need to index fields other than name (and vendors if you add it)

searchDb.addAll(
favorites.map((favorite) => ({
id: favorite.id,
name: favorite.name ?? "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need the insertable name in the searchDb when favorite name is undefined, which probably means passing in insertables. You could also probably write a helper like getInsertableName(favorite, insertable) which returns "" if the insertable doesn't exist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants