Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ module.exports = {
"no-undef-init": "error",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-expressions": "warn",
"no-unused-labels": "error",
"no-var": "error",
"object-shorthand": "error",
Expand Down
378 changes: 284 additions & 94 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workshop_butler_widget",
"version": "1.16.2",
"version": "1.17.0",
"description": "Workshop Butler JS Widgets",
"author": "https://workshopbutler.com",
"license": "ISC",
Expand Down
1 change: 1 addition & 0 deletions site/pages/event-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
show: true,
bio: true,
},
forwardSearchParams: true,
trainerPageUrl: '/trainer-profile',
registrationPageUrl: '/registration',
expiredTickets: true,
Expand Down
6 changes: 5 additions & 1 deletion site/pages/registration.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
trainers: {
show: true,
bio: true,
}
},
forwardSearchParams: [{name: 'ref', to: 'first_name', hidden: true}, {
name: 'name',
to: '0421dc07',
}]
}
];
const config = {
Expand Down
3 changes: 2 additions & 1 deletion src/models/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export default class Event {
const language = Language.fromJSON(json.language);
const trainers = Event.getTrainers(json, options);
const tickets = Event.getTickets(json.free, json.tickets);
const registrationPage = new RegistrationPage(json.registration_page, options.registrationPageUrl, json.hashed_id);
const registrationPage = new RegistrationPage(json.registration_page,
options.registrationPageUrl, options.forwardSearchParams, json.hashed_id);
const type = json.type ? (typeof json.type === 'number' ? json.type : new Type(json.type)) : undefined;
const coverImage = CoverImage.fromJSON(json.cover_image);
const category = json.category ? new Category(json.category) : undefined;
Expand Down
19 changes: 9 additions & 10 deletions src/models/workshop/RegistrationPage.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import IPlainObject from '../../interfaces/IPlainObject';
import ForwardSearchParamsSetting, {
ForwardSearchParamsSettingType,
} from '../../widgets/config/ForwardSearchParamsSetting';

/**
* Contains the logic for the event registration
*/
export default class RegistrationPage {

/**
* Returns a correctly formed url for a registration page of the event
* @param registrationPageUrl {string} Url of the page with RegistrationPage widget
* @param eventId {string} Hashed event id
*/
protected static getInternalUrl(registrationPageUrl: string, eventId: string): string {
return registrationPageUrl + `?id=${eventId}`;
}
readonly external: boolean;
readonly url?: string;
readonly passQueryParams: ForwardSearchParamsSetting;

constructor(attrs: IPlainObject, registrationUrl: string | null = null, eventId: string) {
constructor(attrs: IPlainObject, registrationUrl: string | null = null,
forwardSearchParams: ForwardSearchParamsSettingType,
eventId: string) {
if (attrs) {
this.external = attrs.external;
this.url = attrs.url;
}
this.passQueryParams = new ForwardSearchParamsSetting(forwardSearchParams);
if (!this.external && registrationUrl) {
this.url = RegistrationPage.getInternalUrl(registrationUrl, eventId);
this.url = this.passQueryParams.createUrl(registrationUrl, {id: eventId});
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/utils/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function isString(value: any): value is string {
return typeof value === 'string';
}
export function isBoolean(value: any): value is boolean {
return typeof value === 'boolean';
}
6 changes: 4 additions & 2 deletions src/widgets/RegistrationPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export default class RegistrationPage extends Widget<RegistrationPageConfig> {
this.getErrorMessages());

const paymentConfig = this.getPaymentConfig();
const formConfig = new RegistrationFormConfig(this.event.id, this.config.successRedirectUrl);
const formConfig = new RegistrationFormConfig(this.event.id,
this.config.searchToFieldConfigs,
this.config.successRedirectUrl);
return new SharedRegistrationForm(this.$root.find('.wsb-body'), formHelper,
formConfig, paymentConfig);
}
Expand All @@ -144,7 +146,7 @@ export default class RegistrationPage extends Widget<RegistrationPageConfig> {
const registerUrl = `attendees/register?api_key=${this.apiKey}&t=${this.getWidgetStats()}`;
const preRegisterUrl = `attendees/pre-register?api_key=${this.apiKey}&t=${this.getWidgetStats()}`;
const taxValidationUrl = `tax-validation/:number?api_key=${this.apiKey}&t=${this.getWidgetStats()}`
+`&lang=${this.formatter.getLocale()}`;
+ `&lang=${this.formatter.getLocale()}`;

return new PaymentConfig(this.event.cardPayment?.active || false, this.event.free,
this.event.cardPayment?.testMode() || false, preRegisterUrl, registerUrl, taxValidationUrl,
Expand Down
8 changes: 6 additions & 2 deletions src/widgets/config/EventPageConfig.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {logError} from '../../common/Error';
import {absoluteURL, safeHref} from '../../common/helpers/UrlParser';
import IPlainObject from '../../interfaces/IPlainObject';
import WidgetConfig from './WidgetConfig';
import CoverImageConfig from './CoverImageConfig';
import TrainersConfig from './TrainersConfig';
import WidgetConfig from './WidgetConfig';
import {ForwardSearchParamsSettingType, WithPassSearchParamsSetting} from './ForwardSearchParamsSetting';

/**
* Contains @EventPageConfig widget configuration options
*/
export default class EventPageConfig extends WidgetConfig {
export default class EventPageConfig extends WidgetConfig implements WithPassSearchParamsSetting {

/**
* Returns the config if the options are correct
Expand Down Expand Up @@ -86,6 +87,8 @@ export default class EventPageConfig extends WidgetConfig {
*/
readonly showAdditionalButton: boolean;

readonly forwardSearchParams: ForwardSearchParamsSettingType;

protected constructor(options: IPlainObject) {
super(options);
this.trainers = new TrainersConfig(options.trainers);
Expand All @@ -100,6 +103,7 @@ export default class EventPageConfig extends WidgetConfig {
options.coverImage.width, options.coverImage.height) : new CoverImageConfig();
this.eventPageUrl = safeHref();
this.showAdditionalButton = options.showAdditionalButton !== undefined ? options.showAdditionalButton : false;
this.forwardSearchParams = options.forwardSearchParams;
}

}
82 changes: 82 additions & 0 deletions src/widgets/config/ForwardSearchParamsSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import URI from 'urijs';

export type ForwardSearchParamsSettingType = boolean | string | undefined;

export interface WithPassSearchParamsSetting {
forwardSearchParams: ForwardSearchParamsSettingType;
}

/**
* This setting defines if the search params in URL should be passed to other widgets within between-widget navigation.
* For example, when we navigate from the event page widget to the registration form widget, the query params
* could be added to the registration form widget URL to trace referrals.
*
* If `forward` is set to `true`, search params will be passed further. Some params could be replaced if they are
* reserved by the widget (like `id`).
*
* A user can specify which params should be forwarded by providing a list of params separated by comma.
*/
export default class ForwardSearchParamsSetting {

/**
* If `forward` is set to `true`, search params will be passed further. Some params could be replaced if they are
* reserved by the widget (like `id`).
*
* @default false
* @type {boolean}
*/
readonly forward: boolean;

/**
* A set of parameters that should be forwarded.
*
* @default Seq()
* @type {Set<string>}
*/
readonly limitParams: Set<string>;

constructor(value: ForwardSearchParamsSettingType) {
this.forward = value ? (typeof value === 'boolean' ? value : true) : false;
this.limitParams = value ? ForwardSearchParamsSetting.parseParameters(value) : new Set();
}

createUrl(url: string, params: { [key: string]: string | number }): string {
const search = this.forward ? window.location.search : '';
const uri = new URI(url + search);
if (this.forward && this.limitParams.size > 0) {
Object.entries(uri.search(true)).forEach(value => {
const [key] = value;
if (!this.limitParams.has(key)) {
uri.removeSearch(key);
}
});
}
this.addSearchParams(uri, params);
return uri.toString();
}

/**
* Applies the given query params to the given URI, and replaces the value of existing ones.
*
* @param uri {URI} the URI to apply the query params to
* @param params {Object} the query params to apply
* @protected
*/
protected addSearchParams(uri: URI, params: { [key: string]: string | number }): void {
const map = new Map<string, string | number>(Object.entries(params));
map.forEach((value, key) => {
if (uri.hasSearch(key)) {
uri.setSearch(key, value.toString());
} else {
uri.addSearch(key, value);
}
});
}

private static parseParameters(param: boolean | string): Set<string> {
if (typeof param === 'string') {
return new Set(param.split(',').map(s => s.trim()));
}
return new Set<string>();
}
}
85 changes: 68 additions & 17 deletions src/widgets/config/RegistrationPageConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,35 @@ import {logError} from '../../common/Error';
import IPlainObject from '../../interfaces/IPlainObject';
import WidgetConfig from './WidgetConfig';
import TrainersConfig from './TrainersConfig';
import {isBoolean, isString} from '../../utils/validators';

/**
* The registration page widgets supports the injection of values from search query into the form. It works this way:
* - The search query is parsed and the values are extracted.
* - The widget inserts the extracted values into the related form fields.
* - If the field is configured to be hidden, then the user is not able to change the value as
* it's hidden from the form
*
* This interface describes the configuration for each field.
*
* @param {string} to The name of the field to which the value should be injected. For example,
* if the search param is `ref` but the field name is `reference`, then a user sets `to=reference`.
* @param {boolean} hidden If true, the field will not be displayed in the form if the search parameter is present.
*/
export interface SearchToFormFieldConfig {
to: string;
hidden: boolean;
}

/**
* Contains @RegistrationPageConfig widget configuration options
*/
export default class RegistrationPageConfig extends WidgetConfig {

/**
* Returns the config if the options are correct
* @param options {IPlainObject} Widget's options
*/
* Returns the config if the options are correct
* @param options {IPlainObject} Widget's options
*/
static create(options: IPlainObject): RegistrationPageConfig | null {
if (RegistrationPageConfig.validate(options)) {
return new RegistrationPageConfig(options);
Expand All @@ -21,9 +40,9 @@ export default class RegistrationPageConfig extends WidgetConfig {
}

/**
* Returns true if the options can be used to create the widget's config
* @param options {IPlainObject} Widget's config
*/
* Returns true if the options can be used to create the widget's config
* @param options {IPlainObject} Widget's config
*/
protected static validate(options: IPlainObject): boolean {
let valid = true;
if (options.eventPageUrl && typeof options.eventPageUrl !== 'string') {
Expand All @@ -32,6 +51,7 @@ export default class RegistrationPageConfig extends WidgetConfig {
}
return valid;
}

readonly eventPageUrl?: string;

/**
Expand All @@ -40,28 +60,28 @@ export default class RegistrationPageConfig extends WidgetConfig {
readonly trainers: TrainersConfig;

/**
* When true, expired tickets are shown
*/
* When true, expired tickets are shown
*/
readonly expiredTickets: boolean;

/**
* When true, the number of left tickets for each ticket type is shown
*/
* When true, the number of left tickets for each ticket type is shown
*/
readonly numberOfTickets: boolean;

/**
* Array of country codes to use for the registration form
*/
* Array of country codes to use for the registration form
*/
readonly countryOnlyFrom: string[];

/**
* Preselected default country
*/
* Preselected default country
*/
readonly countryDefault: string;

/**
* Redirect to success page url instead of only showing success message
*/
* Redirect to success page url instead of only showing success message
*/
readonly successRedirectUrl: string;

/**
Expand All @@ -74,10 +94,15 @@ export default class RegistrationPageConfig extends WidgetConfig {
*/
readonly eventId?: string;

/**
* The names of search parameters that should be injected into the form together with their configurations
*/
readonly searchToFieldConfigs: Map<string, SearchToFormFieldConfig>;

protected constructor(options: IPlainObject) {
super(options);
this.eventPageUrl = options.eventPageUrl ? options.eventPageUrl : undefined;
this.trainers = new TrainersConfig(options.trainers);;
this.trainers = new TrainersConfig(options.trainers);
this.expiredTickets = options.expiredTickets !== undefined ? options.expiredTickets : false;
this.numberOfTickets = options.numberOfTickets !== undefined ? options.numberOfTickets : true;
if (options.country) {
Expand All @@ -87,5 +112,31 @@ export default class RegistrationPageConfig extends WidgetConfig {
this.successRedirectUrl = options.successRedirectUrl !== undefined ? options.successRedirectUrl : undefined;
this.ticketId = options.ticketId ? options.ticketId : undefined;
this.eventId = options.eventId ? options.eventId : undefined;
this.searchToFieldConfigs = Array.isArray(options.forwardSearchParams) ?
RegistrationPageConfig.processSearchToFormFieldOption(options.forwardSearchParams) :
new Map();
}

/**
* Converts `forwardSearchParams` option to a map of search parameters and their configurations.
* The correct format is: [{name: 'searchName', to: 'fieldName', hidden: false}, ...]
*
* However, the method is very forgiving and will handle any format for the search param configuration if it has
* `name` property. In this case, the `to` property is set to `name` value, and the `hidden` property is set to false
*
* @param options {IPlainObject[]} The array of search parameters configurations
* @protected
*/
protected static processSearchToFormFieldOption(options: IPlainObject[]): Map<string, SearchToFormFieldConfig> {
const values = new Map();
options.forEach(x => {
if (isString(x.name)) {
const name = x.name;
const to = isString(x.to) ? x.to : name;
const hidden = isBoolean(x.hidden) ? x.hidden : false;
values.set(name, {to, hidden});
}
});
return values;
}
}
Loading