Skip to content

Commit 01b81b8

Browse files
authored
Added "start from a template" on object creation form (#5828)
1 parent 17e4e46 commit 01b81b8

32 files changed

+1075
-103
lines changed

frontend/app/src/config/constants.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,14 @@ export const relationshipsForListView = {
129129
many: ["Attribute"],
130130
};
131131

132-
export const relationshipsForDetailsView = {
132+
export const relationshipsForDetailsView: { one: RelationshipKind[]; many: RelationshipKind[] } = {
133133
one: ["Generic", "Attribute", "Component", "Parent", "Hierarchy"],
134134
many: ["Attribute", "Parent"],
135135
};
136136

137-
export const relationshipsForTabs = {
137+
export const relationshipsForTabs: { one: RelationshipKind[]; many: RelationshipKind[] } = {
138138
one: [],
139-
many: ["Generic", "Component", "Hierarchy"],
139+
many: ["Generic", "Component", "Hierarchy", "Template"],
140140
};
141141

142142
export const RELATIONSHIP_VIEW_BLACKLIST = [

frontend/app/src/entities/nodes/object-item-details/action-buttons/relationships-buttons.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { QSP } from "@/config/qsp";
22
import { ADD_RELATIONSHIP } from "@/entities/nodes/relationships/api/addRelationship";
33
import { Permission } from "@/entities/permission/types";
44
import { genericSchemasAtom, nodeSchemasAtom } from "@/entities/schema/stores/schema.atom";
5+
import { ModelSchema } from "@/entities/schema/types";
56
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
67
import { useMutation } from "@/shared/api/graphql/useQuery";
78
import { queryClient } from "@/shared/api/rest/client";
@@ -19,16 +20,19 @@ import { StringParam, useQueryParam } from "use-query-params";
1920

2021
interface RelationshipsButtonsProps {
2122
permission: Permission;
23+
schema: ModelSchema;
2224
}
2325

24-
export function RelationshipsButtons({ permission }: RelationshipsButtonsProps) {
26+
export function RelationshipsButtons({
27+
permission,
28+
schema: parentSchema,
29+
}: RelationshipsButtonsProps) {
2530
const { objectKind, objectid } = useParams();
2631
const [addRelationship] = useMutation(ADD_RELATIONSHIP);
2732
const generics = useAtomValue(genericSchemasAtom);
2833
const schemaList = useAtomValue(nodeSchemasAtom);
2934
const [relationshipTab] = useQueryParam(QSP.TAB, StringParam);
3035

31-
const parentSchema = schemaList.find((s) => s.kind === objectKind);
3236
const parentGeneric = generics.find((s) => s.kind === objectKind);
3337
const relationshipSchema = parentSchema?.relationships?.find((r) => r?.name === relationshipTab);
3438
const relationshipGeneric = parentGeneric?.relationships?.find(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useObjects } from "@/entities/nodes/object/domain/get-objects.query";
2+
import { getNodeLabel } from "@/entities/nodes/object/utils/get-node-label";
3+
import { NodeObject } from "@/entities/nodes/types";
4+
import { TemplateSchema } from "@/entities/schema/types";
5+
import {
6+
ComboboxEmpty,
7+
ComboboxItem,
8+
ComboboxList,
9+
ComboboxListProps,
10+
} from "@/shared/components/ui/combobox";
11+
import { Spinner } from "@/shared/components/ui/spinner";
12+
13+
export interface ObjectTemplateAutocompleteProps extends Omit<ComboboxListProps, "onSelect"> {
14+
templateSchema: TemplateSchema;
15+
onSelect: (node: NodeObject) => void;
16+
}
17+
18+
export function ObjectTemplateAutocomplete({
19+
templateSchema,
20+
onSelect,
21+
...props
22+
}: ObjectTemplateAutocompleteProps) {
23+
const { data, isPending, error } = useObjects({
24+
schema: templateSchema,
25+
getAttributesVisible: (attributes) => attributes,
26+
getRelationshipsVisible: (relationships) => relationships,
27+
});
28+
29+
if (error) {
30+
return <div>Error: {error.message}</div>;
31+
}
32+
33+
return (
34+
<ComboboxList {...props}>
35+
{isPending && <Spinner className="flex justify-center m-2" />}
36+
37+
<ComboboxEmpty>No template found</ComboboxEmpty>
38+
39+
{data?.pages.flat().map((node) => (
40+
<ComboboxItem key={node.id} value={node.id} onSelect={() => onSelect(node)}>
41+
<span className="truncate">{getNodeLabel(node)}</span>
42+
</ComboboxItem>
43+
))}
44+
</ComboboxList>
45+
);
46+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ObjectTemplateAutocomplete } from "@/entities/nodes/object-template/object-template-autocomplete";
2+
import { NodeObject } from "@/entities/nodes/types";
3+
import { useSchema } from "@/entities/schema/ui/hooks/useSchema";
4+
import { Popover } from "@/shared/components/aria/popover";
5+
import ObjectForm, { ObjectFormProps } from "@/shared/components/form/object-form";
6+
import { classNames } from "@/shared/utils/common";
7+
import { FileBoxIcon, PlusIcon } from "lucide-react";
8+
import { useEffect, useRef, useState } from "react";
9+
import { Button, ButtonProps, Dialog, DialogTrigger } from "react-aria-components";
10+
11+
interface StartButtonProps extends ButtonProps {
12+
icon: React.ReactNode;
13+
title: string;
14+
description: string;
15+
className?: string;
16+
ref?: React.Ref<HTMLButtonElement>;
17+
}
18+
19+
const StartButton = ({ icon, title, description, className, ...props }: StartButtonProps) => (
20+
<Button
21+
className={classNames(
22+
"flex items-center gap-2 border border-dashed border-gray-400 p-4 rounded-lg hover:bg-gray-50",
23+
className
24+
)}
25+
{...props}
26+
>
27+
<div className="bg-indigo-100 rounded-lg p-3">{icon}</div>
28+
29+
<div className="flex flex-col items-start gap-1">
30+
<p className="text-sm font-medium">{title}</p>
31+
<p className="text-xs text-gray-600">{description}</p>
32+
</div>
33+
</Button>
34+
);
35+
36+
const StartFromTemplateButton = ({
37+
objectTemplateSchema,
38+
onSelect,
39+
}: {
40+
objectTemplateSchema: any;
41+
onSelect: (template: NodeObject | null) => void;
42+
}) => {
43+
let buttonRef = useRef<HTMLButtonElement>(null);
44+
let [buttonWidth, setButtonWidth] = useState<string | null>(null);
45+
useEffect(() => {
46+
if (buttonRef.current) {
47+
setButtonWidth(buttonRef.current.offsetWidth + "px");
48+
}
49+
}, [buttonRef]);
50+
51+
return (
52+
<DialogTrigger>
53+
<StartButton
54+
ref={buttonRef}
55+
icon={<FileBoxIcon className="size-6" />}
56+
title="Start from template"
57+
description="Pick a premade object and customize it"
58+
/>
59+
60+
<Popover style={buttonWidth ? { width: buttonWidth } : undefined} placement="bottom start">
61+
<Dialog>
62+
<ObjectTemplateAutocomplete
63+
autoFocus
64+
templateSchema={objectTemplateSchema}
65+
onSelect={onSelect}
66+
/>
67+
</Dialog>
68+
</Popover>
69+
</DialogTrigger>
70+
);
71+
};
72+
73+
export interface ObjectTemplateFormProps extends ObjectFormProps {
74+
objectTemplateKind: string;
75+
}
76+
77+
export default function ObjectTemplateForm({
78+
objectTemplateKind,
79+
...props
80+
}: ObjectTemplateFormProps) {
81+
const { schema: objectTemplateSchema } = useSchema(objectTemplateKind);
82+
const [selectedObjectTemplate, setSelectedObjectTemplate] = useState<NodeObject | null>();
83+
84+
if (!objectTemplateSchema) {
85+
return `Could not find template schema for ${objectTemplateKind}`;
86+
}
87+
88+
if (selectedObjectTemplate !== undefined) {
89+
return <ObjectForm {...props} objectTemplate={selectedObjectTemplate} />;
90+
}
91+
92+
return (
93+
<div className="flex flex-col gap-4 p-6">
94+
<StartButton
95+
icon={<PlusIcon className="size-6" />}
96+
title="Start from scratch"
97+
description="Create a new blank object"
98+
onPress={() => setSelectedObjectTemplate(null)}
99+
/>
100+
<StartFromTemplateButton
101+
objectTemplateSchema={objectTemplateSchema}
102+
onSelect={setSelectedObjectTemplate}
103+
/>
104+
</div>
105+
);
106+
}

frontend/app/src/entities/nodes/object/domain/get-objects.query.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import { useCurrentBranch } from "@/entities/branches/ui/branches-provider";
2-
import { ModelSchema } from "@/entities/schema/types";
3-
import { ContextParams } from "@/shared/api/types";
4-
import { Filter } from "@/shared/hooks/useFilters";
2+
import { PaginationParams } from "@/shared/api/types";
53
import { datetimeAtom } from "@/shared/stores/time.atom";
64
import { infiniteQueryOptions, useInfiniteQuery } from "@tanstack/react-query";
75
import { useAtomValue } from "jotai";
8-
import { OBJECTS_PER_PAGE, getObjects } from "./get-objects";
6+
import { GetObjectsParams, OBJECTS_PER_PAGE, getObjects } from "./get-objects";
97

10-
type GetObjectsQueryParams = ContextParams & {
11-
schema: ModelSchema;
12-
filters?: Array<Filter>;
13-
};
8+
type GetObjectsQueryParams = Omit<GetObjectsParams, keyof PaginationParams>;
149

1510
export function getObjectsInfiniteQueryOptions({
1611
schema,
1712
filters,
1813
branchName,
1914
atDate,
15+
getAttributesVisible,
16+
getRelationshipsVisible,
2017
}: GetObjectsQueryParams) {
2118
return infiniteQueryOptions({
2219
queryKey: [branchName, atDate, "objects", schema.kind, JSON.stringify(filters)],
@@ -27,6 +24,8 @@ export function getObjectsInfiniteQueryOptions({
2724
branchName,
2825
atDate,
2926
filters,
27+
getAttributesVisible,
28+
getRelationshipsVisible,
3029
});
3130
},
3231
initialPageParam: 0,

frontend/app/src/entities/nodes/object/domain/get-objects.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getAttributesVisibleInListView } from "@/entities/nodes/object/utils/get-attributes-visible-in-list";
22
import { getRelationshipsVisibleInListView } from "@/entities/nodes/object/utils/get-relationships-visible-in-list";
33
import { NodeObject } from "@/entities/nodes/types";
4-
import { ModelSchema } from "@/entities/schema/types";
4+
import { AttributeSchema, ModelSchema, RelationshipSchema } from "@/entities/schema/types";
55
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
66
import {
77
addAttributesToRequest,
@@ -19,13 +19,15 @@ export const OBJECTS_PER_PAGE = 40;
1919

2020
////////////////////////////////////////////////////////////////////////////////////////////////////
2121

22-
export type GetObjects = (
23-
args: ContextParams &
24-
PaginationParams & {
25-
schema: ModelSchema;
26-
filters?: Array<Filter>;
27-
}
28-
) => Promise<Array<NodeObject>>;
22+
export type GetObjectsParams = ContextParams &
23+
PaginationParams & {
24+
schema: ModelSchema;
25+
filters?: Array<Filter>;
26+
getAttributesVisible?: (attributes: AttributeSchema[]) => AttributeSchema[];
27+
getRelationshipsVisible?: (relationships: RelationshipSchema[]) => RelationshipSchema[];
28+
};
29+
30+
export type GetObjects = (args: GetObjectsParams) => Promise<Array<NodeObject>>;
2931

3032
export const getObjects: GetObjects = async ({
3133
schema,
@@ -34,9 +36,11 @@ export const getObjects: GetObjects = async ({
3436
branchName,
3537
atDate,
3638
filters,
39+
getAttributesVisible = getAttributesVisibleInListView,
40+
getRelationshipsVisible = getRelationshipsVisibleInListView,
3741
}) => {
38-
const attributesVisible = getAttributesVisibleInListView(schema.attributes ?? []);
39-
const relationshipsVisible = getRelationshipsVisibleInListView(schema.relationships ?? []);
42+
const attributesVisible = getAttributesVisible(schema.attributes ?? []);
43+
const relationshipsVisible = getRelationshipsVisible(schema.relationships ?? []);
4044

4145
const schemaKind = schema.kind as string;
4246
const kindFilter = filters?.find((filter) => filter.name === "kind__value");

frontend/app/src/entities/nodes/object/ui/object-table/cells/table-relationship-cell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getNodeLabel } from "@/entities/nodes/object/utils/get-node-label";
22
import {
3+
NodeCore,
34
NodeRelationship,
45
NodeRelationshipMany,
56
NodeRelationshipOne,
@@ -36,7 +37,7 @@ export function TableRelationshipCell({
3637
return nodes.map((node) => <RelationshipNodeDisplay key={node.id} node={node} />);
3738
}
3839

39-
export function RelationshipNodeDisplay({ node }: NodeRelationshipOne) {
40+
export function RelationshipNodeDisplay({ node }: { node: NodeCore }) {
4041
const { schema } = useSchema(node.__typename);
4142

4243
if (!schema) return `Schema for ${node.__typename} not found`;

frontend/app/src/entities/nodes/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type NodeAttribute = {
1616
};
1717

1818
export type NodeRelationshipOne = {
19-
node: NodeCore;
19+
node: NodeCore | null;
2020
};
2121

2222
export type NodeRelationshipMany = {

frontend/app/src/entities/role-manager/ui/account-role-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const AccountRoleForm = ({
4848

4949
const permissions = getRelationshipDefaultValue({
5050
relationshipData: currentObject?.permissions?.value,
51-
peerField: "identifier",
51+
relationshipName: "identifier",
5252
});
5353

5454
const defaultValues = {

frontend/app/src/entities/schema/ui/schema-viewer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ModelSchema } from "@/entities/schema/types";
99
import { isGenericSchema } from "@/entities/schema/utils/is-generic-schema";
1010
import { isNodeSchema } from "@/entities/schema/utils/is-node-schema";
1111
import { isProfileSchema } from "@/entities/schema/utils/is-profile-schema";
12+
import { isTemplateSchema } from "@/entities/schema/utils/is-template-schema";
1213
import { Button } from "@/shared/components/buttons/button-primitive";
1314
import { Badge } from "@/shared/components/ui/badge";
1415
import { classNames } from "@/shared/utils/common";
@@ -209,9 +210,15 @@ const Properties = ({ schema }: { schema: ModelSchema }) => {
209210
<PropertyRow title="Default filter" value={schema.default_filter} />
210211
<PropertyRow title="Order by" value={schema.order_by} />
211212
<PropertyRow title="Uniqueness constraints" value={schema.uniqueness_constraints} />
213+
</div>
214+
215+
<div>
212216
{!isProfileSchema(schema) && (
213217
<PropertyRow title="Generate profile" value={schema.generate_profile} />
214218
)}
219+
{!isTemplateSchema(schema) && (
220+
<PropertyRow title="Generate template" value={schema.generate_template} />
221+
)}
215222
</div>
216223

217224
<div>

0 commit comments

Comments
 (0)