Skip to content

Instantly share code, notes, and snippets.

@fatihsolhan
Last active April 2, 2022 15:23
Show Gist options
  • Save fatihsolhan/4f5403ebdc0e793bcb2484ab13b28da3 to your computer and use it in GitHub Desktop.
Save fatihsolhan/4f5403ebdc0e793bcb2484ab13b28da3 to your computer and use it in GitHub Desktop.
/** @jsxRuntime classic */
/** @jsx jsx */
import { Fragment, ReactNode, useState } from 'react';
import { Button } from '@keystone-ui/button';
import { Inline, jsx, Stack, useTheme } from '@keystone-ui/core';
import { FieldContainer, FieldLabel, FieldLegend } from '@keystone-ui/fields';
import { DrawerController } from '@keystone-ui/modals';
import {
CardValueComponent,
CellComponent,
FieldController,
FieldControllerConfig,
FieldProps,
ListMeta,
} from '@keystone-6/core/types';
import { Link } from '@keystone-6/core/admin-ui/router';
import { useKeystone, useList } from '@keystone-6/core/admin-ui/context';
import { gql, useQuery } from '@keystone-6/core/admin-ui/apollo';
import { CellContainer, CreateItemDrawer } from '@keystone-6/core/admin-ui/components';
import { Field as FilteredRelationshipSelect } from './FilteredRelationshipSelect';
const CUSTOM_FILTER = '{ "role": { "type": { "equals": "ADMIN" } } }'
function LinkToRelatedItems({
itemId,
value,
list,
refFieldKey,
}: {
itemId: string | null;
value: FieldProps<typeof controller>['value'] & { kind: 'many' | 'one' };
list: ListMeta;
refFieldKey?: string;
}) {
function constructQuery({
refFieldKey,
itemId,
value,
}: {
refFieldKey?: string;
itemId: string | null;
value: FieldProps<typeof controller>['value'] & { kind: 'many' | 'one' };
}) {
if (!!refFieldKey && itemId) {
return `!${refFieldKey}_matches="${itemId}"`;
}
return `!id_in="${(value?.value as { id: string; label: string }[])
.slice(0, 100)
.map(({ id }: { id: string }) => id)
.join(',')}"`;
}
const commonProps = {
size: 'small',
tone: 'active',
weight: 'link',
} as const;
if (value.kind === 'many') {
const query = constructQuery({ refFieldKey, value, itemId });
return (
<Button {...commonProps} as={Link} href={`/${list.path}?${query}`}>
View related {list.plural}
</Button>
);
}
return (
<Button {...commonProps} as={Link} href={`/${list.path}/${value.value?.id}`}>
View {list.singular} details
</Button>
);
}
const RelationshipLinkButton = ({ href, children }: { href: string; children: ReactNode }) => (
<Button css={{ padding: 0, height: 'auto' }} weight="link" tone="active" as={Link} href={href}>
{children}
</Button>
);
const RelationshipDisplay = ({
list,
value,
}: {
list: ListMeta;
value: SingleRelationshipValue | ManyRelationshipValue;
}) => {
if (value.kind === 'many') {
if (value.value.length) {
return (
<Inline gap="small">
{value.value.map(i => (
<RelationshipLinkButton href={`/${list.path}/${i.id}`}>
{i.label}
</RelationshipLinkButton>
))}
</Inline>
);
} else {
return <div>(No {list.plural})</div>;
}
} else {
if (value.value) {
return (
<RelationshipLinkButton href={`/${list.path}/${value.value.id}`}>
{value.value.label}
</RelationshipLinkButton>
);
} else {
return <div>(No {list.singular})</div>;
}
}
};
export const Field = ({
field,
autoFocus,
value,
onChange,
forceValidation,
}: FieldProps<typeof controller>) => {
const keystone = useKeystone();
const foreignList = useList(field.refListKey);
const localList = useList(field.listKey);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const authenticatedItem = keystone.authenticatedItem;
return (
<FieldContainer as="fieldset">
<FieldLabel as="legend">{field.label}</FieldLabel>
{onChange ? (
<Fragment>
<Stack gap="medium">
<FilteredRelationshipSelect
extraFilters={CUSTOM_FILTER}
controlShouldRenderValue
autoFocus={autoFocus}
isDisabled={onChange === undefined}
list={foreignList}
portalMenu
state={
value.kind === 'many'
? {
kind: 'many',
value: value.value,
onChange(newItems) {
onChange({
...value,
value: newItems,
});
},
}
: {
kind: 'one',
value: value.value,
onChange(newVal) {
if (value.kind === 'one') {
onChange({
...value,
value: newVal,
});
}
},
}
}
/>
<Stack across gap="small">
{!field.hideCreate && (
<Button
size="small"
disabled={isDrawerOpen}
onClick={() => {
setIsDrawerOpen(true);
}}
>
Create related {foreignList.singular}
</Button>
)}
{authenticatedItem.state === 'authenticated' &&
authenticatedItem.listKey === field.refListKey &&
(value.kind === 'many'
? value.value.find(x => x.id === authenticatedItem.id) === undefined
: value.value?.id !== authenticatedItem.id) && (
<Button
size="small"
isDisabled={onChange === undefined}
onClick={() => {
const val = {
label: authenticatedItem.label,
id: authenticatedItem.id,
};
if (value.kind === 'many') {
onChange({
...value,
value: [...value.value, val],
});
} else {
onChange({
...value,
value: val,
});
}
}}
>
{value.kind === 'many' ? 'Add ' : 'Set as '}
{authenticatedItem.label}
</Button>
)}
{!!(value.kind === 'many'
? value.value.length
: value.kind === 'one' && value.value) && (
<LinkToRelatedItems
itemId={value.id}
refFieldKey={field.refFieldKey}
list={foreignList}
value={value}
/>
)}
</Stack>
</Stack>
<DrawerController isOpen={isDrawerOpen}>
<CreateItemDrawer
listKey={foreignList.key}
onClose={() => {
setIsDrawerOpen(false);
}}
onCreate={val => {
setIsDrawerOpen(false);
if (value.kind === 'many') {
onChange({
...value,
value: [...value.value, val],
});
} else if (value.kind === 'one') {
onChange({
...value,
value: val,
});
}
}}
/>
</DrawerController>
</Fragment>
) : (
<RelationshipDisplay value={value} list={foreignList} />
)}
</FieldContainer>
);
};
export const Cell: CellComponent<typeof controller> = ({ field, item }) => {
const list = useList(field.refListKey);
const { colors } = useTheme();
if (field.display === 'count') {
const count = item[`${field.path}Count`] ?? 0;
return (
<CellContainer>
{count} {count === 1 ? list.singular : list.plural}
</CellContainer>
);
}
const data = item[field.path];
const items = (Array.isArray(data) ? data : [data]).filter(item => item);
const displayItems = items.length < 5 ? items : items.slice(0, 3);
const overflow = items.length < 5 ? 0 : items.length - 3;
const styles = {
color: colors.foreground,
textDecoration: 'none',
':hover': {
textDecoration: 'underline',
},
} as const;
return (
<CellContainer>
{displayItems.map((item, index) => (
<Fragment key={item.id}>
{!!index ? ', ' : ''}
<Link href={`/${list.path}/[id]`} as={`/${list.path}/${item.id}`} css={styles}>
{item.label || item.id}
</Link>
</Fragment>
))}
{overflow ? `, and ${overflow} more` : null}
</CellContainer>
);
};
export const CardValue: CardValueComponent<typeof controller> = ({ field, item }) => {
const list = useList(field.refListKey);
const data = item[field.path];
return (
<FieldContainer>
<FieldLabel>{field.label}</FieldLabel>
{(Array.isArray(data) ? data : [data])
.filter(item => item)
.map((item, index) => (
<Fragment key={item.id}>
{!!index ? ', ' : ''}
<Link href={`/${list.path}/[id]`} as={`/${list.path}/${item.id}`}>
{item.label || item.id}
</Link>
</Fragment>
))}
</FieldContainer>
);
};
type SingleRelationshipValue = {
kind: 'one';
id: null | string;
initialValue: { label: string; id: string } | null;
value: { label: string; id: string } | null;
};
type ManyRelationshipValue = {
kind: 'many';
id: null | string;
initialValue: { label: string; id: string }[];
value: { label: string; id: string }[];
};
type CardsRelationshipValue = {
kind: 'cards-view';
id: null | string;
itemsBeingEdited: ReadonlySet<string>;
itemBeingCreated: boolean;
initialIds: ReadonlySet<string>;
currentIds: ReadonlySet<string>;
displayOptions: CardsDisplayModeOptions;
};
type CountRelationshipValue = {
kind: 'count';
id: null | string;
count: number;
};
type CardsDisplayModeOptions = {
cardFields: readonly string[];
linkToItem: boolean;
removeMode: 'disconnect' | 'none';
inlineCreate: { fields: readonly string[] } | null;
inlineEdit: { fields: readonly string[] } | null;
inlineConnect: boolean;
};
type RelationshipController = FieldController<
ManyRelationshipValue | SingleRelationshipValue | CardsRelationshipValue | CountRelationshipValue,
string
> & {
display: 'count' | 'cards-or-select';
listKey: string;
refListKey: string;
refFieldKey?: string;
hideCreate: boolean;
many: boolean;
};
export const controller = (
config: FieldControllerConfig<
{
refFieldKey?: string;
refListKey: string;
many: boolean;
hideCreate: boolean;
} & (
| {
displayMode: 'select';
refLabelField: string;
}
| {
displayMode: 'cards';
cardFields: readonly string[];
linkToItem: boolean;
removeMode: 'disconnect' | 'none';
inlineCreate: { fields: readonly string[] } | null;
inlineEdit: { fields: readonly string[] } | null;
inlineConnect: boolean;
refLabelField: string;
}
| { displayMode: 'count' }
)
>
): RelationshipController => {
const cardsDisplayOptions =
config.fieldMeta.displayMode === 'cards'
? {
cardFields: config.fieldMeta.cardFields,
inlineCreate: config.fieldMeta.inlineCreate,
inlineEdit: config.fieldMeta.inlineEdit,
linkToItem: config.fieldMeta.linkToItem,
removeMode: config.fieldMeta.removeMode,
inlineConnect: config.fieldMeta.inlineConnect,
}
: undefined;
return {
refFieldKey: config.fieldMeta.refFieldKey,
many: config.fieldMeta.many,
listKey: config.listKey,
path: config.path,
label: config.label,
display: config.fieldMeta.displayMode === 'count' ? 'count' : 'cards-or-select',
refListKey: config.fieldMeta.refListKey,
graphqlSelection:
config.fieldMeta.displayMode === 'count'
? `${config.path}Count`
: `${config.path} {
id
label: ${config.fieldMeta.refLabelField}
}`,
hideCreate: config.fieldMeta.hideCreate,
// note we're not making the state kind: 'count' when ui.displayMode is set to 'count'.
// that ui.displayMode: 'count' is really just a way to have reasonable performance
// because our other UIs don't handle relationships with a large number of items well
// but that's not a problem here since we're creating a new item so we might as well them a better UI
defaultValue:
cardsDisplayOptions !== undefined
? {
kind: 'cards-view',
currentIds: new Set(),
id: null,
initialIds: new Set(),
itemBeingCreated: false,
itemsBeingEdited: new Set(),
displayOptions: cardsDisplayOptions,
}
: config.fieldMeta.many
? {
id: null,
kind: 'many',
initialValue: [],
value: [],
}
: { id: null, kind: 'one', value: null, initialValue: null },
deserialize: data => {
if (config.fieldMeta.displayMode === 'count') {
return { id: data.id, kind: 'count', count: data[`${config.path}Count`] ?? 0 };
}
if (cardsDisplayOptions !== undefined) {
const initialIds = new Set<string>(
(Array.isArray(data[config.path])
? data[config.path]
: data[config.path]
? [data[config.path]]
: []
).map((x: any) => x.id)
);
return {
kind: 'cards-view',
id: data.id,
itemsBeingEdited: new Set(),
itemBeingCreated: false,
initialIds,
currentIds: initialIds,
displayOptions: cardsDisplayOptions,
};
}
if (config.fieldMeta.many) {
let value = (data[config.path] || []).map((x: any) => ({
id: x.id,
label: x.label || x.id,
}));
return {
kind: 'many',
id: data.id,
initialValue: value,
value,
};
}
let value = data[config.path];
if (value) {
value = {
id: value.id,
label: value.label || value.id,
};
}
return {
kind: 'one',
id: data.id,
value,
initialValue: value,
};
},
filter: {
Filter: ({ onChange, value }) => {
const foreignList = useList(config.fieldMeta.refListKey);
const { filterValues, loading } = useRelationshipFilterValues({
value,
list: foreignList,
});
const state: {
kind: 'many';
value: { label: string; id: string }[];
onChange: (newItems: { label: string; id: string }[]) => void;
} = {
kind: 'many',
value: filterValues,
onChange(newItems) {
onChange(newItems.map(item => item.id).join(','));
},
};
return (
<FilteredRelationshipSelect
extraFilters={CUSTOM_FILTER}
controlShouldRenderValue
list={foreignList}
isLoading={loading}
isDisabled={onChange === undefined}
state={state}
/>
);
},
graphql: ({ value }) => {
const foreignIds = getForeignIds(value);
if (config.fieldMeta.many) {
return {
[config.path]: {
some: {
id: {
in: foreignIds,
},
},
},
};
}
return {
[config.path]: {
id: {
in: foreignIds,
},
},
};
},
Label({ value }) {
const foreignList = useList(config.fieldMeta.refListKey);
const { filterValues } = useRelationshipFilterValues({
value,
list: foreignList,
});
if (!filterValues.length) {
return `has no value`;
}
if (filterValues.length > 1) {
const values = filterValues.map((i: any) => i.label).join(', ');
return `is in [${values}]`;
}
const optionLabel = filterValues[0].label;
return `is ${optionLabel}`;
},
types: {
matches: {
label: 'Matches',
initialValue: '',
},
},
},
validate(value) {
return (
value.kind !== 'cards-view' ||
(value.itemsBeingEdited.size === 0 && !value.itemBeingCreated)
);
},
serialize: state => {
if (state.kind === 'many') {
const newAllIds = new Set(state.value.map(x => x.id));
const initialIds = new Set(state.initialValue.map(x => x.id));
let disconnect = state.initialValue
.filter(x => !newAllIds.has(x.id))
.map(x => ({ id: x.id }));
let connect = state.value.filter(x => !initialIds.has(x.id)).map(x => ({ id: x.id }));
if (disconnect.length || connect.length) {
let output: any = {};
if (disconnect.length) {
output.disconnect = disconnect;
}
if (connect.length) {
output.connect = connect;
}
return {
[config.path]: output,
};
}
} else if (state.kind === 'one') {
if (state.initialValue && !state.value) {
return { [config.path]: { disconnect: true } };
} else if (state.value && state.value.id !== state.initialValue?.id) {
return {
[config.path]: {
connect: {
id: state.value.id,
},
},
};
}
} else if (state.kind === 'cards-view') {
let disconnect = [...state.initialIds]
.filter(id => !state.currentIds.has(id))
.map(id => ({ id }));
let connect = [...state.currentIds]
.filter(id => !state.initialIds.has(id))
.map(id => ({ id }));
if (config.fieldMeta.many) {
if (disconnect.length || connect.length) {
return {
[config.path]: {
connect: connect.length ? connect : undefined,
disconnect: disconnect.length ? disconnect : undefined,
},
};
}
} else if (connect.length) {
return {
[config.path]: {
connect: connect[0],
},
};
} else if (disconnect.length) {
return { [config.path]: { disconnect: true } };
}
}
return {};
},
};
};
function useRelationshipFilterValues({ value, list }: { value: string; list: ListMeta }) {
const foreignIds = getForeignIds(value);
const where = { id: { in: foreignIds } };
const query = gql`
query FOREIGNLIST_QUERY($where: ${list.gqlNames.whereInputName}!) {
items: ${list.gqlNames.listQueryName}(where: $where) {
id
${list.labelField}
}
}
`;
const { data, loading } = useQuery(query, {
variables: {
where,
},
});
return {
filterValues:
data?.items?.map((item: any) => {
return {
id: item.id,
label: item[list.labelField] || item.id,
};
}) || foreignIds.map(f => ({ label: f, id: f })),
loading: loading,
};
}
function getForeignIds(value: string) {
if (typeof value === 'string' && value.length > 0) {
return value.split(',');
}
return [];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment