Skip to content

Commit 9195636

Browse files
authored
Merge pull request #542 from code0-tech/feat/#541
Input suggestion multi-select and multi-focus
2 parents 745736a + 3138e1f commit 9195636

23 files changed

+1950
-226
lines changed

src/components/d-flow/input/DFlowInputDataType.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ const RuleHeader: React.FC<{
540540
const nextValue = suggestion.value
541541
if (!nextValue) return
542542
onDataTypeChange(nextValue)
543-
const label = suggestion?.ref?.displayText?.join(" ") ?? dataTypeLabel ?? ""
543+
const label = suggestion?.valueData?.displayText?.join(" ") ?? dataTypeLabel ?? ""
544544
setDataTypeValue(label)
545545
}, [dataTypeLabel, isBlocked, onDataTypeChange])
546546

src/components/d-flow/suggestion/DFlowSuggestionMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const DFlowSuggestionMenu: React.FC<DFlowSuggestionMenuProps> = (props) =
6565
ref={menuRef}
6666
suggestions={toInputSuggestions(stateSuggestions)}
6767
onSuggestionSelect={(suggestion) => {
68-
onSuggestionSelect(suggestion.ref as DFlowSuggestion)
68+
onSuggestionSelect(suggestion.valueData as DFlowSuggestion)
6969
}}
7070
/>
7171
<MenuSeparator/>

src/components/d-flow/suggestion/DFlowSuggestionMenu.util.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ export const toInputSuggestions = (suggestions: DFlowSuggestion[]): InputSuggest
5050

5151
return {
5252
children,
53-
ref: suggestion,
53+
valueData: suggestion,
5454
value: suggestion.value,
55-
groupLabel,
55+
groupBy: groupLabel,
5656
};
5757
})
5858
}

src/components/d-flow/suggestion/DFlowSuggestionSearchInput.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {InputProps, Input, setElementKey} from "../../form/Input";
1+
import {InputProps, Input} from "../../form";
22
import React, {RefObject} from "react";
33
import {Button} from "../../button/Button";
44
import {IconX} from "@tabler/icons-react";
55
import "./DFlowSuggestionSearchInput.style.scss"
6+
import {setElementKey} from "../../form/Input.utils";
67

78
interface DFlowSuggestionSearchInputProps extends Omit<InputProps<string | null>, "wrapperComponent" | "type"> {
89
//defaults to false

src/components/d-flow/tab/DFlowTabDefault.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ReferenceValue,
1919
Scalars
2020
} from "@code0-tech/sagittarius-graphql-types";
21+
import {InputSyntaxSegment} from "../../form/Input.syntax.hook";
2122

2223
export interface DFlowTabDefaultProps {
2324
functionInstance: NodeFunctionView
@@ -93,30 +94,47 @@ export const DFlowTabDefault: React.FC<DFlowTabDefaultProps> = (props) => {
9394
description={description}
9495
clearable
9596
key={JSON.stringify(parameter.value)}
96-
transformValue={value => {
97+
transformSyntax={(value): InputSyntaxSegment[] => {
98+
const rawValue = value ?? ""
99+
const textValue = typeof rawValue === "string" ? rawValue : String(rawValue)
100+
101+
const buildTextSegment = (text: string): InputSyntaxSegment[] => [{
102+
type: "text",
103+
start: 0,
104+
end: text.length,
105+
visualLength: text.length,
106+
content: text,
107+
}]
108+
109+
const buildBlockSegment = (node: React.ReactNode): InputSyntaxSegment[] => [{
110+
type: "block",
111+
start: 0,
112+
end: textValue.length,
113+
visualLength: 1,
114+
content: node,
115+
}]
116+
97117
try {
98-
if (!value) return value
99-
if ((JSON.parse(value) as NodeParameterValue).__typename === "NodeFunction") {
100-
const def = functionService.getById((JSON.parse(value) as NodeFunction).functionDefinition?.id!!)
101-
return <Badge
102-
color={"info"}>{def?.names?.nodes!![0]?.content}</Badge>
118+
119+
const parsed = JSON.parse(textValue) as NodeParameterValue
120+
if (parsed?.__typename === "NodeFunction") {
121+
const def = functionService.getById((parsed as NodeFunction).functionDefinition?.id!!)
122+
return buildBlockSegment(
123+
<Badge color={"info"}>{def?.names?.nodes!![0]?.content}</Badge>
124+
)
103125
}
104-
if ((JSON.parse(value) as NodeParameterValue).__typename === "ReferenceValue") {
105-
const refObject = JSON.parse(value) as ReferenceValue
106-
return <Badge
107-
color={"warning"}>{refObject.depth}-{refObject.scope}-{refObject.node}-{JSON.stringify(refObject.dataTypeIdentifier)}</Badge>
126+
127+
if (parsed?.__typename === "ReferenceValue") {
128+
const refObject = parsed as ReferenceValue
129+
return buildBlockSegment(
130+
<Badge color={"warning"}>{refObject.depth}-{refObject.scope}-{refObject.node}-{JSON.stringify(refObject.dataTypeIdentifier)}</Badge>
131+
)
108132
}
109133
} catch (e) {
134+
// fall through to text rendering
110135
}
111-
return value
112-
}}
113-
disableOnValue={value => {
114-
if (!value) return false
115-
try {
116-
return (value as NodeParameterValue).__typename === "NodeFunction" || (value as NodeParameterValue).__typename === "ReferenceValue"
117-
} catch (e) {
118-
}
119-
return false
136+
137+
return buildTextSegment(textValue)
120138
}}
121139
defaultValue={defaultValue}
122140
onSuggestionSelect={(suggestion) => {

src/components/d-user/DUser.service.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import {ReactiveArrayService, ReactiveArrayStore} from "../../utils/reactiveArrayService";
1+
import {ReactiveArrayService} from "../../utils/reactiveArrayService";
22
import {
3-
Maybe,
43
User,
54
UsersEmailVerificationInput,
65
UsersEmailVerificationPayload,
7-
UserSession,
86
UsersIdentityLinkInput,
97
UsersIdentityLinkPayload,
108
UsersIdentityLoginInput,
@@ -24,11 +22,11 @@ import {
2422
UsersMfaTotpValidateSecretInput,
2523
UsersMfaTotpValidateSecretPayload,
2624
UsersPasswordResetInput,
27-
UsersPasswordResetPayload, UsersPasswordResetRequestInput,
25+
UsersPasswordResetPayload,
26+
UsersPasswordResetRequestInput,
2827
UsersPasswordResetRequestPayload,
2928
UsersRegisterInput,
30-
UsersRegisterPayload,
31-
UsersUpdateInput
29+
UsersRegisterPayload
3230
} from "@code0-tech/sagittarius-graphql-types";
3331
import {DUserView} from "./DUser.view";
3432

@@ -41,6 +39,10 @@ export abstract class DUserReactiveService extends ReactiveArrayService<DUserVie
4139
return this.values().find(user => user.id === id);
4240
}
4341

42+
getByUsername(username: User['username']): DUserView | undefined {
43+
return this.values().find(user => user.username === username);
44+
}
45+
4446
abstract usersEmailVerification(payload: UsersEmailVerificationInput): Promise<UsersEmailVerificationPayload | undefined>;
4547

4648
abstract usersIdentityLink(payload: UsersIdentityLinkInput): Promise<UsersIdentityLinkPayload | undefined>;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {Meta} from "@storybook/react-vite";
2+
import {DUserReactiveService} from "./DUser.service";
3+
import {
4+
UsersEmailVerificationInput,
5+
UsersEmailVerificationPayload,
6+
UsersIdentityLinkInput,
7+
UsersIdentityLinkPayload,
8+
UsersIdentityLoginInput,
9+
UsersIdentityLoginPayload,
10+
UsersIdentityRegisterInput,
11+
UsersIdentityRegisterPayload,
12+
UsersIdentityUnlinkInput,
13+
UsersIdentityUnlinkPayload,
14+
UsersLoginInput,
15+
UsersLoginPayload,
16+
UsersLogoutInput,
17+
UsersLogoutPayload,
18+
UsersMfaBackupCodesRotateInput,
19+
UsersMfaBackupCodesRotatePayload,
20+
UsersMfaTotpGenerateSecretInput,
21+
UsersMfaTotpGenerateSecretPayload,
22+
UsersMfaTotpValidateSecretInput,
23+
UsersMfaTotpValidateSecretPayload,
24+
UsersPasswordResetInput,
25+
UsersPasswordResetPayload,
26+
UsersPasswordResetRequestInput,
27+
UsersPasswordResetRequestPayload,
28+
UsersRegisterInput,
29+
UsersRegisterPayload
30+
} from "@code0-tech/sagittarius-graphql-types";
31+
import {ContextStoreProvider, useReactiveArrayService} from "../../utils";
32+
import {DUserView} from "./DUser.view";
33+
import React from "react";
34+
import {DUserInput} from "./DUserInput";
35+
import {Text} from "../text/Text";
36+
import {Badge} from "../badge/Badge";
37+
import {IconArrowDown, IconArrowUp, IconCornerDownLeft} from "@tabler/icons-react";
38+
import {Spacing} from "../spacing/Spacing";
39+
import {Flex} from "../flex/Flex";
40+
41+
const meta: Meta = {
42+
title: "DUser",
43+
component: DUserInput
44+
}
45+
46+
export default meta
47+
48+
class DUserReactiveServiceExtended extends DUserReactiveService {
49+
usersEmailVerification(payload: UsersEmailVerificationInput): Promise<UsersEmailVerificationPayload | undefined> {
50+
return Promise.resolve(undefined);
51+
}
52+
53+
usersIdentityLink(payload: UsersIdentityLinkInput): Promise<UsersIdentityLinkPayload | undefined> {
54+
return Promise.resolve(undefined);
55+
}
56+
57+
usersIdentityLogin(payload: UsersIdentityLoginInput): Promise<UsersIdentityLoginPayload | undefined> {
58+
return Promise.resolve(undefined);
59+
}
60+
61+
usersIdentityRegister(payload: UsersIdentityRegisterInput): Promise<UsersIdentityRegisterPayload | undefined> {
62+
return Promise.resolve(undefined);
63+
}
64+
65+
usersIdentityUnlink(payload: UsersIdentityUnlinkInput): Promise<UsersIdentityUnlinkPayload | undefined> {
66+
return Promise.resolve(undefined);
67+
}
68+
69+
usersLogin(payload: UsersLoginInput): Promise<UsersLoginPayload | undefined> {
70+
return Promise.resolve(undefined);
71+
}
72+
73+
usersLogout(payload: UsersLogoutInput): Promise<UsersLogoutPayload | undefined> {
74+
return Promise.resolve(undefined);
75+
}
76+
77+
usersMfaBackupCodesRotate(payload: UsersMfaBackupCodesRotateInput): Promise<UsersMfaBackupCodesRotatePayload | undefined> {
78+
return Promise.resolve(undefined);
79+
}
80+
81+
usersMfaTotpGenerateSecret(payload: UsersMfaTotpGenerateSecretInput): Promise<UsersMfaTotpGenerateSecretPayload | undefined> {
82+
return Promise.resolve(undefined);
83+
}
84+
85+
usersMfaTotpValidateSecret(payload: UsersMfaTotpValidateSecretInput): Promise<UsersMfaTotpValidateSecretPayload | undefined> {
86+
return Promise.resolve(undefined);
87+
}
88+
89+
usersPasswordReset(payload: UsersPasswordResetInput): Promise<UsersPasswordResetPayload | undefined> {
90+
return Promise.resolve(undefined);
91+
}
92+
93+
usersPasswordResetRequest(payload: UsersPasswordResetRequestInput): Promise<UsersPasswordResetRequestPayload | undefined> {
94+
return Promise.resolve(undefined);
95+
}
96+
97+
usersRegister(payload: UsersRegisterInput): Promise<UsersRegisterPayload | undefined> {
98+
return Promise.resolve(undefined);
99+
}
100+
}
101+
102+
103+
export const Input = () => {
104+
105+
const user = useReactiveArrayService<DUserView, DUserReactiveServiceExtended>(DUserReactiveServiceExtended, [
106+
new DUserView({
107+
id: "gid://sagittarius/User/1",
108+
username: "exampleuser",
109+
email: "test@gmail.com",
110+
admin: undefined,
111+
avatarPath: "",
112+
firstname: undefined,
113+
lastname: undefined,
114+
namespace: undefined,
115+
namespaceMemberships: undefined,
116+
createdAt: new Date().toString(),
117+
updatedAt: new Date().toString(),
118+
}),
119+
new DUserView({
120+
id: "gid://sagittarius/User/1",
121+
username: "nsammito",
122+
email: "test@gmail.com",
123+
admin: undefined,
124+
avatarPath: "",
125+
firstname: undefined,
126+
lastname: undefined,
127+
namespace: undefined,
128+
namespaceMemberships: undefined,
129+
createdAt: new Date().toString(),
130+
updatedAt: new Date().toString(),
131+
})
132+
])
133+
134+
return <ContextStoreProvider services={[user]}>
135+
<DUserInput title={"Users to invite"} description={"Invite users to your workspace or organization"}/>
136+
</ContextStoreProvider>
137+
138+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from "react";
2+
import {InputSuggestion, TextInput, TextInputProps} from "../form";
3+
import {InputSyntaxSegment} from "../form/Input.syntax.hook";
4+
import {Badge} from "../badge/Badge";
5+
import {Text} from "../text/Text";
6+
import {useService, useStore} from "../../utils";
7+
import {DUserReactiveService} from "./DUser.service";
8+
import {MenuItem, MenuLabel} from "../menu/Menu";
9+
import {Flex} from "../flex/Flex";
10+
import {IconArrowDown, IconArrowUp, IconCornerDownLeft} from "@tabler/icons-react";
11+
import {Spacing} from "../spacing/Spacing";
12+
13+
export interface DUserInputProps extends TextInputProps {
14+
15+
}
16+
17+
export const DUserInput: React.FC<DUserInputProps> = (props) => {
18+
19+
const {...rest} = props
20+
21+
const userService = useService(DUserReactiveService)
22+
const userStore = useStore(DUserReactiveService)
23+
const suggestions: InputSuggestion[] = React.useMemo(() => {
24+
return userService.values().map(user => ({
25+
value: user.username || "",
26+
children: user.username,
27+
insertMode: "insert",
28+
valueData: user,
29+
groupBy: "Users"
30+
}))
31+
}, [userStore])
32+
33+
const transformSyntax = (
34+
_?: string | null,
35+
appliedParts: (InputSuggestion | any)[] = [],
36+
): InputSyntaxSegment[] => {
37+
38+
let cursor = 0
39+
40+
return appliedParts.map((part: string | InputSuggestion, index) => {
41+
if (typeof part === "object") {
42+
const segment = {
43+
type: "block",
44+
start: cursor,
45+
end: cursor + part.value.length,
46+
visualLength: 1,
47+
content: <Badge color={"info"} border>
48+
<Text style={{color: "inherit"}}>
49+
@{part.value}
50+
</Text>
51+
</Badge>,
52+
}
53+
cursor += part.value.length
54+
return segment
55+
}
56+
const textString = part ?? ""
57+
if (!textString.length) return
58+
59+
if (index == appliedParts.length - 1) {
60+
const segment = {
61+
type: "text",
62+
start: cursor,
63+
end: cursor + textString.length,
64+
visualLength: textString.length,
65+
content: textString,
66+
}
67+
cursor += textString.length
68+
return segment
69+
}
70+
cursor += textString.length
71+
return {}
72+
}) as InputSyntaxSegment[]
73+
}
74+
75+
return <TextInput placeholder={"Enter users"}
76+
suggestionsEmptyState={<MenuItem><Text>No user found</Text></MenuItem>}
77+
onLastTokenChange={token => {
78+
userService.getByUsername(token)
79+
}}
80+
suggestionsFooter={<MenuLabel>
81+
<Flex style={{gap: ".35rem"}}>
82+
<Flex align={"center"} style={{gap: "0.35rem"}}>
83+
<Flex>
84+
<Badge border><IconArrowUp size={12}/></Badge>
85+
<Badge border><IconArrowDown size={12}/></Badge>
86+
</Flex>
87+
move
88+
</Flex>
89+
<Spacing spacing={"xxs"}/>
90+
<Flex align={"center"} style={{gap: ".35rem"}}>
91+
<Badge border><IconCornerDownLeft size={12}/></Badge>
92+
insert
93+
</Flex>
94+
</Flex>
95+
</MenuLabel>}
96+
filterSuggestionsByLastToken
97+
enforceUniqueSuggestions
98+
transformSyntax={transformSyntax} {...rest}
99+
suggestions={suggestions}/>
100+
}

0 commit comments

Comments
 (0)