Skip to content

Instantly share code, notes, and snippets.

@marcio0
Last active September 26, 2022 10:29
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcio0/ab4ad06c0848fee9228929d3d725afa7 to your computer and use it in GitHub Desktop.
Save marcio0/ab4ad06c0848fee9228929d3d725afa7 to your computer and use it in GitHub Desktop.
filtered reference field on sanity
import React from "react";
import PropTypes from "prop-types";
import { has } from "lodash";
import PatchEvent, { set, unset } from "part:@sanity/form-builder/patch-event";
import ReferenceInput from "@sanity/form-builder/lib/inputs/ReferenceInput";
import { withDocument } from "part:@sanity/form-builder";
import { map } from "rxjs/operators";
import client from "part:@sanity/base/client";
import { createWeightedSearch } from "../../utils/search";
import { observeForPreview } from "part:@sanity/base/preview";
// https://github.com/sanity-io/sanity/blob/21af6baffe/packages/%40sanity/form-builder/src/sanity/inputs/client-adapters/reference.ts
// https://github.com/sanity-io/sanity/blob/21af6baffe/packages/%40sanity/base/src/search/weighted/createWeightedSearch.js
// https://github.com/sanity-io/sanity/blob/21af6baffe88d57db32d0a05e048ef7d3d671523/packages/%40sanity/form-builder/src/sanity/inputs/SanityReferenceInput.tsx
/*
To use it:
{
name: "...",
title: "...",
type: "reference",
to: [
{
type: "..."
}
],
inputComponent: FilteredReference,
options: {
filters: document => {
const myReference = document.myReference || {};
return [`references("${myReference._ref}")`];
}
}
},
*/
export function getPreviewSnapshot(value, referenceType) {
return observeForPreview(value, referenceType).pipe(
map(result => result.snapshot)
);
}
export function search(textTerm, referenceType, additionalFilters) {
const doSearch = createWeightedSearch(
referenceType.to,
client,
additionalFilters
);
return doSearch(textTerm, { includeDrafts: false }).pipe(
map(results => results.map(res => res.hit))
);
}
class FilteredReference extends React.Component {
setInput = input => {
this._input = input;
};
focus = () => {
this._input.focus();
};
doSearch = (textTerm, referenceType) => {
const { document, type } = this.props;
const filtersMeth = (type.options || {}).filters || (() => []);
const filters = filtersMeth(document);
return search(textTerm, referenceType, filters);
};
render() {
return (
<ReferenceInput
{...this.props}
onSearch={this.doSearch}
getPreviewSnapshot={getPreviewSnapshot}
ref={this.setInput}
/>
);
}
}
export default withDocument(FilteredReference);
import React from "react";
import { GiLargeDress } from "react-icons/gi";
import FilteredReference from "../components/input/FilteredReference";
export default {
name: "foo",
title: "Foo",
type: "document",
icon: GiLargeDress,
fields: [
{
name: "name",
title: "Name",
type: "string"
},
{
title: "Bla",
name: "bla",
type: "reference",
to: [{ type: "bla" }],
inputComponent: FilteredReference,
options: {
filters: document => {
return [`references("${document.bla._ref}")`];
}
}
}
}
};
import { map } from "rxjs/operators";
import { joinPath } from "@sanity/base/lib/util/searchUtils";
import { compact, flatten, flow, sortBy, union } from "lodash";
import { removeDupes } from "@sanity/base/lib/util/draftUtils";
import { applyWeights } from "@sanity/base/lib/search/weighted/applyWeights";
const combinePaths = flow([flatten, union, compact]);
const toGroqParams = terms =>
terms.reduce((acc, term, i) => {
acc[`t${i}`] = `${term}*`; // "t" is short for term
return acc;
}, {});
export function createWeightedSearch(types, client, additionalFilters) {
const searchSpec = types.map(type => {
return {
typeName: type.name,
paths: type.__experimental_search.map(config => ({
weight: config.weight,
path: joinPath(config.path)
}))
};
});
const combinedSearchPaths = combinePaths(
searchSpec.map(configForType => configForType.paths.map(opt => opt.path))
);
const selections = searchSpec.map(
spec =>
`_type == "${spec.typeName}" => {${spec.paths.map(
(cfg, i) => `"w${i}": ${cfg.path}`
)}}`
);
// this is the actual search function that takes the search string and returns the hits
return function search(queryString, opts = {}) {
const terms = queryString.split(/\W+/).filter(Boolean);
const constraints = terms.map((term, i) =>
combinedSearchPaths.map(joinedPath => `${joinedPath} match $t${i}`)
);
const filters = [
"_type in $types",
...constraints.map(constraint => `(${constraint.join("||")})`),
...additionalFilters
];
const query = `*[${filters.join(
"&&"
)}][0...$limit]{_type, _id, ...select(${selections.join(",\n")})}`;
return client.observable
.fetch(query, {
...toGroqParams(terms),
types: searchSpec.map(spec => spec.typeName),
limit: 1000
})
.pipe(
map(removeDupes),
map(hits => applyWeights(searchSpec, hits, terms)),
map(hits => sortBy(hits, hit => -hit.score)),
map(hits => hits.slice(0, 100))
);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment