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(
|
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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -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" };
|
||||||
|
|
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
|
@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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue