summaryrefslogtreecommitdiff
path: root/web/source/settings/lib/form/submit.ts
blob: 498481deb562d2c537058f07a0db573cff6331e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
	GoToSocial
	Copyright (C) GoToSocial Authors admin@gotosocial.org
	SPDX-License-Identifier: AGPL-3.0-or-later

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import getFormMutations from "./get-form-mutations";

import { useRef } from "react";

import type {
	MutationTrigger,
	UseMutationStateResult,
} from "@reduxjs/toolkit/dist/query/react/buildHooks";

import type {
	FormSubmitEvent,
	FormSubmitFunction,
	FormSubmitResult,
	HookedForm,
} from "./types";

interface UseFormSubmitOptions {
	/**
	 * Include only changed fields when submitting the form.
	 * If no fields have been changed, submit will be a noop.
	 */
	changedOnly: boolean;
	/**
	 * Optional function to run when the form has been sent
	 * and a response has been returned from the server.
	 */
	onFinish?: ((_res: any) => void);
	/**
	 * Can be optionally used to modify the final mutation argument from the
	 * gathered mutation data before it's passed into the trigger function.
	 * 
	 * Useful if the mutation trigger function takes not just a simple key/value
	 * object but a more complicated object.
	 */
	customizeMutationArgs?: (_mutationData: { [k: string]: any }) => any;
}

/**
 * Parse changed values from the hooked form into a request
 * body, and submit it using the given mutation trigger.
 * 
 * This function basically wraps RTK Query's submit methods to
 * work with our hooked form interface.
 * 
 * An `onFinish` callback function can be provided, which will
 * be executed on a **successful** run of the given MutationTrigger,
 * with the mutation result passed into it.
 * 
 * If `changedOnly` is false, then **all** fields of the given HookedForm
 * will be submitted to the mutation endpoint, not just changed ones.
 * 
 * The returned function and result can be triggered and read
 * from just like an RTK Query mutation hook result would be.
 * 
 * See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
 */
export default function useFormSubmit(
	form: HookedForm,
	mutationQuery: readonly [MutationTrigger<any>, UseMutationStateResult<any, any>],
	opts: UseFormSubmitOptions = { changedOnly: true }
): [ FormSubmitFunction, FormSubmitResult ] {
	if (!Array.isArray(mutationQuery)) {
		throw "useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?";
	}

	const { changedOnly, onFinish } = opts;
	const [runMutation, mutationResult] = mutationQuery;
	const usedAction = useRef<FormSubmitEvent>(undefined);

	const submitForm = async(e: FormSubmitEvent) => {
		let action: FormSubmitEvent;
		
		if (typeof e === "string") {
			if (e !== "") {
				// String action name was provided.
				action = e;
			} else {
				// Empty string action name was provided.
				action = undefined;
			}
		} else if (e) {
			// Submit event action was provided.
			e.preventDefault();
			if (e.nativeEvent.submitter) {
				// We want the name of the element that was invoked to submit this form,
				// which will be something that extends HTMLElement, though we don't know
				// what at this point. If it's an empty string, fall back to undefined.
				// 
				// See: https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter
				action = (e.nativeEvent.submitter as Object as { name: string }).name || undefined;
			} else {
				// No submitter defined. Fall back
				// to just use the FormSubmitEvent.
				action = e;
			}
		} else {
			// Void or null or something
			// else was provided.
			action = undefined;
		}

		usedAction.current = action;

		// Transform the hooked form into an object.
		let {
			mutationData,
			updatedFields,
		} = getFormMutations(form, { changedOnly });
		
		// If there were no updated fields according to
		// the form parsing then there's nothing for us
		// to do, since remote and desired state match.
		if (updatedFields.length == 0) {
			return;
		}

		// Final tweaks on the mutation
		// argument before triggering it.
		mutationData.action = action;
		if (opts.customizeMutationArgs) {
			mutationData = opts.customizeMutationArgs(mutationData);
		}

		try {
			const res = await runMutation(mutationData);
			if (onFinish) {
				onFinish(res);
			}
		} catch (e) {
			// eslint-disable-next-line no-console
			console.error(`caught error running mutation: ${e}`);
		}
	};
	
	return [
		submitForm,
		{
			...mutationResult,
			action: usedAction.current
		}
	];
}