Skip to content

Commit

Permalink
add decorator allowedAction to the callable Button actions
Browse files Browse the repository at this point in the history
add decorator allowedAction to the callable Button actions
  • Loading branch information
jrief committed Jun 13, 2024
1 parent 9564181 commit 466c5c3
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 27 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
## Changes

1.5
* Drop support for Django-4.1.
* Drop support for Django-4.1 and Python-3.9.
* Add support for Python-3.12.
* Attribute `<button df-click="…">` now accepts function `setFieldValue()`. This can be used to
transfer values from one field to another one.
*
* Introduce partial submits and prefilling forms in collections.
* **Always** include `<script src="{% url 'javascript-catalog' %}"></script>` to the `<head>`-
section of an HTML template. This is because `gettext` is used inside many JavaScript files.

1.4.5
* Fix: When submitting a form with a `FileField`, the `UploadedFileInput` widget returns ``None``
Expand Down
68 changes: 44 additions & 24 deletions client/django-formset/DjangoFormset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,22 @@ class TernaryAction {
}


/*
* Decorator to be added to functions in class DjangoButton which are eligible to be chained into an action queue.
*/
function allowedAction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const action = originalMethod.apply(this, args);
return typeof action === 'function' ? (...funcArgs: any[]) => action.apply(target, funcArgs) : action;
};

descriptor.value.isAllowedAction = true; // tag function to be allowed as ButtonAction
return descriptor;
}



class DjangoButton {
private readonly formset: DjangoFormset;
public readonly element: HTMLButtonElement;
Expand Down Expand Up @@ -593,7 +609,6 @@ class DjangoButton {
/**
* Event handler to be called when someone clicks on the button.
*/
// @ts-ignore
private clicked = (event: Event) => {
if (event.currentTarget === this.element) {
this.formset.currentActiveButton = this;
Expand All @@ -610,7 +625,7 @@ class DjangoButton {
/**
* Disable the button for further submission.
*/
// @ts-ignore
@allowedAction
private disable() {
return (response: Response) => {
this.element.disabled = true;
Expand All @@ -621,7 +636,7 @@ class DjangoButton {
/**
* Re-enable the button for further submission.
*/
// @ts-ignore
@allowedAction
private enable() {
return (response: Response) => {
this.element.disabled = false;
Expand All @@ -632,7 +647,7 @@ class DjangoButton {
/**
* Validate form content and submit to the endpoint given in element `<django-formset>`.
*/
// @ts-ignore
@allowedAction
submit(data?: Object) {
return () => {
return new Promise((resolve, reject) => {
Expand All @@ -646,7 +661,7 @@ class DjangoButton {
/**
* Validate the current form content and only submit that form's content to the endpoint given in element `<django-formset>`.
*/
// @ts-ignore
@allowedAction
submitPartial(data?: Object) {
return () => {
return new Promise((resolve, reject) => {
Expand All @@ -661,15 +676,15 @@ class DjangoButton {
/**
* Reset form content to their initial values.
*/
// @ts-ignore
@allowedAction
reset() {
return (response: Response) => {
this.formset.resetToInitial();
return Promise.resolve(response);
};
}

// @ts-ignore
@allowedAction
private reload(includeQuery?: Boolean) {
return (response: Response) => {
includeQuery ? location.reload() : location.replace(window.location.pathname);
Expand All @@ -687,7 +702,7 @@ class DjangoButton {
* @param proceedUrl (optional): If set, proceed to that URL regardless of the
* response status.
*/
// @ts-ignore
@allowedAction
private proceed(proceedUrl?: string) {
return async (response: Response) => {
if (typeof proceedUrl === 'string' && proceedUrl.length > 0) {
Expand All @@ -711,7 +726,7 @@ class DjangoButton {
*
* @param ms: Time to wait in milliseconds.
*/
// @ts-ignore
@allowedAction
private delay(ms: number) {
return (response: Response) => new Promise(resolve => this.timeoutHandler = window.setTimeout(() => {
this.timeoutHandler = undefined;
Expand All @@ -722,7 +737,7 @@ class DjangoButton {
/**
* Replace the button's decorator against a spinner icon.
*/
// @ts-ignore
@allowedAction
private spinner() {
return (response: Response) => {
this.decoratorElement?.replaceChildren(this.spinnerElement);
Expand All @@ -733,15 +748,15 @@ class DjangoButton {
/**
* Replace the button's decorator against an okay animation.
*/
// @ts-ignore
@allowedAction
private okay(ms?: number) {
return this.decorate(this.okayElement, ms);
}

/**
* Replace the button's decorator against a bummer animation.
*/
// @ts-ignore
@allowedAction
private bummer(ms?: number) {
return this.decorate(this.bummerElement, ms);
}
Expand All @@ -751,7 +766,7 @@ class DjangoButton {
*
* @param cssClass: The CSS class.
*/
// @ts-ignore
@allowedAction
private addClass(cssClass: string) {
return (response: Response) => {
this.element.classList.add(cssClass);
Expand All @@ -764,7 +779,7 @@ class DjangoButton {
*
* @param cssClass: The CSS class.
*/
// @ts-ignore
@allowedAction
private removeClass(cssClass: string) {
return (response: Response) => {
this.element.classList.remove(cssClass);
Expand All @@ -777,7 +792,7 @@ class DjangoButton {
*
* @param cssClass: The CSS class.
*/
// @ts-ignore
@allowedAction
private toggleClass(cssClass: string) {
return (response: Response) => {
this.element.classList.toggle(cssClass);
Expand All @@ -790,7 +805,7 @@ class DjangoButton {
*
* @param event: The named event.
*/
// @ts-ignore
@allowedAction
private emit(namedEvent: string, detail?: Object) {
return (response: Response) => {
const options = {bubbles: true, cancelable: true};
Expand All @@ -808,7 +823,7 @@ class DjangoButton {
* For debugging purpose only: Intercept, log and forward the response object to the next handler.
* @param selector: If selector points onto a valid element in the DOM, the server response is inserted.
*/
// @ts-ignore
@allowedAction
private intercept(selector?: string) {
return (response: Response) => {
const body = {
Expand All @@ -831,7 +846,7 @@ class DjangoButton {
/**
* Clear all errors in the current django-formset.
*/
// @ts-ignore
@allowedAction
private clearErrors() {
return (response: Response) => {
this.formset.clearErrors();
Expand All @@ -842,7 +857,7 @@ class DjangoButton {
/**
* Scroll to first element reporting an error.
*/
// @ts-ignore
@allowedAction
private scrollToError() {
return (response: Response) => {
const errorReportElement = this.formset.findFirstErrorReport();
Expand All @@ -856,7 +871,7 @@ class DjangoButton {
/**
* Confirm a user response. If it is accepted proceed, otherwise reject.
*/
// @ts-ignore
@allowedAction
private confirm(message: string) {
if (typeof message !== 'string')
throw new Error("The confirm() action requires a message.")
Expand All @@ -873,7 +888,7 @@ class DjangoButton {
* Show an alert message with the response text for other types of errors, such as permission denied.
* This can be useful information to the end user in case the Django endpoint can not process a request.
*/
// @ts-ignore
@allowedAction
private alertOnError() {
return (response: Response) => {
if (response.status !== 422) {
Expand All @@ -884,8 +899,9 @@ class DjangoButton {
}

/**
* Action to activate a button so that it can be used by dialogs.
* Action to activate a button so that a dialog can be induced by it.
*/
@allowedAction
private activate(...args: any[]) {
return (response: Response) => {
this.formset.updateOperability(...args);
Expand All @@ -896,13 +912,15 @@ class DjangoButton {
/**
* Transfer value from one element to another one.
*/
@allowedAction
private setFieldValue(target: Path, source: FieldValue) {
return (response: Response) => {
this.formset.setFieldValue(target, source);
return Promise.resolve(response);
}
}

@allowedAction
private deletePartial(target: Path, source: FieldValue) {
return (response: Response) => {
if (typeof source === 'string' && parseInt(source)) {
Expand All @@ -915,13 +933,13 @@ class DjangoButton {
/**
* Dummy action to be called in case of empty actionsQueue.
*/
@allowedAction
private noop() {
return (response: Response) => {
return Promise.resolve(response);
}
}

// @ts-ignore
/*
* Called after all actions have been executed.
*/
Expand Down Expand Up @@ -1010,7 +1028,7 @@ class DjangoButton {
const innerAction = (action: any) => {
if (isPlainObject(action) && typeof action.funcname === 'string' && Array.isArray(action.args)) {
const func = this[action.funcname as keyof DjangoButton];
if (typeof func !== 'function')
if (typeof func !== 'function' || !((func as any)['isAllowedAction']))
throw new Error(`Unknown function '${action.funcname}'.`);
return new ButtonAction(func, action.args.map(innerAction));
}
Expand Down Expand Up @@ -1058,10 +1076,12 @@ class DjangoButton {
this.formset.currentActiveButton = null;
}

@allowedAction
private getDataValue(path: Path) {
return this.formset.getDataValue(path);
}

@allowedAction
private getResponseValue(path: Path, body: JSONValue) {
return getDataValue(body, path);
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"esModuleInterop": false,
"allowJs": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": false,
"experimentalDecorators": true,
"importHelpers": true,
"rootDir": "./",
"baseUrl": "./client",
Expand Down

0 comments on commit 466c5c3

Please sign in to comment.