Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knovator/masters-admin",
"version": "3.1.0",
"version": "3.2.0",
"description": "Package for integrating Masters, Submasters functionality in React projects",
"main": "dist/index.js",
"module": "dist/masters-admin.esm.js",
Expand Down Expand Up @@ -113,6 +113,7 @@
},
"dependencies": {
"@types/react-beautiful-dnd": "^13.1.2",
"react-beautiful-dnd": "^13.1.0"
"react-beautiful-dnd": "^13.1.0",
"react-select": "^5.8.0"
}
}
51 changes: 50 additions & 1 deletion src/components/Common/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { forwardRef, MutableRefObject, useEffect } from "react"
import { Controller, useForm } from "react-hook-form"

import Multiselect from "../Input/Multiselect"
import { TRANSLATION_PAIRS_COMMON } from "../../../constants/common"
import { isEmpty } from "../../../utils/util"
import Input from "../Input"
import MergeInput from "../Input/MergeInput"

interface FormProps {
schema: SchemaType[]
Expand All @@ -13,6 +14,7 @@ interface FormProps {
indicatesRequired?: string
onSubmit: (data: any) => void
ref: MutableRefObject<HTMLFormElement | null>
updateData: object;
}

const Form = forwardRef<HTMLFormElement | null, FormProps>(
Expand All @@ -24,6 +26,7 @@ const Form = forwardRef<HTMLFormElement | null, FormProps>(
isUpdating = false,
languages,
indicatesRequired = TRANSLATION_PAIRS_COMMON.indicatesRequired,
updateData,
},
ref,
) => {
Expand Down Expand Up @@ -76,6 +79,52 @@ const Form = forwardRef<HTMLFormElement | null, FormProps>(
/>
)
break
case "multiselect":
input = (
<Controller
control={control}
name={schema.accessor}
rules={schema.validations}
render={({ field }) => (
// @ts-ignore
<Multiselect
label={schema.label}
value={field.value}
onChange={field.onChange}
error={errors[schema.accessor]?.message}
className="kms_w-full"
disabled={isUpdating && typeof schema.editable !== "undefined" && !schema.editable}
isRequired={schema.isRequired}
/>
)}
/>
);
break;
case "ReactSelect":
input = (
<Controller
control={control}
name={schema.accessor}
rules={schema.validations}
render={({ field }) => (
// @ts-ignore
<MergeInput
data={updateData}
label={schema.label}
value={field.value}
onChange={field.onChange}
error={errors[schema.accessor]?.message}
className="kms_w-full"
disabled={
isUpdating && typeof schema.editable !== "undefined" && !schema.editable
}
isRequired={schema.isRequired}

/>
)}
/>
)
break
case "select":
input = (
<Input.Select
Expand Down
102 changes: 102 additions & 0 deletions src/components/Common/Input/MergeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useCallback, useRef, useState } from "react"
import ReactSelect from "react-select/async"
import { useSubMasterState } from "../../../context/SubMasterContext"
import { MultiValue, SingleValue } from "react-select"

type OptionType = { label: string; value: string }

interface ReactSelectProps {
onChange?: (newValue: MultiValue<OptionType> | SingleValue<OptionType>, actionMeta: any) => void
label?: string
error?: string
className?: string
disabled?: boolean
value?: string
id?: string
isMulti?: boolean
isRequired?: boolean
selectedOptions?: MultiValue<OptionType> | SingleValue<OptionType>
isSearchable?: boolean
placeholder?: string
data: any
}

const MergeInput = ({
onChange,
label,
error,
isMulti = true,
selectedOptions: initialSelectedOptions = [],
isRequired,
isSearchable = true,
placeholder,
data,
}: ReactSelectProps) => {
const callerRef = useRef<NodeJS.Timeout | null>(null)
const [selectedOptions, setSelectedOptions] = useState<MultiValue<OptionType> | SingleValue<OptionType>>(
initialSelectedOptions,
)

const { getSubMastersList } = useSubMasterState()
const loadSubMasterOptions = useCallback(
(inputValue: string, callback: (options: OptionType[]) => void) => {
if (callerRef.current) clearTimeout(callerRef.current)

callerRef.current = setTimeout(async () => {
try {
getSubMastersList({ search: inputValue, all: false, exclude: data?._id }, (subMasters) => {
const options =
subMasters?.map((item: any) => ({
label: item.names.en,
value: item._id,
})) || []
callback(options)
})
} catch (error) {
console.error("Error loading submaster options:", error)
}
}, 300)
},
[getSubMastersList],
)

return (
<div className="kms_input-wrapper">
{label && <label className="kms_input-label">{label}</label>}
<ReactSelect
data-testid={`input-select-${label}`}
value={selectedOptions}
onChange={(newValue, actionMeta) => {
setSelectedOptions(newValue)
if (onChange) {
onChange(newValue, actionMeta)
}
}}
isMulti={isMulti}
defaultOptions
isSearchable={isSearchable}
loadOptions={loadSubMasterOptions}
placeholder={placeholder}
isDisabled={false}
styles={{
control: (provided) => ({
...provided,
border: "1px solid #94A3B8",
paddingTop: "0.1rem",
paddingBottom: "0.1rem",
borderRadius: "0.5rem",
outline: "4px solid transparent",
}),
placeholder: (provided) => ({
...provided,
color: "#9CA3AF",
marginLeft: "0px",
}),
}}
/>
{error && <p className="khb_input-error">{error}</p>}
</div>
)
}

export default MergeInput
87 changes: 87 additions & 0 deletions src/components/Common/Input/Multiselect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { KeyboardEventHandler } from "react"
import CreatableSelect from "react-select/creatable"

interface Option {
label: string
value: string
}

interface MultiSelectProps {
value: string[]
onChange: (newValue: string[]) => void
rest?: React.HTMLAttributes<HTMLDivElement>
label?: string
error?: string
className?: string
isRequired?: boolean
disabled?: boolean
}

const components = {
DropdownIndicator: null,
}

const createOption = (label: string): Option => ({
label,
value: label,
})

const Multiselect: React.FC<MultiSelectProps> = ({ onChange, label, error, isRequired, disabled, value }) => {
const [inputValue, setInputValue] = React.useState<string | undefined>("")
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!inputValue) return
switch (event.key) {
case "Enter":
case "Tab":
if (value === undefined) {
onChange([inputValue])
setInputValue("")
break
}
const updatedValue = [...value, inputValue]
onChange(updatedValue)
setInputValue("")
event.preventDefault()
break
}
}

return (
<div className="kms_input-wrapper">
{label && <label className="kms_input-label">{label}</label>}
<CreatableSelect
isMulti
isClearable
components={components}
inputValue={inputValue}
menuIsOpen={false}
onKeyDown={handleKeyDown}
onInputChange={setInputValue}
onChange={(newData) => {
onChange(newData.map((option) => option.value))
}}
placeholder="Enter Synonyms"
value={value?.map(createOption)}
isDisabled={disabled}
styles={{
control: (provided) => ({
...provided,
border: "1px solid #94A3B8",
paddingTop: "0.1rem",
paddingBottom: "0.1rem",
borderRadius: "0.5rem",
outline: "4px solid transparent",
}),
placeholder: (provided) => ({
...provided,
color: "#9CA3AF",
marginLeft: "0px",
}),
}}
/>
{error && <p className="kms_input-error">{error}</p>}
</div>
)
}

export default Multiselect
15 changes: 15 additions & 0 deletions src/components/SubMaster/SubMasterForm/SubMasterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ const SubMasterForm = forwardRef<HTMLFormElement | null, FormContainerProps>(({
required: commonTranslations.codeRequired,
},
},
{
label: "Synonym",
accessor: "synonym",
type: "multiselect",
isRequired: true,
placeholder: "Enter Synonyms",
},
{
label: "Merge Submasters",
accessor: "mergeSubmasters",
type: "ReactSelect",
isRequired: true,
placeholder: "Select submasters to merge",
},
{
label: commonTranslations.webDisplay,
accessor: "webDsply",
Expand Down Expand Up @@ -127,6 +141,7 @@ const SubMasterForm = forwardRef<HTMLFormElement | null, FormContainerProps>(({
languages={languages}
data={updateData}
isUpdating={formState === "UPDATE"}
updateData={updateData}
/>
)
})
Expand Down
2 changes: 2 additions & 0 deletions src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const TRANSLATION_PAIRS_COMMON = {
name: "Name",
namePlaceholder: "Enter name",
nameRequired: "Name is required",
synonyms: "Synonyms",
synonymsPlaceholder: "Enter Synonyms",

webDisplay: "Web Display", // Form, Web Display Field
enterWebDisplay: "Enter Web Display", // Web Display Placeholder
Expand Down
10 changes: 6 additions & 4 deletions src/hook/useSubMaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const useSubMaster = ({ defaultLimit, routes, defaultSort = ["seq", 1], preConfi
onError(code, "error", data?.message)
}
const getSubMastersList = useCallback(
async (search?: string, all: boolean = false) => {
async ({ search, all = false, exclude }: { search?: string, all?: boolean, exclude?: string } = {}, callback?: (data: any) => void) => {
try {
let sortConfig = sortConfigRef.current
setLoading(true)
Expand All @@ -61,6 +61,7 @@ const useSubMaster = ({ defaultLimit, routes, defaultSort = ["seq", 1], preConfi
onError: handleError(CALLBACK_CODES.GET_ALL),
data: {
search,
exclude,
query: {
parentCode: selectedMaster?.code,
},
Expand All @@ -81,7 +82,8 @@ const useSubMaster = ({ defaultLimit, routes, defaultSort = ["seq", 1], preConfi
setLoading(false)
setTotalPages(paginationGetter(response).totalPages)
setTotalRecords(paginationGetter(response).totalDocs)
return setList(dataGetter(response))
if(typeof callback === 'function') return callback(dataGetter(response));
else return setList(dataGetter(response))
}
setLoading(false)
} catch (error) {
Expand Down Expand Up @@ -330,7 +332,7 @@ const useSubMaster = ({ defaultLimit, routes, defaultSort = ["seq", 1], preConfi
}
setSequencing(status)
sortConfigRef.current = ["seq", 1]
getSubMastersList("", status)
getSubMastersList({ all: status, search: ''})
}
const onChangePageSize = (size: number): void => {
limitRef.current = size
Expand All @@ -341,7 +343,7 @@ const useSubMaster = ({ defaultLimit, routes, defaultSort = ["seq", 1], preConfi
const onChangeCurrentPage = (page: number): void => {
currentPageRef.current = page
setSequencing(false)
getSubMastersList(searchRef.current)
getSubMastersList({search: searchRef.current})
}

useEffect(() => {
Expand Down
6 changes: 3 additions & 3 deletions types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ interface SubMasterContextType {
loader?: JSX.Element
canDelete?: boolean
canPartialUpdate?: boolean
getSubMastersList: (search?: string) => Promise<void>
getSubMastersList: (options: {search?: string, all: boolean , exclude?: []} = {}, callback?: (data: any) => void) => Promise<void>
onChangeSequence: (sourceIndex: number, destinationIndex: number) => Promise<void>
sequencing: boolean
setSequencing: (status: boolean) => void
Expand Down Expand Up @@ -355,8 +355,8 @@ interface SchemaType {
validations?: import("react-hook-form").RegisterOptions
editable?: boolean
onInput?: (e: React.ChangeEvent<HTMLInputElement>) => void
type?: "text" | "number" | "select" | "checkbox" | "textarea"
options?: { value: string; label: string }[]
type?: "text" | "number" | "select" | "checkbox" | "textarea" | "multiselect" | "ReactSelect"
options?: { value: string; label: string }[] | strings
defaultValue?: string | number | boolean
placeholder?: string
}
Expand Down
Loading