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:
parent
ab1fade529
commit
a3dcaa0f0e
8 changed files with 166 additions and 40 deletions
|
@ -30,8 +30,22 @@ class ItemsController < ApplicationController
|
|||
items: @items.as_json(
|
||||
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_count: @items.count,
|
||||
query: @query.to_s,
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +122,7 @@ class ItemsController < ApplicationController
|
|||
return {} if pet_type_name.blank?
|
||||
|
||||
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
|
||||
|
||||
def search_error(e)
|
||||
|
|
|
@ -41,6 +41,9 @@ export function useSearchResults(
|
|||
const currentPageIndex = currentPageNumber - 1;
|
||||
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!
|
||||
const {
|
||||
loading: loadingGQL,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { normalizeSwfAssetToLayer } from "./shared-types";
|
||||
|
||||
export function useAltStylesForSpecies(speciesId, options = {}) {
|
||||
return useQuery({
|
||||
...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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { normalizeSwfAssetToLayer } from "./shared-types";
|
||||
|
||||
export function useItemAppearances(id, options = {}) {
|
||||
return useQuery({
|
||||
...options,
|
||||
|
@ -9,7 +11,9 @@ export function useItemAppearances(id, options = {}) {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw new Error(
|
||||
|
@ -20,27 +24,95 @@ async function loadItemAppearancesData(id) {
|
|||
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) {
|
||||
return {
|
||||
name: data.name,
|
||||
appearances: data.appearances.map((appearance) => ({
|
||||
body: normalizeBody(appearance.body),
|
||||
swfAssets: appearance.swf_assets.map((asset) => ({
|
||||
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,
|
||||
},
|
||||
})),
|
||||
swfAssets: appearance.swf_assets.map(normalizeSwfAssetToLayer),
|
||||
})),
|
||||
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) {
|
||||
if (String(body.id) === "0") {
|
||||
return { id: "0" };
|
||||
|
|
19
app/javascript/wardrobe-2020/loaders/shared-types.js
Normal file
19
app/javascript/wardrobe-2020/loaders/shared-types.js
Normal 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,
|
||||
};
|
||||
}
|
|
@ -445,8 +445,23 @@ class Item < ApplicationRecord
|
|||
@parent_swf_asset_relationships_to_update = rels
|
||||
end
|
||||
|
||||
Appearance = Struct.new(:body, :swf_assets)
|
||||
Appearance::Body = Struct.new(:id, :species)
|
||||
# NOTE: Adding the JSON serializer makes `as_json` treat this like a model
|
||||
# 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
|
||||
all_swf_assets = swf_assets.to_a
|
||||
|
||||
|
|
|
@ -169,16 +169,27 @@ class PetType < ApplicationRecord
|
|||
}.first
|
||||
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
|
||||
# body.
|
||||
relationships = ParentSwfAssetRelationship.includes(:swf_asset).
|
||||
relationships = ParentSwfAssetRelationship.
|
||||
includes(swf_asset: swf_asset_includes).
|
||||
where(parent_type: "Item", parent_id: item_ids).
|
||||
where(swf_asset: {body_id: [body_id, 0]})
|
||||
|
||||
# Then, convert this into a hash from item ID to SWF assets.
|
||||
assets_by_item_id = relationships.group_by(&:parent_id).
|
||||
transform_values { |rels| rels.map(&:swf_asset) }
|
||||
pet_type_body = Item::Appearance::Body.new(body_id, species)
|
||||
all_pets_body = Item::Appearance::Body.new(0, nil)
|
||||
|
||||
# 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
|
||||
|
||||
def self.all_by_ids_or_children(ids, pet_states)
|
||||
|
|
|
@ -17,6 +17,14 @@ class Zone < ActiveRecord::Base
|
|||
@sometimes ? "#{label} sometimes" : label
|
||||
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)
|
||||
label.delete('\- /').parameterize
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue