Skip to content

Commit 9cc5ea1

Browse files
[DT-2692] feat: pick content relationship fields (#1634)
Co-authored-by: Xavier Rutayisire <xavier.rutayisire@gmail.com>
1 parent f3bce37 commit 9cc5ea1

6 files changed

Lines changed: 1668 additions & 795 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"resolutions": {
6868
"connected-next-router/react-redux": "8.0.7",
6969
"react-beautiful-dnd/react-redux": "8.0.7",
70-
"express": "4.20.0"
70+
"express": "4.20.0",
71+
"@prismicio/types-internal": "3.10.0"
7172
},
7273
"workspaces": [
7374
"playwright",

packages/slice-machine/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"@emotion/react": "11.11.1",
4343
"@extractus/oembed-extractor": "3.1.8",
4444
"@prismicio/client": "7.17.0",
45-
"@prismicio/editor-fields": "0.4.75",
46-
"@prismicio/editor-support": "0.4.75",
47-
"@prismicio/editor-ui": "0.4.75",
45+
"@prismicio/editor-fields": "0.4.76",
46+
"@prismicio/editor-support": "0.4.76",
47+
"@prismicio/editor-ui": "0.4.76",
4848
"@prismicio/mock": "0.7.1",
4949
"@prismicio/mocks": "2.13.0",
5050
"@prismicio/simulator": "0.1.4",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { useStableCallback } from "@prismicio/editor-support/React";
2+
import {
3+
Box,
4+
Text,
5+
TreeView,
6+
TreeViewCheckbox,
7+
TreeViewCheckboxProps,
8+
TreeViewSection,
9+
} from "@prismicio/editor-ui";
10+
import { FormikContext, useField, useFormik } from "formik";
11+
import { useEffect } from "react";
12+
import { useSelector } from "react-redux";
13+
14+
import { selectAllCustomTypes } from "@/modules/availableCustomTypes";
15+
16+
type FieldMap = Record<string, boolean>;
17+
export type CustomTypeFieldMap = Record<string, FieldMap>;
18+
19+
type CustomTypeField = string | { id: string; fields: string[] };
20+
21+
interface ContentRelationshipFieldPickerProps {
22+
initialValues: CustomTypeField[] | undefined;
23+
onChange: (fields: CustomTypeField[]) => void;
24+
}
25+
26+
export function ContentRelationshipFieldPicker(
27+
props: ContentRelationshipFieldPickerProps,
28+
) {
29+
const { initialValues, onChange } = props;
30+
const customTypes = useCustomTypes();
31+
32+
const stableOnChange = useStableCallback(onChange);
33+
const form = useFormik<CustomTypeFieldMap>({
34+
initialValues: initialValues ? convertCustomTypesToForm(initialValues) : {},
35+
onSubmit: () => undefined, // values will be updated on change
36+
});
37+
38+
useEffect(() => {
39+
stableOnChange(convertFormToCustomTypes(form.values));
40+
}, [form.values, stableOnChange]);
41+
42+
return (
43+
<FormikContext.Provider value={form}>
44+
<Box overflow="hidden" flexDirection="column" border borderRadius={6}>
45+
<Box
46+
border={{ bottom: true }}
47+
padding={{ inline: 16, bottom: 16, top: 12 }}
48+
flexDirection="column"
49+
gap={8}
50+
>
51+
<Box flexDirection="column">
52+
<Text variant="h4" color="grey12">
53+
Types
54+
</Text>
55+
<Text color="grey12">
56+
Choose which fields you want to expose from the linked document.
57+
</Text>
58+
</Box>
59+
<TreeView
60+
title="Exposed fields"
61+
subtitle={`(${countPickedFields(form.values)})`}
62+
>
63+
{customTypes.map((ct) => {
64+
const count = countPickedFields(form.values[ct.id]);
65+
const countLabel = count === 1 ? "1 field" : `${count} fields`;
66+
67+
return (
68+
<TreeViewSection
69+
key={ct.id}
70+
title={ct.label}
71+
subtitle={count > 0 ? `(${countLabel} exposed)` : undefined}
72+
badge="Custom type"
73+
>
74+
{ct.fields.map((field) => (
75+
<TreeViewCheckboxField
76+
key={field.id}
77+
id={field.id}
78+
title={field.label}
79+
customTypeId={ct.id}
80+
/>
81+
))}
82+
</TreeViewSection>
83+
);
84+
})}
85+
</TreeView>
86+
</Box>
87+
<Box backgroundColor="white" flexDirection="column" padding={12}>
88+
<Text variant="normal" color="grey11">
89+
Have ideas for improving this field?{" "}
90+
<a
91+
// TODO: Add real URL: https://linear.app/prismic/issue/DT-2693
92+
href="https://community.prismic.io/t/TODO"
93+
target="_blank"
94+
rel="noopener noreferrer"
95+
style={{ color: "inherit", textDecoration: "underline" }}
96+
>
97+
Please provide your feedback here.
98+
</a>
99+
</Text>
100+
</Box>
101+
</Box>
102+
</FormikContext.Provider>
103+
);
104+
}
105+
106+
function TreeViewCheckboxField(
107+
props: {
108+
id: string;
109+
customTypeId: string;
110+
} & Omit<TreeViewCheckboxProps, "checked" | "onCheckedChange">,
111+
) {
112+
const { id, customTypeId, ...checkboxProps } = props;
113+
const [field, _, helpers] = useField<boolean>(`${customTypeId}.${id}`);
114+
115+
return (
116+
<TreeViewCheckbox
117+
{...checkboxProps}
118+
checked={field.value}
119+
onCheckedChange={(checked) => helpers.setValue(checked)}
120+
/>
121+
);
122+
}
123+
124+
type SimplifiedCustomTypeField = {
125+
id: string;
126+
label: string;
127+
};
128+
129+
interface SimplifiedCustomType {
130+
id: string;
131+
label: string;
132+
fields: SimplifiedCustomTypeField[];
133+
}
134+
135+
function useCustomTypes() {
136+
const customTypes = useSelector(selectAllCustomTypes);
137+
const simplifiedCustomTypes = customTypes.flatMap<SimplifiedCustomType>(
138+
(customType) => {
139+
// In the store we have remote and local custom types, we want to show
140+
// the local ones, so that the user is able to create a content
141+
// relationship with custom types present on the user's computer (pushed
142+
// or not).
143+
if (!("local" in customType)) return [];
144+
145+
const { id, label, tabs } = customType.local;
146+
147+
const fields = tabs.flatMap<SimplifiedCustomTypeField>((tab) => {
148+
return tab.value.flatMap((field) => {
149+
// filter out uid fields because it's a special field returned by the
150+
// API and is not part of the data object in the document.
151+
if (field.value.type === "UID" || field.key === "uid") {
152+
return [];
153+
}
154+
155+
return {
156+
id: field.key,
157+
label: field.value.config?.label ?? field.key,
158+
};
159+
});
160+
});
161+
162+
if (fields.length === 0) return [];
163+
164+
return { id, label: label ?? id, fields };
165+
},
166+
);
167+
168+
simplifiedCustomTypes.sort((a, b) => a.id.localeCompare(b.id));
169+
return simplifiedCustomTypes;
170+
}
171+
172+
function countPickedFields(fields: CustomTypeFieldMap | FieldMap | undefined) {
173+
if (!fields) return 0;
174+
175+
return Object.values(fields).reduce<number>(
176+
(count, value: boolean | FieldMap) => {
177+
if (typeof value === "boolean" && value) return count + 1;
178+
return count + Object.values(value).filter(Boolean).length;
179+
},
180+
0,
181+
);
182+
}
183+
184+
function convertCustomTypesToForm(value: CustomTypeField[]) {
185+
return value.reduce<CustomTypeFieldMap>((customTypes, customType) => {
186+
if (typeof customType === "string") {
187+
customTypes[customType] = {};
188+
return customTypes;
189+
}
190+
191+
const { id, fields } = customType;
192+
customTypes[id] = fields.reduce<FieldMap>((customTypeFields, field) => {
193+
customTypeFields[field] = true;
194+
return customTypeFields;
195+
}, {});
196+
197+
return customTypes;
198+
}, {});
199+
}
200+
201+
/** Convert the picked fields map to the customtypes config and filter out empty customtypes */
202+
function convertFormToCustomTypes(fields: CustomTypeFieldMap) {
203+
return Object.entries(fields).flatMap<CustomTypeField>(([ctId, fields]) => {
204+
const fieldEntries = Object.entries(fields);
205+
if (!fieldEntries.some(([_, checked]) => checked)) return [];
206+
return [
207+
{
208+
id: ctId,
209+
fields: fieldEntries.flatMap(([id, checked]) => (checked ? [id] : [])),
210+
},
211+
];
212+
});
213+
}

packages/slice-machine/src/legacy/lib/models/common/widgets/ContentRelationship/Form.tsx

Lines changed: 43 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,71 @@
11
import { FormikProps } from "formik";
2-
import { useSelector } from "react-redux";
3-
import Select from "react-select";
4-
import { Box, Label } from "theme-ui";
2+
import { Box } from "theme-ui";
53
import * as yup from "yup";
64

5+
import { ContentRelationshipFieldPicker } from "@/features/customTypes/fields/ContentRelationshipFieldPicker";
76
import { Col, Flex as FlexGrid } from "@/legacy/components/Flex";
87
import WidgetFormField from "@/legacy/lib/builders/common/EditModal/Field";
98
import { createFieldNameFromKey } from "@/legacy/lib/forms";
109
import { DefaultFields } from "@/legacy/lib/forms/defaults";
11-
import { selectAllCustomTypes } from "@/modules/availableCustomTypes";
12-
13-
import { hasLocal } from "../../ModelData";
1410

1511
const FormFields = {
1612
label: DefaultFields.label,
1713
id: DefaultFields.id,
1814
customtypes: {
19-
validate: () => yup.array().of(yup.string()),
15+
validate: () =>
16+
yup.array(
17+
yup.lazy((value) => {
18+
return typeof value === "object"
19+
? yup.object({
20+
id: yup.string(),
21+
fields: yup.array(yup.string()),
22+
})
23+
: yup.string();
24+
}),
25+
),
2026
},
2127
};
2228

2329
type FormProps = {
24-
config: { label: string; select: string; customtypes?: string[] };
30+
config: {
31+
label: string;
32+
select: string;
33+
customtypes?: (string | { id: string; fields: string[] })[];
34+
};
2535
id: string;
26-
// type: string; // TODO: this exists in the yup schema but this doesn't seem to be validated by formik
36+
// TODO: this exists in the yup schema but this doesn't seem to be validated by formik
2737
};
2838

2939
const WidgetForm = ({
3040
initialValues,
31-
values: formValues,
32-
fields,
3341
setFieldValue,
42+
fields,
3443
}: FormikProps<FormProps> & { fields: Record<string, unknown> }) => {
35-
const customTypes = useSelector(selectAllCustomTypes).filter(hasLocal);
36-
37-
const options = customTypes.map((ct) => ({
38-
value: ct.local.id,
39-
label: ct.local.label,
40-
}));
41-
42-
const selectValues = formValues.config.customtypes
43-
? formValues.config.customtypes.map((id) => {
44-
const ct = customTypes.find(
45-
(frontendCustomType) => frontendCustomType.local.id === id,
46-
);
47-
return { value: ct?.local.id, label: ct?.local.label };
48-
})
49-
: null;
50-
5144
return (
52-
<FlexGrid>
53-
{Object.entries(FormFields)
54-
.filter((e) => e[0] !== "customtypes")
55-
.map(([key, field]) => (
56-
<Col key={key}>
57-
<WidgetFormField
58-
fieldName={createFieldNameFromKey(key)}
59-
formField={field}
60-
fields={fields}
61-
initialValues={initialValues}
62-
/>
63-
</Col>
64-
))}
65-
<Col>
66-
<Box
67-
sx={{
68-
mt: 2,
69-
alignItems: "center",
45+
<>
46+
<FlexGrid>
47+
{Object.entries(FormFields)
48+
.filter((e) => e[0] !== "customtypes")
49+
.map(([key, field]) => (
50+
<Col key={key}>
51+
<WidgetFormField
52+
fieldName={createFieldNameFromKey(key)}
53+
formField={field}
54+
fields={fields}
55+
initialValues={initialValues}
56+
/>
57+
</Col>
58+
))}
59+
</FlexGrid>
60+
<Box mt={20}>
61+
<ContentRelationshipFieldPicker
62+
initialValues={initialValues.config.customtypes}
63+
onChange={(fields) => {
64+
void setFieldValue("config.customtypes", fields);
7065
}}
71-
>
72-
<Label htmlFor="origin" mb="1">
73-
Types
74-
</Label>
75-
<Select
76-
isMulti
77-
name="origin"
78-
options={options}
79-
onChange={(v) => {
80-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
81-
if (v) {
82-
void setFieldValue(
83-
"config.customtypes",
84-
v.map(({ value }) => value),
85-
);
86-
}
87-
}}
88-
value={selectValues}
89-
theme={(theme) => {
90-
return {
91-
...theme,
92-
colors: {
93-
...theme.colors,
94-
text: "text",
95-
primary: "background",
96-
},
97-
};
98-
}}
99-
/>
100-
</Box>
101-
</Col>
102-
</FlexGrid>
66+
/>
67+
</Box>
68+
</>
10369
);
10470
};
10571

0 commit comments

Comments
 (0)