Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #10 from openizr/0.0.21
Browse files Browse the repository at this point in the history
0.0.21
  • Loading branch information
matthieujabbour committed Aug 1, 2021
2 parents 7092182 + a7d05d9 commit 8100971
Show file tree
Hide file tree
Showing 44 changed files with 1,363 additions and 929 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Environment (development|preproduction|production)
ENV=development

# Which network interface should expose app's ports
HOST_IP=127.0.0.1

# Playground port
PLAYGROUND_PORT=5050

Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: "3.6"
services:
library:
container_name: ${PROJECT_NAME}_library
image: openizr/node:2.0.0-dev
image: openizr/node:2.0.1-dev
env_file:
- .env
command: sh -c "dsync src/scripts/types.d.ts dist/types.d.ts & dsync src/scripts/types.d.ts dist/react.d.ts & dsync src/scripts/types.d.ts dist/vue.d.ts & yarn && yarn run dev"
Expand All @@ -12,11 +12,11 @@ services:
restart: unless-stopped
playground:
container_name: ${PROJECT_NAME}_playground
image: openizr/node:2.0.0-dev
image: openizr/node:2.0.1-dev
env_file:
- .env
ports:
- "127.0.0.1:${PLAYGROUND_PORT}:${PLAYGROUND_PORT}"
- "${HOST_IP}:${PLAYGROUND_PORT}:${PLAYGROUND_PORT}"
command: sh -c "dsync /library/dist/ node_modules/gincko/"
volumes:
- ./playground:/var/www/html
Expand Down
16 changes: 8 additions & 8 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
],
"license": "MIT",
"devDependencies": {
"@types/prop-types": "^15.7.3",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.7",
"@vue/test-utils": "^1.2.1",
"coveralls": "^3.1.0",
"@types/prop-types": "^15.7.4",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"@vue/test-utils": "^1.2.2",
"coveralls": "^3.1.1",
"typescript-dev-kit": "^3.1.1"
},
"eslintConfig": {
Expand All @@ -38,10 +38,10 @@
]
},
"dependencies": {
"basx": "^1.3.4",
"diox": "^4.0.1",
"basx": "^1.3.6",
"diox": "^4.0.5",
"localforage": "^1.9.0",
"sonar-ui": "^0.0.38"
"sonar-ui": "^0.0.39"
},
"engines": {
"node": ">= 10.0.0",
Expand Down
4 changes: 2 additions & 2 deletions library/src/scripts/__mocks__/localforage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
export default {
getItem: jest.fn(() => {
if (process.env.CACHE_EXISTING_FORM === 'true') {
return Promise.resolve(JSON.stringify({
return Promise.resolve({
formValues: { test: 'value' },
steps: [{
fields: [
Expand All @@ -38,7 +38,7 @@ export default {
id: 'test',
status: 'initial',
}],
}));
});
}
return Promise.resolve(null);
}),
Expand Down
96 changes: 46 additions & 50 deletions library/src/scripts/core/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ import Store from 'diox';
import { deepCopy } from 'basx';
import localforage from 'localforage';
import steps from 'scripts/core/steps';
import { Step } from 'scripts/propTypes/step';
import { Field } from 'scripts/propTypes/field';
import userActions from 'scripts/core/userActions';
import valuesLoader from 'scripts/core/valuesLoader';
import errorHandler from 'scripts/core/errorHandler';
import valuesChecker from 'scripts/core/valuesChecker';
import valuesUpdater from 'scripts/core/valuesUpdater';
import { InferProps } from 'prop-types';
import stepPropTypes from 'scripts/propTypes/step';
import fieldPropTypes from 'scripts/propTypes/field';
import configurationPropTypes from 'scripts/propTypes/configuration';
import { Configuration } from 'scripts/propTypes/configuration';

type HookData = FormValues | Error | Step | UserAction | null;

export type FormValue = Json;
export type Plugin = (engine: Engine) => void;
export type Step = InferProps<typeof stepPropTypes>;
export type Field = InferProps<typeof fieldPropTypes>;
export type Configuration = InferProps<typeof configurationPropTypes>;
export type FormValue = any; // eslint-disable-line @typescript-eslint/no-explicit-any
export type FormEvent = 'loadNextStep' | 'loadedNextStep' | 'userAction' | 'submit' | 'error';
export type Hook<Type> = (data: Type, next: (data?: Type) => Promise<Type>) => Promise<Type>;

Expand All @@ -50,14 +48,17 @@ export default class Engine {
/** Cache name key. */
private cacheKey: string;

/** Whether form should store its state in cache. */
private useCache: boolean;

/** Timeout after which to refresh cache. */
private cacheTimeout: number | null;

/** Form engine configuration. Contains steps, elements, ... */
private configuration: Configuration;

/** Contains all events hooks to trigger when events are fired. */
private hooks: { [eventName: string]: Hook<Json>[]; };
private hooks: { [eventName: string]: Hook<FormValues | Error | Step | UserAction | null>[]; };

/** Contains the actual form steps, as they are currently displayed to end-user. */
private generatedSteps: Step[];
Expand All @@ -70,26 +71,17 @@ export default class Engine {
*
* @param {FormEvent} eventName Event's name.
*
* @param {Json} [data = undefined] Additional data to pass to the hooks chain.
* @param {FormValues | Error | Step | UserAction | null} [data = undefined] Additional data
* to pass to the hooks chain.
*
* @returns {Promise} Pending hooks chain.
* @returns {Promise<FormValues | Error | Step | UserAction | null>} Pending hooks chain.
*
* @throws {Error} If any event hook does not return a Promise.
*/
private triggerHooks(eventName: 'submit', data: FormValues | null): Promise<FormValues | null>;

private triggerHooks(eventName: 'loadNextStep', data: Step | null): Promise<Step | null>;

private triggerHooks(eventName: 'loadedNextStep', data: Step | null): Promise<Step | null>;

private triggerHooks(eventName: 'userAction', data: UserAction | null): Promise<UserAction | null>;

private triggerHooks(eventName: 'error', data: Error | null): Promise<Error | null>;

private triggerHooks(eventName: FormEvent, data?: Json): Promise<Json> {
private triggerHooks(eventName: FormEvent, data?: HookData): Promise<HookData> {
const hooksChain = this.hooks[eventName].concat([]).reverse().reduce((chain, hook) => (
(updatedData): Promise<Json> => {
const hookResult = hook(updatedData, chain as (data: Json) => Promise<Json>);
(updatedData): Promise<HookData> => {
const hookResult = hook(updatedData, chain as (data?: HookData) => Promise<HookData>);
if (!(hookResult && typeof hookResult.then === 'function')) {
throw new Error(`Event "${eventName}": all your hooks must return a Promise.`);
}
Expand All @@ -98,7 +90,7 @@ export default class Engine {
), (updatedData) => Promise.resolve(updatedData));
// Hooks chain must first be wrapped in a Promise to catch all errors with proper error hooks.
return Promise.resolve()
.then(() => (hooksChain as (data: Json) => Promise<Json>)(data))
.then(() => (hooksChain as (data?: HookData) => Promise<HookData>)(data))
.then((updatedData) => {
if (updatedData === undefined) {
throw new Error(
Expand All @@ -117,17 +109,18 @@ export default class Engine {
const currentStep = this.generatedSteps[this.getCurrentStepIndex()] || null;
this.setCurrentStep(currentStep, true);
window.clearTimeout(this.cacheTimeout as number);
// If cache is enabled, we store current form state, except on form submission, when
// If cache is enabled, we store current form state, except after submission, when
// cache must be completely cleared.
if (this.configuration.cache !== false && eventName !== 'submit') {
if (this.useCache && eventName !== 'submit') {
this.cacheTimeout = window.setTimeout(() => {
localforage.setItem(this.cacheKey, JSON.stringify({
localforage.setItem(this.cacheKey, {
steps: this.generatedSteps,
formValues: this.formValues,
}));
});
}, 500);
} else {
localforage.removeItem(this.cacheKey);
} else if (eventName === 'submit' && this.configuration.clearCacheOnSubmit !== false) {
this.useCache = false;
this.clearCache();
}
});
}
Expand All @@ -152,7 +145,7 @@ export default class Engine {
this.generatedSteps = newSteps;
this.triggerHooks('loadedNextStep', newSteps[newSteps.length - 1]).then((updatedNextStep) => {
if (updatedNextStep !== null) {
this.setCurrentStep(updatedNextStep);
this.setCurrentStep(<Step>updatedNextStep);
}
});
} else {
Expand All @@ -171,7 +164,7 @@ export default class Engine {
const nextStep = this.createStep(nextStepId || null);
this.triggerHooks('loadNextStep', nextStep).then((updatedNextStep) => {
if (updatedNextStep !== null) {
this.updateGeneratedSteps(this.getCurrentStepIndex() + 1, updatedNextStep);
this.updateGeneratedSteps(this.getCurrentStepIndex() + 1, <Step>updatedNextStep);
}
});
}
Expand Down Expand Up @@ -223,7 +216,9 @@ export default class Engine {
this.formValues[userAction.fieldId] = userAction.value;
this.generatedSteps = this.generatedSteps.slice(0, userAction.stepIndex + 1);
}
this.triggerHooks('userAction', userAction).then(this.handleSubmit.bind(this));
this.triggerHooks('userAction', userAction).then((updatedUserAction) => (
this.handleSubmit.bind(this)(<UserAction | null>updatedUserAction)
));
}

/**
Expand All @@ -242,6 +237,7 @@ export default class Engine {
this.cacheTimeout = null;
this.generatedSteps = [];
this.configuration = configuration;
this.useCache = this.configuration.cache !== false;
this.cacheKey = `gincko_${configuration.id || 'cache'}`;
this.hooks = {
error: [],
Expand Down Expand Up @@ -272,6 +268,7 @@ export default class Engine {
setCurrentStep: this.setCurrentStep.bind(this),
createField: this.createField.bind(this),
createStep: this.createStep.bind(this),
clearCache: this.clearCache.bind(this),
});
});

Expand All @@ -284,12 +281,12 @@ export default class Engine {

// Depending on the configuration, we want either to load the complete form from cache, or just
// its filled values and restart journey from the beginning.
localforage.getItem(this.cacheKey).then((data) => {
const parsedData = JSON.parse(data as string || '{"formValues":{}}');
if (this.configuration.autoFill !== false) {
localforage.getItem<{ formValues: FormValues; steps: Step[]; }>(this.cacheKey).then((data) => {
const parsedData = data || { formValues: {}, steps: [] };
if (this.useCache && this.configuration.autoFill !== false) {
this.formValues = parsedData.formValues;
}
if (data !== null && this.configuration.restartOnReload !== true) {
if (data !== null && this.useCache && this.configuration.restartOnReload !== true) {
const lastStepIndex = parsedData.steps.length - 1;
const lastStep = parsedData.steps[lastStepIndex];
this.generatedSteps = parsedData.steps.slice(0, lastStepIndex);
Expand Down Expand Up @@ -440,21 +437,11 @@ export default class Engine {
*
* @param {FormEvent} eventName Name of the event to register hook for.
*
* @param {Hook<Json>} hook Hook to register.
* @param {Hook<FormValues | Error | Step | UserAction | null>} hook Hook to register.
*
* @returns {void}
*/
public on(eventName: 'userAction', hook: Hook<UserAction | null>): void;

public on(eventName: 'loadNextStep', hook: Hook<Step | null>): void;

public on(eventName: 'loadedNextStep', hook: Hook<Step | null>): void;

public on(eventName: 'error', hook: Hook<Error | null>): void;

public on(eventName: 'submit', hook: Hook<FormValues | null>): void;

public on(eventName: FormEvent, hook: Hook<Json>): void {
public on(eventName: FormEvent, hook: Hook<FormValues | Error | Step | UserAction | null>): void {
this.hooks[eventName].push(hook);
}

Expand All @@ -479,4 +466,13 @@ export default class Engine {
public userAction(userAction: UserAction): void {
this.store.mutate('userActions', 'ADD', userAction);
}

/**
* Clears current form cache.
*
* @returns {Promise<void>}
*/
public async clearCache(): Promise<void> {
return localforage.removeItem(this.cacheKey);
}
}
12 changes: 7 additions & 5 deletions library/src/scripts/core/__mocks__/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
*
*/

import { Configuration } from 'scripts/core/Engine';
import { Configuration } from 'scripts/propTypes/configuration';

type Any = any; // eslint-disable-line @typescript-eslint/no-explicit-any

/**
* Engine mock.
*/
export default jest.fn((configuration = {}) => {
const hooks: { [key: string]: Json[] } = {
const hooks: { [eventName: string]: ((...args: Any[]) => Any)[]; } = {
error: [],
submit: [],
userAction: [],
Expand Down Expand Up @@ -121,11 +123,11 @@ export default jest.fn((configuration = {}) => {
],
};
}),
on: jest.fn((event: string, callback: Json) => {
on: jest.fn((event: string, callback: () => Any) => {
hooks[event].push(callback);
}),
trigger: (event: string, data: Json, nextData?: Json): Json => (
Promise.all(hooks[event].map((hook) => hook(data, (updatedData: Json) => {
trigger: (event: string, data: Any, nextData?: Any): Any => (
Promise.all(hooks[event].map((hook) => hook(data, (updatedData: Any) => {
next(updatedData);
return Promise.resolve(nextData);
})))
Expand Down
Loading

0 comments on commit 8100971

Please sign in to comment.