Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (WIP) poc action forms #1166

Closed
wants to merge 2 commits into from
Closed
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
102 changes: 100 additions & 2 deletions packages/_example/src/forest/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export default function makeAgent() {

return createAgent<Schema>(envOptions)
.addDataSource(createSqlDataSource({ dialect: 'sqlite', storage: './assets/db.sqlite' }))

.addDataSource(
createSqlDataSource(
'postgres://postgres.mdvbfrrifupdjukiqrtb:qI3iUbMd2O6ssYpY@aws-0-eu-west-2.pooler.supabase.com:5432/postgres',
),
)
.addDataSource(
// Using an URI
createSqlDataSource('mariadb://example:password@localhost:3808/example'),
Expand Down Expand Up @@ -84,5 +88,99 @@ export default function makeAgent() {
.customizeCollection('post', customizePost)
.customizeCollection('comment', customizeComment)
.customizeCollection('review', customizeReview)
.customizeCollection('sales', customizeSales);
.customizeCollection('sales', customizeSales)
.customizeCollection('reservations', collection => {
collection.overrideCreate(async context => {
const { data } = context;

const record = await context.collection.create(data);
console.log('create reservation', JSON.stringify(record));

return record;
});

collection.addAction('action with form', {
scope: 'Bulk',
execute: (context, resultBuilder) => {
resultBuilder.success('ok');
},
form: [
{
type: 'Layout',
component: 'Page',
nextButtonLabel: '==>',
previousButtonLabel: '<==',
elements: [
{ type: 'String', label: 'first_name' },
{
type: 'Layout',
component: 'Separator',
// "if_": lambda ctx: ctx.form_values.get("Gender", "") in ["other", ""],
},
{
type: 'Layout',
component: 'Row',
fields: [
{ type: 'Enum', label: 'Gender', enumValues: ['M', 'F', 'other'] },
{
type: 'String',
label: 'Gender_other',
},
],
},
],
},
{
type: 'Layout',
component: 'Page',
// "if_": lambda ctx: ctx.form_values.get("Number of children") != 0,
elements: [
{ type: 'Number', label: 'Number of children' },
{
type: 'Layout',
component: 'Row',
fields: [
{ type: 'Number', label: 'Age of older child' },
{ type: 'Number', label: 'Age of younger child' },
],
},
{ type: 'Boolean', label: 'Are they wise' },
],
nextButtonLabel: '==>',
previousButtonLabel: '<==',
},
{
type: 'Layout',
component: 'Page',
// "if_": lambda ctx: ctx.form_values.get("Are they wise") is False,
elements: [
{
type: 'Layout',
component: 'Row',
fields: [
{ type: 'StringList', label: 'Why_its_your_fault' },
{ type: 'String', label: 'Why_its_their_fault', widget: 'TextArea' },
],
},
],
nextButtonLabel: '==>',
previousButtonLabel: '<==',
},
],
});

collection.addAction('static action with form', {
scope: 'Bulk',
execute: (context, resultBuilder) => {
resultBuilder.success('ok');
},
form: [{ type: 'String', label: 'name' }],
});

// collection.overrideUpdate(async context => {
// const { filter, patch } = context;

// await context.collection.update(filter, patch);
// });
});
}
124 changes: 124 additions & 0 deletions packages/_example/src/forest/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,48 @@ export type DevXpMembersFilter = TPaginatedFilter<Schema, 'dev_xp_members'>;
export type DevXpMembersSortClause = TSortClause<Schema, 'dev_xp_members'>;
export type DevXpMembersAggregation = TAggregation<Schema, 'dev_xp_members'>;

export type BookingsCustomizer = CollectionCustomizer<Schema, 'bookings'>;
export type BookingsRecord = TPartialRow<Schema, 'bookings'>;
export type BookingsConditionTree = TConditionTree<Schema, 'bookings'>;
export type BookingsFilter = TPaginatedFilter<Schema, 'bookings'>;
export type BookingsSortClause = TSortClause<Schema, 'bookings'>;
export type BookingsAggregation = TAggregation<Schema, 'bookings'>;

export type CancellationCustomizer = CollectionCustomizer<Schema, 'cancellation'>;
export type CancellationRecord = TPartialRow<Schema, 'cancellation'>;
export type CancellationConditionTree = TConditionTree<Schema, 'cancellation'>;
export type CancellationFilter = TPaginatedFilter<Schema, 'cancellation'>;
export type CancellationSortClause = TSortClause<Schema, 'cancellation'>;
export type CancellationAggregation = TAggregation<Schema, 'cancellation'>;

export type DeparturesCustomizer = CollectionCustomizer<Schema, 'departures'>;
export type DeparturesRecord = TPartialRow<Schema, 'departures'>;
export type DeparturesConditionTree = TConditionTree<Schema, 'departures'>;
export type DeparturesFilter = TPaginatedFilter<Schema, 'departures'>;
export type DeparturesSortClause = TSortClause<Schema, 'departures'>;
export type DeparturesAggregation = TAggregation<Schema, 'departures'>;

export type ReservationsCustomizer = CollectionCustomizer<Schema, 'reservations'>;
export type ReservationsRecord = TPartialRow<Schema, 'reservations'>;
export type ReservationsConditionTree = TConditionTree<Schema, 'reservations'>;
export type ReservationsFilter = TPaginatedFilter<Schema, 'reservations'>;
export type ReservationsSortClause = TSortClause<Schema, 'reservations'>;
export type ReservationsAggregation = TAggregation<Schema, 'reservations'>;

export type TablethingCustomizer = CollectionCustomizer<Schema, 'tablething'>;
export type TablethingRecord = TPartialRow<Schema, 'tablething'>;
export type TablethingConditionTree = TConditionTree<Schema, 'tablething'>;
export type TablethingFilter = TPaginatedFilter<Schema, 'tablething'>;
export type TablethingSortClause = TSortClause<Schema, 'tablething'>;
export type TablethingAggregation = TAggregation<Schema, 'tablething'>;

export type TabletrucCustomizer = CollectionCustomizer<Schema, 'tabletruc'>;
export type TabletrucRecord = TPartialRow<Schema, 'tabletruc'>;
export type TabletrucConditionTree = TConditionTree<Schema, 'tabletruc'>;
export type TabletrucFilter = TPaginatedFilter<Schema, 'tabletruc'>;
export type TabletrucSortClause = TSortClause<Schema, 'tabletruc'>;
export type TabletrucAggregation = TAggregation<Schema, 'tabletruc'>;

export type CustomerCustomizer = CollectionCustomizer<Schema, 'customer'>;
export type CustomerRecord = TPartialRow<Schema, 'customer'>;
export type CustomerConditionTree = TConditionTree<Schema, 'customer'>;
Expand Down Expand Up @@ -275,6 +317,29 @@ export type Schema = {
nested: {};
flat: {};
};
'bookings': {
plain: {
'cancellation_id': number | null;
'created_at': string;
'id': number;
'name': string | null;
};
nested: {
'cancellation': Schema['cancellation']['plain'] & Schema['cancellation']['nested'];
};
flat: {
'cancellation:created_at': string;
'cancellation:id': number;
};
};
'cancellation': {
plain: {
'created_at': string;
'id': number;
};
nested: {};
flat: {};
};
'card': {
plain: {
'card_number': number | null;
Expand Down Expand Up @@ -329,6 +394,15 @@ export type Schema = {
nested: {};
flat: {};
};
'departures': {
plain: {
'created_at': string;
'id': number;
'name': string | null;
};
nested: {};
flat: {};
};
'dev_xp_members': {
plain: {
'id': number;
Expand Down Expand Up @@ -442,6 +516,30 @@ export type Schema = {
'customer:updatedAt': string;
};
};
'reservations': {
plain: {
'booking_id': number | null;
'created_at': string;
'departure_id': number | null;
'id': number;
'name': string | null;
};
nested: {
'booking': Schema['bookings']['plain'] & Schema['bookings']['nested'];
'departure': Schema['departures']['plain'] & Schema['departures']['nested'];
};
flat: {
'booking:cancellation_id': number | null;
'booking:created_at': string;
'booking:id': number;
'booking:name': string | null;
'departure:created_at': string;
'departure:id': number;
'departure:name': string | null;
'booking:cancellation:created_at': string;
'booking:cancellation:id': number;
};
};
'review': {
plain: {
'id': number;
Expand Down Expand Up @@ -562,4 +660,30 @@ export type Schema = {
'owner:lastName': string;
};
};
'tablething': {
plain: {
'created_at': string;
'id': number;
'name': string | null;
'newCol': string | null;
'truc_id': number | null;
};
nested: {
'truc': Schema['tabletruc']['plain'] & Schema['tabletruc']['nested'];
};
flat: {
'truc:created_at': string | null;
'truc:data': string | null;
'truc:id': number;
};
};
'tabletruc': {
plain: {
'created_at': string | null;
'data': string | null;
'id': number;
};
nested: {};
flat: {};
};
};
21 changes: 14 additions & 7 deletions packages/agent/src/routes/modification/action/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default class ActionRoute extends CollectionRoute {
// As forms are dynamic, we don't have any way to ensure that we're parsing the data correctly
// => better send invalid data to the getForm() customer handler than to the execute() one.
const unsafeData = ForestValueConverter.makeFormDataUnsafe(rawData);
const fields = await this.collection.getForm(
const { fields } = await this.collection.getForm(
caller,
this.actionName,
unsafeData,
Expand Down Expand Up @@ -158,17 +158,24 @@ export default class ActionRoute extends CollectionRoute {

const caller = QueryStringParser.parseCaller(context);
const filter = await this.getRecordSelection(context);
const fields = await this.collection.getForm(caller, this.actionName, data, filter, {
changedField: body.data.attributes.changed_field,
searchField: body.data.attributes.search_field,
searchValues,
includeHiddenFields: false,
});
const { fields, layout } = await this.collection.getForm(
caller,
this.actionName,
data,
filter,
{
changedField: body.data.attributes.changed_field,
searchField: body.data.attributes.search_field,
searchValues,
includeHiddenFields: false,
},
);

context.response.body = {
fields: fields.map(field =>
SchemaGeneratorActions.buildFieldSchema(this.collection.dataSource, field),
),
layout,
};
}

Expand Down
14 changes: 11 additions & 3 deletions packages/agent/src/utils/forest-schema/action-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export default class ForestValueConverter {
const field = fields.find(f => f.label === key);

// Skip fields from the default form
if (!SchemaGeneratorActions.defaultFields.map(f => f.field).includes(key)) {
if (
!SchemaGeneratorActions.defaultFields.map(f => f.type !== 'Layout' && f.field).includes(key)
) {
if (ActionFields.isCollectionField(field) && value) {
const collection = dataSource.getCollection(field.collectionName);

Expand Down Expand Up @@ -50,7 +52,11 @@ export default class ForestValueConverter {

for (const field of fields) {
// Skip fields from the default form
if (!SchemaGeneratorActions.defaultFields.map(f => f.field).includes(field.field)) {
if (
!SchemaGeneratorActions.defaultFields
.map(f => f.type !== 'Layout' && f.field)
.includes(field.field)
) {
if (field.reference && field.value) {
const [collectionName] = field.reference.split('.');
const collection = dataSource.getCollection(collectionName);
Expand Down Expand Up @@ -82,7 +88,9 @@ export default class ForestValueConverter {

for (const [key, value] of Object.entries(rawData)) {
// Skip fields from the default form
if (!SchemaGeneratorActions.defaultFields.map(f => f.field).includes(key)) {
if (
!SchemaGeneratorActions.defaultFields.map(f => f.type !== 'Layout' && f.field).includes(key)
) {
if (Array.isArray(value) && value.every(v => this.isDataUri(v))) {
data[key] = value.map(uri => this.parseDataUri(uri));
} else if (this.isDataUri(value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
ActionFieldUserDropdown,
} from '@forestadmin/datasource-toolkit';
import {
ForestServerActionField,
ForestServerActionFieldAddressAutocompleteOptions,
ForestServerActionFieldCheckboxGroupOptions,
ForestServerActionFieldCheckboxOptions,
Expand All @@ -34,12 +33,15 @@ import {
ForestServerActionFieldTextInputOptions,
ForestServerActionFieldTimePickerOptions,
ForestServerActionFieldUserDropdown,
ForestServerActionInputField,
} from '@forestadmin/forestadmin-client';

import ActionFields from './action-fields';

export default class GeneratorActionFieldWidget {
static buildWidgetOptions(field: ActionField): ForestServerActionField['widgetEdit'] | undefined {
static buildWidgetOptions(
field: ActionField,
): ForestServerActionInputField['widgetEdit'] | undefined {
if (!ActionFields.hasWidget(field) || ['Collection', 'Enum', 'EnumList'].includes(field.type)) {
return undefined;
}
Expand Down
Loading
Loading