Skip to content

Commit 2775c1d

Browse files
authored
Simplified relationship many component (#5902)
1 parent e5069d5 commit 2775c1d

File tree

7 files changed

+235
-154
lines changed

7 files changed

+235
-154
lines changed

frontend/app/src/entities/nodes/api/generateRelationshipListQuery.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ export const generateRelationshipListQuery = ({
77
limit = 0,
88
offset = 0,
99
search = "",
10-
peerField,
1110
}: PaginationParams & {
1211
peer: string;
1312
parent?: { name?: string; value?: string };
1413
search?: string;
15-
peerField?: string;
1614
}): string => {
1715
const defaultArgs = { limit, offset, any__value: search, partial_match: true };
1816

@@ -31,8 +29,9 @@ export const generateRelationshipListQuery = ({
3129
edges: {
3230
node: {
3331
id: true,
32+
hfid: true,
3433
display_label: true,
35-
...(peerField ? { [peerField]: { value: true } } : {}),
34+
__typename: true,
3635
},
3736
},
3837
},

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
ACCOUNT_GROUP_OBJECT,
3-
ACCOUNT_PERMISSION_OBJECT,
4-
ACCOUNT_ROLE_OBJECT,
5-
} from "@/config/constants";
1+
import { ACCOUNT_GROUP_OBJECT, ACCOUNT_ROLE_OBJECT } from "@/config/constants";
62
import { currentBranchAtom } from "@/entities/branches/stores";
73
import { createObject } from "@/entities/nodes/api/createObject";
84
import { updateObjectWithId } from "@/entities/nodes/api/updateObjectWithId";
@@ -14,17 +10,20 @@ import { FormFieldValue } from "@/shared/components/form/type";
1410
import { getCurrentFieldValue } from "@/shared/components/form/utils/getFieldDefaultValue";
1511
import { getCreateMutationFromFormDataOnly } from "@/shared/components/form/utils/mutations/getCreateMutationFromFormData";
1612
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
17-
import { Form, FormSubmit } from "@/shared/components/ui/form";
13+
import { Form, FormField, FormInput, FormSubmit } from "@/shared/components/ui/form";
1814
import { datetimeAtom } from "@/shared/stores/time.atom";
1915
import { stringifyWithoutQuotes } from "@/shared/utils/string";
2016
import { gql } from "@apollo/client";
2117
import { useAtomValue } from "jotai";
2218
import { FieldValues, useForm } from "react-hook-form";
2319
import { toast } from "react-toastify";
2420

21+
import { PermissionCombobox } from "@/entities/role-manager/ui/permission-combobox";
22+
import { LabelFormField } from "@/shared/components/form/fields/common";
2523
import InputField from "@/shared/components/form/fields/input.field";
2624
import RelationshipManyField from "@/shared/components/form/fields/relationship-many.field";
2725
import { getRelationshipDefaultValue } from "@/shared/components/form/utils/getRelationshipDefaultValue";
26+
import { updateRelationshipFieldValue } from "@/shared/components/form/utils/updateFormFieldValue";
2827
import { isRequired } from "@/shared/components/form/utils/validation";
2928

3029
interface NumberPoolFormProps extends Pick<NodeFormProps, "onSuccess"> {
@@ -138,16 +137,26 @@ export const AccountRoleForm = ({
138137
options={groups.value}
139138
/>
140139

141-
<RelationshipManyField
140+
<FormField
142141
name="permissions"
143-
label="Permissions"
144-
relationship={{
145-
name: "permissions",
146-
peer: ACCOUNT_PERMISSION_OBJECT,
147-
cardinality: "many",
142+
render={({ field }) => {
143+
const fieldData = field.value;
144+
return (
145+
<div className="flex flex-col gap-2">
146+
<LabelFormField label="Permissions" fieldData={fieldData} />
147+
148+
<FormInput>
149+
<PermissionCombobox
150+
{...field}
151+
value={fieldData.value}
152+
onChange={(newValue) => {
153+
field.onChange(updateRelationshipFieldValue(newValue, permissions));
154+
}}
155+
/>
156+
</FormInput>
157+
</div>
158+
);
148159
}}
149-
options={permissions.value}
150-
peerField="identifier"
151160
/>
152161

153162
<div className="text-right">
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { ACCOUNT_PERMISSION_OBJECT } from "@/config/constants";
2+
import { useObjects } from "@/entities/nodes/object/domain/get-objects.query";
3+
import { AddRelationshipAction } from "@/entities/nodes/relationships/ui/add-relationship-action";
4+
import { NodeCore } from "@/entities/nodes/types";
5+
import { useSchema } from "@/entities/schema/ui/hooks/useSchema";
6+
import { Button } from "@/shared/components/buttons/button-primitive";
7+
import ErrorScreen from "@/shared/components/errors/error-screen";
8+
import { Badge } from "@/shared/components/ui/badge";
9+
import {
10+
Combobox,
11+
ComboboxContent,
12+
ComboboxEmpty,
13+
ComboboxItem,
14+
ComboboxList,
15+
} from "@/shared/components/ui/combobox";
16+
import { PopoverTrigger } from "@/shared/components/ui/popover";
17+
import { Spinner } from "@/shared/components/ui/spinner";
18+
import { inputStyle } from "@/shared/components/ui/style";
19+
import { classNames, debounce } from "@/shared/utils/common";
20+
import { Icon } from "@iconify-icon/react";
21+
import { PopoverTriggerProps } from "@radix-ui/react-popover";
22+
import React, { forwardRef } from "react";
23+
24+
type PermissionNode = NodeCore & { identifier: { value: string } };
25+
26+
export interface PermissionComboboxProps extends Omit<PopoverTriggerProps, "value" | "onChange"> {
27+
value: PermissionNode[] | null;
28+
onChange: (value: PermissionNode[]) => void;
29+
}
30+
31+
// This component is a temporary solution to display the permissions in a combobox
32+
// We cannot use relationship many because the general beheviour is to use hfid/display_label
33+
// On permission, label is it an attribute called identifier
34+
export function PermissionCombobox({
35+
value,
36+
onChange,
37+
className,
38+
...props
39+
}: PermissionComboboxProps) {
40+
const [open, setOpen] = React.useState(false);
41+
42+
const handleSelect = (relationship: PermissionNode) => {
43+
onChange(value ? [...value, relationship] : [relationship]);
44+
};
45+
46+
return (
47+
<Combobox open={open} onOpenChange={setOpen}>
48+
<PopoverTrigger asChild>
49+
<div
50+
className={classNames(
51+
inputStyle,
52+
"has-[>:last-child:focus]:outline-none has-[>:last-child:focus]:ring-2 has-[>:last-child:focus]:ring-custom-blue-600/25 has-[>:last-child:focus]:border-custom-blue-600",
53+
"cursor-pointer",
54+
className
55+
)}
56+
>
57+
<div className="flex-grow flex flex-wrap gap-2">
58+
{value?.map((node) => (
59+
<Badge key={node.id} className="flex items-center gap-1 pr-0.5">
60+
{node.identifier.value}
61+
62+
<Button
63+
size="icon"
64+
variant="ghost"
65+
onClick={(e) => {
66+
e.stopPropagation();
67+
onChange(value.filter((item) => item.id !== node.id));
68+
}}
69+
className="text-gray-500 hover:text-gray-800 h-4 w-4"
70+
aria-label="Remove"
71+
data-testid="remove-option"
72+
>
73+
&times;
74+
</Button>
75+
</Badge>
76+
))}
77+
</div>
78+
79+
<button
80+
type="button"
81+
className="text-gray-600 outline-none w-3.5 h-3.5"
82+
onClick={() => setOpen(!open)}
83+
{...props}
84+
>
85+
<Icon icon="mdi:unfold-more-horizontal" />
86+
</button>
87+
</div>
88+
</PopoverTrigger>
89+
90+
<ComboboxContent>
91+
<PermissionComboboxList onSelect={handleSelect} value={value} />
92+
<AddRelationshipAction peer={ACCOUNT_PERMISSION_OBJECT} onSuccess={handleSelect} />
93+
</ComboboxContent>
94+
</Combobox>
95+
);
96+
}
97+
98+
export interface RelationshipComboboxListProps {
99+
value: PermissionNode[] | null;
100+
onSelect: (value: PermissionNode) => void;
101+
}
102+
103+
export const PermissionComboboxList = forwardRef<HTMLDivElement, RelationshipComboboxListProps>(
104+
({ value, onSelect }, ref) => {
105+
const [search, setSearch] = React.useState("");
106+
const { schema } = useSchema(ACCOUNT_PERMISSION_OBJECT);
107+
const { isPending, data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useObjects({
108+
schema: schema!,
109+
filters: search ? [{ name: "any__value", value: search }] : undefined,
110+
});
111+
112+
if (error) return <ErrorScreen message={error.message} />;
113+
114+
const setSearchDebounced = debounce(setSearch, 300);
115+
116+
return (
117+
<ComboboxList
118+
ref={ref}
119+
onValueChange={(newValue) => setSearchDebounced(newValue)}
120+
shouldFilter={false}
121+
>
122+
{isPending ? (
123+
<Spinner className="flex justify-center m-2" />
124+
) : (
125+
<>
126+
<ComboboxEmpty>No {schema?.label ?? "results"} found</ComboboxEmpty>
127+
128+
{data.pages.map((page) => {
129+
return page
130+
.filter((node) => !value?.some((v) => v.id === node.id))
131+
.map((n) => {
132+
const node = n as unknown as PermissionNode;
133+
return (
134+
<ComboboxItem
135+
key={node.id}
136+
value={node.id}
137+
selectedValue={null}
138+
onSelect={() =>
139+
onSelect({
140+
id: node.id,
141+
display_label: node.identifier.value,
142+
identifier: { value: node.identifier.value },
143+
__typename: node.__typename,
144+
})
145+
}
146+
>
147+
<span className="truncate">{node.identifier.value}</span>
148+
</ComboboxItem>
149+
);
150+
});
151+
})}
152+
</>
153+
)}
154+
155+
{hasNextPage && (
156+
<ComboboxItem
157+
value="Load more"
158+
onSelect={() => fetchNextPage()}
159+
disabled={!hasNextPage || isFetchingNextPage}
160+
className="justify-center text-custom-blue-700"
161+
>
162+
{isFetchingNextPage ? "Loading more..." : "Load more"}
163+
</ComboboxItem>
164+
)}
165+
</ComboboxList>
166+
);
167+
}
168+
);

frontend/app/src/entities/schema/utils/is-hierarchical-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const isHierarchicalSchema = (
77
): schema is ModelSchema & { hierarchy: string } => {
88
return "hierarchy" in schema && !!schema.hierarchy;
99
};
10+
1011
export const getRootSchemaOfHierarchicalSchema = (schema: NodeSchema): NodeSchema => {
1112
const nodes = store.get(nodeSchemasAtom);
1213
const parentSchema = nodes.find(({ kind }) => kind === schema.parent);

frontend/app/src/shared/components/form/fields/relationship-many.field.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from "@/shared/components/form/type";
66
import { FormField, FormInput, FormMessage } from "@/shared/components/ui/form";
77

8-
import { Node } from "@/entities/nodes/getObjectItemDisplayValue";
8+
import { NodeCore } from "@/entities/nodes/types";
99
import { DEFAULT_FORM_FIELD_VALUE } from "@/shared/components/form/constants";
1010
import { updateRelationshipFieldValue } from "@/shared/components/form/utils/updateFormFieldValue";
1111
import { RelationshipManyInput } from "@/shared/components/inputs/relationship-many";
@@ -51,7 +51,7 @@ export default function RelationshipManyField({
5151
"has-[>:last-child:focus]:ring-red-500/25 has-[>:last-child:focus]:border-red-500"
5252
)}
5353
peer={props.relationship.peer}
54-
value={fieldData.value as Node[] | null}
54+
value={fieldData.value as NodeCore[] | null}
5555
onChange={(newValue) => {
5656
field.onChange(updateRelationshipFieldValue(newValue, defaultValue));
5757
}}

0 commit comments

Comments
 (0)