neeto-fields-nano
neeto-fields-nano enables the management of dynamically added fields (often
referred as custom fields) for the resources across neeto products.
Index
General information
-
This nano deals with fields and field values.
-
Field has a name
, kind
, resource_type
and owner
.
-
Field value has a field
and resource
item associated to it.
-
resource_type
is the type of resource for which we are creating the field.
It is used for the categorized fetching and rendering of the fields. eg:
users, tickets, deals, etc
-
owner
is basically scope of a field. It is whom to the field belongs. eg:
Organization, Project, etc
-
kind
is the nature of the data meant as value for the field. All available
kinds
are:
text
, number
, monetary
, single_option
, multi_option
, date
, time
,
date_range
, time_range
, textarea
, person
, checkbox
, regex
,
integer
, decimal
, datetime
.
Installation instructions
Engine installation
-
Add this line to your application's Gemfile:
source "NEETO_GEM_SERVER_URL" do
gem 'neeto-fields-engine'
end
-
And then execute:
bundle install
-
Add this line to your application's config/routes.rb
file
mount NeetoFieldsEngine::Engine => "/neeto_fields_engine"
-
Add required migrations in the db/migrate
folder. Run the following
commands to generate the migrations.
rails g neeto_fields_engine:install
This will generate the migration to create the neeto_fields_engine_fields
table (which holds the fields) and neeto_fields_engine_field_values
table
(which holds the value for the fields).
- Run the following command to create the tables.
rails db:migrate
- Generate the required associations between models using the following
command:
rails g neeto_fields_engine:associations
This will prompt the user to enter 2 things:
- Name of Owner model.
- Names of Resource models.
-
Owner: It is the ultimate owner of the fields being added. It can be
Organization, Project, etc depending on the business logic. The field will
belongs_to
Owner.
-
Resource: These are the models, to which we want the dynamic fields to be
attached with. This case be anything like Ticket, Deal, etc depending on the
business logic. The field_value belongs_to
each Resource.
Once you enter the appropriate entries for the prompts asked. It will create an
initializer file, which is required for the functioning of the engine.
It will also add necessary associations to respective models, which were
considered as Owner and Resources.
Note: You might need to re-arrange the associations statements which
are normally inserted to top most lines of the file, to satisfy the Rails
standards.
Once you have completed these steps, you are ready to use the
neeto-fields-engine.
Frontend package installation
Install the latest neetoFields nano
package using the below command:
yarn add @bigbinary/neeto-fields-frontend
Dependency
neeto-fields-nano
has a peer dependency which is required to use the nano
properly. Install the peer dependency using the below command:
yarn add uuid
Frontend package exports
The frontend package exports 4 components, 2 utility functions and a hook.
Components
1. FieldsDashboard
The FieldsDashboard component serves as a dashboard for managing all
custom-field related operations. It functions without requiring any props by
default, but you can customize its behavior by passing optional props.
Props:
-
rowData
: Represents the rowData for the table within the dashboard.
-
buildColumnData
: A function that builds the column data (in neetoUI table
format) for the dashboard table. This function gets the onDeleteClick
,
onEditClick
callbacks and defaultColumns
as arguments.
-
showOwnersInMenu
: Accepts a boolean value. When set to true
, the fields
are displayed in categorized form (side menu) based on owners. By default,
fields are categorized based on the resourceType.
-
resourceType
: Explicitly specifies the resource_type
of the fields to be
shown in the dashboard table. This prop is expected when showOwnersInMenu
is set to true
.
-
allowedKinds
:Specifies the list of field kinds allowed to be created.
-
paneProps
: Props to be passed to the Add/Edit pane. It accepts the
following.
children
: The children components to be rendered inside the pane.validations
: The validations for the formik input fields in the
children
. validations
must be provided as an object with the field name
as key and the corresponding yup validation schema as the value.initialValues
: The initial values for the formik input fields in the
children
. It is provided as an object with the field name as key and the
corresponding value.hideRequiredSwitch
: A boolean value when set to true
hides the toggle
switch for isRequried
.onMutationSuccess
: Callback function which is triggered on the success of
mutation functions (create, update & delete).
-
showStateFilter
: Boolean value which specifies whether to show or hide
state filters.
-
fieldStatesTaxonomy
: Specifies the names to be rendered for active
and
inactive
states.
-
breadcrumbs
: Specifies the breadcrumbs to be displayed on the dashboard
page.
-
helpDocUrl
: Specify the URL to the help documentation about fields. This
URL will be shown in the NoData
screen.
-
nameAliases
: This property accepts alias names as key-value pairs to be
displayed for the names of resource types in header and menubar.
-
headerTitle
: Specify the header title explicitly. Default is 'fields'
-
resources
: For owner-based field categorization, provide an array of
objects with id
and name
properties for each owner. For resource
type-based categorization, use an array of strings representing resource
types.
If no resources are provided, for resource type-based categorization, the
menu bar will fetch all resource types. For owner-based categorization, it
will fetch and list all owners in the organization.
Usage:
When Organization is the owner of the fields.
import { FieldsDashboard } from "@bigbinary/neeto-fields-frontend";
<FieldsDashboard
allowedKinds={["text", "number"]}
buildColumnData={({ defaultColumns }) => [
...defaultColumns,
{
dataIndex: "isSystem",
index: "isSystem",
title: t("titles.systemField"),
render: boolVal => (boolVal ? "Yes" : "No"),
},
]}
fieldStatesTaxonomy={{ active: "Active", inactive: "Deactivated" }}
paneProps={{
children: <HostSpecificInputFields />,
validations: {
hostSpecificInputName: validationSchema,
},
initialValues: {
hostSpecificInputName: initialValue,
},
}}
breadcrumbs={[
{
link: "/",
text: "Home",
},
{
link: "/",
text: "Settings",
},
]}
/>;
When Organization is not owner of the fields. Lets say the owner is Project.
import { FieldsDashboard } from "@bigbinary/neeto-fields-frontend";
<FieldsDashboard
allowedKinds={["text", "number"]}
ownerId={projectId}
resourceType="tasks"
showOwnersInMenu
fieldStatesTaxonomy={{ active: "Active", inactive: "Deactivated" }}
paneProps={{
children: <HostSpecificInputFields />,
validations: {
hostSpecificInputName: validationSchema,
},
initialValues: {
hostSpecificInputName: initialValue,
},
}}
breadcrumbs={[
{
link: "/",
text: "Home",
},
{
link: "/",
text: "Settings",
},
]}
/>;
2. FieldsPane
This is the pane which handles the Add / Edit operations of the field.
Props:
isOpen
: Boolean state which specifies the open/close state of the pane.onClose
: The function to be executed on closing the pane.resourceType
: Specifies the resource_type of the field to be created via
the pane.allowedKinds
: Specifies the list of field kinds allowed to be created.children
: Children components for the pane.additionalValidations
: Validations for the formik fields in children
.initialValues
: Initial values for the formik fields in children
.selectedField
: The field object whose editing is concerned with. If this
prop is given, the pane will act as an Edit pane. Else it act as Add pane.hideRequiredSwitch
: Specify whether to hide the Is required
toggle switch
for field in pane.ownerId
: The ID of the owner in case the owner is not an organization.onMutationSuccess
: The callback function which is triggered on the success
of mutation functions( create, update & delete).
Usage:
import { FieldsPane } from "@biginary/neeto-fields-frontend";
const [fieldPaneOpen, setFieldPaneOpen] = useState(false);
<FieldsPane
isOpen={fieldPaneOpen}
allowedKinds={["text", "number"]}
resourceType="users"
additionalValidations={{
hostSpecificInputName: validationSchema,
}}
initialValues={{ hostSpecificInputName: initialValue }}
onClose={() => setFieldPaneOpen(false)}
>
<HostSpecificInputFields />
</FieldsPane>;
3. FieldValuesContainer
The FieldValuesContainer
component handles field values associated with a
specific resource.
Props:
resourceType
: The type of resource.fieldValues
: Field values associated with the resource obtained from the
response.fields
: Fields associated with resource type. This is an optional prop. If
not provided the component will fetch the fields internally.resourceId
: The ID of the resource.ownerId
:The ID of the owner. This prop is required only if the owner is not
an organization.customComponents
: If the host application has any extra kind
other than
the supported ones, you can specify the component to be displayed
corresponding to that kind using this prop. It takes the kind
name as the
key and the component rendering callback function as the value. The callback
function can expect the field
object as argument.className
: Class names for styling.showBorder
: Boolean value to specify whether to show or hide borders.
Default true.formRefs
: A React Ref object that can be used to access the Formik context
of the forms corresponding to each field as key value pairs.disabled
: Boolean value to specify whether to disable all fields.isRequiredColumnName
: The name of column which holds the value which
suggests if a field value is a required
one or not. Default is
isRequired
column.fieldClassName
: Class names for styling the div
containing the input
field.onMutationSuccess
: The callback function which is triggered on the success
of mutation functions( create, update & delete).blockNavigation
: Boolean value to specify whether to block the navigation
if we have unsaved field values entered in field inputs.blockNavigationAlertProps
: Props to be passed to the Alert. It accepts the
following.
title
message
submitButtonLabel
cancelButtonLabel
Usage:
Say the resource over here is a user.
import { FieldValuesContainer } from "@bigbinary/neeto-fields-frontend";
const formRefs = useRef({});
const queryClient = useQueryClient();
<FieldValuesContainer
blockNavigation
formRefs={formRefs}
fieldValues={user.fieldValues} // We expect the user response from host's backend to send associated field_values with it.
resourceId={user?.id}
resourceType="users"
onMutationSuccess={() => queryClient.invalidateQueries(["users"])}
customComponents={{
hostSpecificKindName: field => <HostSpecificInputFields />,
}}
blockNavigationProps={{
title: "Changes not saved!",
message: "You've not saved the changes made.",
}}
/>;
4. FieldInputs
The FieldInputs component render the input UI for the fetched fields. It accepts
the following props:
Props:
fields
: An array of all the fetched fields.customComponents
: If the host application has any extra kind other than the
supported ones, you can specify the component to be displayed corresponding
to that kind using this prop. It takes the kind name as the key and the
component rendering callback function as the value. The callback function can
expect the field
object.formRefs
: A React Ref object that can be used to access the Formik context
of the form.disabled
: Boolean value to specify whether to disable all fields.isRequiredColumnName
: The name of column which holds the value which
suggests if a field value is a required
one or not.
:memo: Note:
To initialize the values for this formik fields, you need to use
mergeInitialValues
function.
To submit the values from this formik form, you need to use
transformValues
function to capture the right
data from neeto-fields.
Usage:
import {
FieldInputs,
useFetchFields,
neetoFieldsUtils,
} from "@bigbinary/neeto-fields-frontend";
const formRef = useRef();
const HostForm = () => {
const {
data: { fields },
} = useFetchFields({
resourceType: "users",
});
const initialValues = neetoFieldsUtils.mergeInitialValues({
initialValues: INITIAL_VALUES,
fields,
});
return (
<Form
formikProps={{
initialValues,
validationSchema: VALIDATION_SCHEMA,
onSubmit: values =>
onSubmit(neetoFieldsUtils.transformValues({ values, fields })),
enableReinitialize: true,
}}
>
{/* Other host specific input fields */}
<FieldInputs formRef={formRef} fields={fields} />
<Button type="submit" label="Submit" />
</Form>
);
};
5. InlineFieldValueInput
The InlineFieldValueInput component render the field value input UI. It accepts
the following props:
Props:
fields
: An array of all the fetched fields.fieldValues
: Field values associated with the resource.resourceType
: The type of resource.resourceId
: The ID of the resource.ownerId
: The ID of the owner in case the owner is not an organization.onMutate
: The callback function which is triggered on the mutationsonMutationSuccess
: The callback function which is triggered on the success
of mutation functions of field value.
Usage:
Usually this is used as a component for inline editing of field values in
tables. The example shows one such usage of building the columnData
for table
with inline editable field value inputs in cell.
const buildColumnDataForFields = (fields, onFieldValueUpdateSuccess) =>
fields.map(field => ({
dataIndex: "field.id",
key: field.id,
title: field.name,
width: 250,
render: (_, user) => (
<InlineFieldValueInput
{...{ field }}
fieldValues={user.fieldValues}
resourceId={user.id}
resourceType="users"
onMutationSuccess={onFieldValueUpdateSuccess}
/>
),
}));
Functions
The package exports the neetoFieldsUtils
, which contains two utility
functions.
1. neetoFieldsUtils.mergeInitialValues
This function builds the initial values for the Formik form that wraps the
<FieldInputs />
component.
Arguments:
initialValues
: The initial value object without considering
<FieldInputs />
fields
: An array of all the fetched fields.
Usage:
import { useFetchFields } from "@bigbinary/neeto-fields-frontend";
const {
data: { fields },
} = useFetchFields({ resourceType: "users" });
const initialValues = neetoFieldsUtils.mergeInitialValues({
initialValues: FORMIK_INITIAL_VALUES,
fields,
});
2. neetoFieldsUtils.transformValues
This function transforms the Formik form values and builds the values
object
including the data from <FieldInputs />
. This transformed object can be passed
to the onSubmit
function of the Formik form.
Arguments:
values
: The Formik form values.
Usage:
import { useFetchFields } from "@bigbinary/neeto-fields-frontend";
const {
data: { fields },
} = useFetchFields({ resourceType: "users" });
<Form
formikProps={{
initialValues,
validationSchema: VALIDATION_SCHEMA,
onSubmit: values =>
onSubmit(neetoFieldsUtils.transformValues({ values, fields })),
enableReinitialize: true,
}}
>
{/* Form's children */}
</Form>;
Hooks
1. useFetchFields
This is a React Query hook for fetching all the fields.
Arguments:
resourceType
: The resource_type of the fields to be fetched.ownerId
: The ID of the owner in case the owner is not an organization.
Usage:
const {
data: { fields, count, activeFieldsCount, inactiveFieldsCount },
} = useFetchFields({
resourceType: "users",
ownerId: "ownerId",
});
2. useShowField
This is a React Query hook for fetching details of a field.
Argument
fieldId
: The ID of field for fetching details.ownerId
: The ID of the owner in case the owner is not an organization.
Usage
const {
data: { field },
} = useShowField({ fieldId, ownerId });
3. useCreateField
This is a React Query hook for creating a field.
Usage
const { mutate: create } = useCreateField();
const payload = {
field: { name, kind, resourceType, ownerId, displayOrder, data },
};
create(payload);
4. useUpdateField
This is a React Query hook for updating a field.
Usage
const { mutate: update } = useUpdateField();
const payload = {
field: { name, kind, resourceType, ownerId, displayOrder, data },
};
update({ fieldId, payload });
5. useDestroyField
This is a React Query hook for deleting a field.
Usage
const { mutate: delete } = useDestroyField();
delete({ fieldId })
Pass the ownerId
too with the payload in case the owner is not an
organization.
Customizability
Engine customizability
In order to customize the engine's default behavior, we have several ways.
- For improving the customizability, we've made every actions' duties into
separate services, which can be overridden from the host apps as and when
required.
- If you wish to perform anything before or after the core logic of an action,
we have provision to do it as a single transaction.
- Every action has
before_{action}_process
method invoked before the core
action logic and after_{action}_process
method invoked after the core action
logic. - These methods can be overridden by defining them in the concern
NeetoFieldsEngine::Fields::Customizable
or
NeetoFieldsEngine::FieldValues::Customizable
depending on the context.
Eg: If you want to do something before the update action of
FieldsController
, then define it in before_update_process
method inside
the NeetoFieldsEngine::Fields::Customizable
concern.
Development instructions
Engine development
-
Add this line to your application's Gemfile (replace the path to the local
copy of neeto-fields-engine):
gem 'neeto-fields-engine', path: '../neeto-fields-engine'
-
And then execute:
bundle install
-
Refer engine installation steps 3 and onwards.
Frontend package development
The usage of yalc
is explained in this video:
https://www.youtube.com/watch?v=QBiYGP0Rhe0
-
See the changes in the host app by executing the following command
Use this command if releasing a package for the first time.
yarn build && yalc publish
Use this command to see changes after the initial publish.
yarn release
Setup dummy host app
Setup
Instructions.
Visit http://spinkart.lvh.me:9100 and login with email oliver@example.com
and
password welcome
.
Additional instructions
- Run
yarn build
to bundle the app.