1
0
Fork 0
forked from OpenNeo/impress

Add useItemSearch for wardrobe app, but don't use it yet!

Adding new functionality to the item search JSON endpoint, and adding
an adapter layer to match the GQL format!

Hopefully this will be pretty drop-in-able, we'll see!
This commit is contained in:
Emi Matchu 2024-02-25 12:06:20 -08:00
parent ab1fade529
commit a3dcaa0f0e
8 changed files with 166 additions and 40 deletions

View file

@ -30,8 +30,22 @@ class ItemsController < ApplicationController
items: @items.as_json( items: @items.as_json(
methods: [:nc?, :pb?, :owned?, :wanted?], methods: [:nc?, :pb?, :owned?, :wanted?],
), ),
appearances: load_appearances, appearances: load_appearances.as_json(
include: {
swf_assets: {
only: [:id, :remote_id, :body_id],
include: {
zone: {
only: [:id, :depth, :label],
methods: [:is_commonly_used_by_items],
}
},
methods: [:urls, :known_glitches],
},
}
),
total_pages: @items.total_pages, total_pages: @items.total_pages,
total_count: @items.count,
query: @query.to_s, query: @query.to_s,
} }
} }
@ -108,7 +122,7 @@ class ItemsController < ApplicationController
return {} if pet_type_name.blank? return {} if pet_type_name.blank?
pet_type = Item::Search::Query.load_pet_type_by_name(pet_type_name) pet_type = Item::Search::Query.load_pet_type_by_name(pet_type_name)
pet_type.appearances_for(@items.map(&:id)) pet_type.appearances_for(@items.map(&:id), swf_asset_includes: [:zone])
end end
def search_error(e) def search_error(e)

View file

@ -41,6 +41,9 @@ export function useSearchResults(
const currentPageIndex = currentPageNumber - 1; const currentPageIndex = currentPageNumber - 1;
const offset = currentPageIndex * SEARCH_PER_PAGE; const offset = currentPageIndex * SEARCH_PER_PAGE;
// const filters = buildSearchFilters(/* TODO */);
// const { loading, error, data } = useItemSearch(filters);
// Here's the actual GQL query! At the bottom we have more config than usual! // Here's the actual GQL query! At the bottom we have more config than usual!
const { const {
loading: loadingGQL, loading: loadingGQL,

View file

@ -1,5 +1,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { normalizeSwfAssetToLayer } from "./shared-types";
export function useAltStylesForSpecies(speciesId, options = {}) { export function useAltStylesForSpecies(speciesId, options = {}) {
return useQuery({ return useQuery({
...options, ...options,
@ -63,21 +65,3 @@ function normalizeAltStyle(altStyleData) {
}, },
}; };
} }
function normalizeSwfAssetToLayer(swfAssetData) {
return {
id: String(swfAssetData.id),
zone: {
id: String(swfAssetData.zone.id),
depth: swfAssetData.zone.depth,
label: swfAssetData.zone.label,
},
bodyId: swfAssetData.body_id,
knownGlitches: swfAssetData.known_glitches,
svgUrl: swfAssetData.urls.svg,
canvasMovieLibraryUrl: swfAssetData.urls.canvas_library,
imageUrl: swfAssetData.urls.png,
swfUrl: swfAssetData.urls.swf,
};
}

View file

@ -1,5 +1,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { normalizeSwfAssetToLayer } from "./shared-types";
export function useItemAppearances(id, options = {}) { export function useItemAppearances(id, options = {}) {
return useQuery({ return useQuery({
...options, ...options,
@ -9,7 +11,9 @@ export function useItemAppearances(id, options = {}) {
} }
async function loadItemAppearancesData(id) { async function loadItemAppearancesData(id) {
const res = await fetch(`/items/${encodeURIComponent(id)}/appearances.json`); const res = await fetch(
`/items/${encodeURIComponent(id)}/appearances.json`,
);
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(
@ -20,27 +24,95 @@ async function loadItemAppearancesData(id) {
return res.json().then(normalizeItemAppearancesData); return res.json().then(normalizeItemAppearancesData);
} }
export function useItemSearch(searchOptions, queryOptions = {}) {
return useQuery({
...queryOptions,
queryKey: ["itemSearch", buildItemSearchParams(searchOptions)],
queryFn: () => loadItemSearch(searchOptions),
});
}
function buildItemSearchParams({
filters = [],
withAppearancesFor = null,
page = 1,
perPage = 30,
}) {
const params = new URLSearchParams();
for (const [i, filter] of filters.entries()) {
params.append(`q[${i}][key]`, filter.key);
params.append(`q[${i}][value]`, filter.value);
if (params.isPositive == false) {
params.append(`q[${i}][is_positive]`, "false");
}
}
if (withAppearancesFor != null) {
params.append("with_appearances_for", withAppearancesFor);
}
params.append("page", page);
params.append("per_page", perPage);
return params.toString();
}
async function loadItemSearch(searchOptions) {
const params = buildItemSearchParams(searchOptions);
const res = await fetch(`/items.json?${params}`);
if (!res.ok) {
throw new Error(
`loading item search failed: ${res.status} ${res.statusText}`,
);
}
return res
.json()
.then((data) => normalizeItemSearchData(data, searchOptions));
}
window.loadItemSearch = loadItemSearch;
function normalizeItemAppearancesData(data) { function normalizeItemAppearancesData(data) {
return { return {
name: data.name, name: data.name,
appearances: data.appearances.map((appearance) => ({ appearances: data.appearances.map((appearance) => ({
body: normalizeBody(appearance.body), body: normalizeBody(appearance.body),
swfAssets: appearance.swf_assets.map((asset) => ({ swfAssets: appearance.swf_assets.map(normalizeSwfAssetToLayer),
id: String(asset.id),
knownGlitches: asset.known_glitches,
zone: normalizeZone(asset.zone),
restrictedZones: asset.restricted_zones.map((z) => normalizeZone(z)),
urls: {
swf: asset.urls.swf,
png: asset.urls.png,
manifest: asset.urls.manifest,
},
})),
})), })),
restrictedZones: data.restricted_zones.map((z) => normalizeZone(z)), restrictedZones: data.restricted_zones.map((z) => normalizeZone(z)),
}; };
} }
function normalizeItemSearchData(data, searchOptions) {
return {
id: buildItemSearchParams(searchOptions),
numTotalItems: data.total_count,
items: data.items.map((item) => ({
id: String(item.id),
name: item.name,
thumbnailUrl: item.thumbnail_url,
isNc: item["nc?"],
isPb: item["pb?"],
currentUserOwnsThis: item["owned?"],
currentUserWantsThis: item["wanted?"],
appearanceOn: normalizeItemSearchAppearance(
data.appearances[item.id],
item,
),
})),
};
}
function normalizeItemSearchAppearance(data, item) {
if (data == null) {
return null;
}
return {
id: `item-${item.id}-body-${data.body.id}`,
layers: data.swf_assets.map(normalizeSwfAssetToLayer),
};
}
function normalizeBody(body) { function normalizeBody(body) {
if (String(body.id) === "0") { if (String(body.id) === "0") {
return { id: "0" }; return { id: "0" };

View file

@ -0,0 +1,19 @@
export function normalizeSwfAssetToLayer(swfAssetData) {
return {
id: String(swfAssetData.id),
remoteId: String(swfAssetData.remote_id),
zone: {
id: String(swfAssetData.zone.id),
depth: swfAssetData.zone.depth,
label: swfAssetData.zone.label,
isCommonlyUsedByItems: swfAssetData.zone.is_commonly_used_by_items,
},
bodyId: swfAssetData.body_id,
knownGlitches: swfAssetData.known_glitches,
svgUrl: swfAssetData.urls.svg,
canvasMovieLibraryUrl: swfAssetData.urls.canvas_library,
imageUrl: swfAssetData.urls.png,
swfUrl: swfAssetData.urls.swf,
};
}

View file

@ -445,8 +445,23 @@ class Item < ApplicationRecord
@parent_swf_asset_relationships_to_update = rels @parent_swf_asset_relationships_to_update = rels
end end
Appearance = Struct.new(:body, :swf_assets) # NOTE: Adding the JSON serializer makes `as_json` treat this like a model
Appearance::Body = Struct.new(:id, :species) # instead of like a hash, so you can target its children with things like
# the `include` option. This feels clunky though, I wish I had something a
# bit more suited to it!
Appearance = Struct.new(:body, :swf_assets) do
include ActiveModel::Serializers::JSON
def attributes
{body: body, swf_assets: swf_assets}
end
end
Appearance::Body = Struct.new(:id, :species) do
include ActiveModel::Serializers::JSON
def attributes
{id: id, species: species}
end
end
def appearances def appearances
all_swf_assets = swf_assets.to_a all_swf_assets = swf_assets.to_a

View file

@ -169,16 +169,27 @@ class PetType < ApplicationRecord
}.first }.first
end end
def appearances_for(item_ids) def appearances_for(item_ids, swf_asset_includes: [])
# First, load all the relationships for these items that also fit this # First, load all the relationships for these items that also fit this
# body. # body.
relationships = ParentSwfAssetRelationship.includes(:swf_asset). relationships = ParentSwfAssetRelationship.
includes(swf_asset: swf_asset_includes).
where(parent_type: "Item", parent_id: item_ids). where(parent_type: "Item", parent_id: item_ids).
where(swf_asset: {body_id: [body_id, 0]}) where(swf_asset: {body_id: [body_id, 0]})
# Then, convert this into a hash from item ID to SWF assets. pet_type_body = Item::Appearance::Body.new(body_id, species)
assets_by_item_id = relationships.group_by(&:parent_id). all_pets_body = Item::Appearance::Body.new(0, nil)
transform_values { |rels| rels.map(&:swf_asset) }
# Then, convert this into a hash from item ID to appearances.
relationships.group_by(&:parent_id).
transform_values do |rels|
assets = rels.map(&:swf_asset)
if assets.all? { |a| a.body_id == 0 }
Item::Appearance.new all_pets_body, assets
else
Item::Appearance.new pet_type_body, assets
end
end
end end
def self.all_by_ids_or_children(ids, pet_states) def self.all_by_ids_or_children(ids, pet_states)

View file

@ -1,4 +1,4 @@
class Zone < ActiveRecord::Base class Zone < ActiveRecord::Base
# When selecting zones that an asset occupies, we allow the zone to set # When selecting zones that an asset occupies, we allow the zone to set
# whether or not the zone is "sometimes" occupied. This is false by default. # whether or not the zone is "sometimes" occupied. This is false by default.
attr_writer :sometimes attr_writer :sometimes
@ -16,6 +16,14 @@ class Zone < ActiveRecord::Base
def uncertain_label def uncertain_label
@sometimes ? "#{label} sometimes" : label @sometimes ? "#{label} sometimes" : label
end end
def is_commonly_used_by_items
# Zone metadata marks item zones with types 2, 3, and 4. But also, in
# practice, the Biology Effects zone (type 1, ID 4) has been used for a few
# items too. So, that's what we return true for!
# TODO: It'd probably be better to make this a database field?
[2, 3, 4].include?(type_id) || id == 4
end
def self.plainify_label(label) def self.plainify_label(label)
label.delete('\- /').parameterize label.delete('\- /').parameterize