diff --git a/cors_proxy/nginx.conf b/cors_proxy/nginx.conf index 8b7bb77dcde5b533cadf0b2147b6bb5d710460c1..e5008e7a596bab4f6166a07bf5c2f03e2a9133a8 100644 --- a/cors_proxy/nginx.conf +++ b/cors_proxy/nginx.conf @@ -10,6 +10,7 @@ http { sendfile on; tcp_nopush on; tcp_nodelay on; + charset utf-8; server { listen 7000; diff --git a/src/components/OMConfigComponent.tsx b/src/components/OMConfigComponent.tsx index e8781dd4735f7fd02bf7f376deee4b0ff155de6e..2850f230dbcaac383faa2adc051097f9ecc7d019 100644 --- a/src/components/OMConfigComponent.tsx +++ b/src/components/OMConfigComponent.tsx @@ -9,7 +9,7 @@ import { useLanguage } from "./LanguageProvider"; import { basePath } from "../../basepath"; import { showError, showErrorToast } from "@/misc/ErrorHandlers"; import { deepEquals, TooltipButton } from "@/misc/Util"; -import { createOMFieldEditor } from "@/components/TypeEditor"; +import { OMFieldEditor } from "@/components/TypeEditor"; import type React from "react"; import type { @@ -41,6 +41,7 @@ export function ListOMFieldComponent({ currentValue, updateCurrentValue, inCreation = false, + disabled = false, }: { object_type: string; object_id?: int; @@ -49,6 +50,7 @@ export function ListOMFieldComponent({ currentValue?: any; updateCurrentValue: (newValue: any, isValid: boolean) => void; inCreation?: boolean; + disabled?: boolean; }) { const { language } = useLanguage(); @@ -58,15 +60,9 @@ export function ListOMFieldComponent({ }, [inCreation, field_desc, currentValue, updateCurrentValue]); let hasChanged = !inCreation && !deepEquals(initialValue, currentValue); - const editor = createOMFieldEditor(field_desc, { - value: currentValue, - updateValue: (val, isValid, affectNow) => updateCurrentValue(val, isValid), - disabled: false, - autoFocus: false, - }); return ( - <tr> + <> <td> <div className="d-flex"> <div @@ -100,9 +96,17 @@ export function ListOMFieldComponent({ </div> </td> <td className="align-middle"> - <div className="w-100 d-inline-grid">{editor}</div> + <div className="w-100 d-inline-grid"> + <OMFieldEditor + description={field_desc} + value={currentValue} + updateValue={(val, isValid, affectNow) => updateCurrentValue(val, isValid)} + disabled={disabled} + autoFocus={false} + /> + </div> </td> - </tr> + </> ); } @@ -275,22 +279,6 @@ export function EmbeddedOMFieldComponent({ description = field!.description; } let hasChanged = !deepEquals(field?.value ?? initialValue, value); - const editor = createOMFieldEditor(description, { - value: value, - updateValue: (newValue: any, isValid: boolean, affectNow: boolean) => { - setValue(newValue); - setIsValid(isValid); - if (isValid) { - if (affectNow) { - nowSendValueUpdate(newValue); - } else { - deferredSendValueUpdate(newValue); - } - } - }, - disabled: false, - autoFocus: !isInstantEdit, - }); let title = language.getOrDefault(`object.${object_type}.${field_id}`, ""); const tooltipKey = `object.${object_type}.${field_id}.tooltip`; @@ -323,7 +311,23 @@ export function EmbeddedOMFieldComponent({ style={{ color: "orange", width: "4px", verticalAlign: "baseline" }} /> )} - {editor} + <OMFieldEditor + description={description} + value={value} + updateValue={(newValue: any, isValid: boolean, affectNow: boolean) => { + setValue(newValue); + setIsValid(isValid); + if (isValid) { + if (affectNow) { + nowSendValueUpdate(newValue); + } else { + deferredSendValueUpdate(newValue); + } + } + }} + disabled={false} + autoFocus={!isInstantEdit} + /> {!isInstantEdit && ( <button type="button" @@ -339,6 +343,168 @@ export function EmbeddedOMFieldComponent({ ); } +function ConflictModal({ + object_type, + object_id, + ourUpdates, + closeClicked, + saveConflictChanges, + ...props +}: { + object_type: string; + object_id: int; + ourUpdates: { [id: string]: any }; + closeClicked: () => void; + saveConflictChanges: ( + updates: { [id: string]: any }, + expected_vals: { [id: string]: any }, + ) => void; + [key: string]: any; +}) { + const api = useBackendContext(); + const { language } = useLanguage(); + const [isLoading, setIsLoading] = useState(true); + const [serverData, setServerData] = useState<GetConfigurationResponse>(); + const [serverValues, setServerValues] = useState<{ [id: string]: [any, boolean] }>({}); + const [currentValues, setCurrentValues] = useState<{ [id: string]: [any, boolean] }>({}); + + useEffect(() => { + setIsLoading(true); + setServerData(undefined); + const vals: { [id: string]: [any, boolean] } = {}; + for (let key of Object.keys(ourUpdates)) { + vals[key] = [ourUpdates[key], true]; + } + setCurrentValues(vals); + api.getOMConfiguration(object_type, object_id) + .then((config) => { + const values: { [id: string]: [any, boolean] } = {}; + for (let field of config.fields) { + values[field.description.id] = [field.value, true]; + } + setServerData(config); + setServerValues(values); + }) + .catch((err) => { + showError(err, "Unable to load configuration"); + //closeClicked(); + }) + .then(() => { + setIsLoading(false); + }); + }, [api, object_type, object_id, ourUpdates, closeClicked]); + + const updates: { [id: string]: any } = {}; + const expectedValues: { [id: string]: any } = {}; + let isValid = true; + for (let field of serverData?.fields ?? []) { + const currentValue = currentValues[field.description.id]; + if (currentValue === undefined) continue; + if (!currentValue[1]) isValid = false; + if (deepEquals(currentValue[0], field.value)) continue; + updates[field.description.id] = currentValue[0]; + expectedValues[field.description.id] = field.value; + } + + const saveChanges = () => { + if (!isValid) return; + saveConflictChanges(updates, expectedValues); + setServerData(undefined); + }; + + return ( + <Modal {...props} onHide={closeClicked}> + <LockEditMode /> + <Modal.Header closeButton> + <Modal.Title>Konflikt</Modal.Title> + </Modal.Header> + <Modal.Body> + {isLoading && ( + <> + <div className="spinner-border me-2" role="status" /> + Lade Konfiguration + </> + )} + Server Data has changed while you were editing! + {serverData?.fields !== undefined && ( + <table className="table "> + <thead> + <tr> + <th className="col-4" /> + <th className="col-4" /> + <th className="col-4" /> + </tr> + </thead> + <tbody> + <tr> + <td className="align-middle">Field</td> + <td className="align-middle">Server version</td> + <td className="align-middle">Your version</td> + </tr> + {serverData!.fields?.map((field) => { + if (!currentValues[field.description.id]) return null; + return ( + <tr + key={field.description.id} + className={ + deepEquals( + ourUpdates[field.description.id], + field.value, + ) + ? "" + : "bg-warning-subtle" + } + > + <ListOMFieldComponent + object_type={object_type} + object_id={object_id} + field_desc={field.description} + initialValue={field.value} + currentValue={serverValues[field.description.id][0]} + updateCurrentValue={() => {}} + disabled={true} + /> + <td className="align-middle"> + <div className="w-100 d-inline-grid"> + <OMFieldEditor + description={field.description} + value={currentValues[field.description.id][0]} + updateValue={( + newValue: any, + isValid: boolean, + ) => + setCurrentValues((prev) => ({ + ...prev, + [field.description.id]: [ + newValue, + isValid, + ], + })) + } + disabled={false} + autoFocus={false} + /> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + )} + </Modal.Body> + <Modal.Footer> + <button className="btn btn-secondary" onClick={closeClicked}> + {language.get("ui.generic.cancel")} + </button> + <button className="btn btn-primary" onClick={saveChanges} disabled={!isValid}> + {language.get("ui.generic.save_changes")} + </button> + </Modal.Footer> + </Modal> + ); +} + export function OMEdit({ object_type, object_id, @@ -351,13 +517,15 @@ export function OMEdit({ const api = useBackendContext(); const reloadFunc = useReloadBoundary(); const { language } = useLanguage(); - const [showModal, setShowModal] = useState(false); + const [showConfigModal, setShowConfigModal] = useState(false); const [isLoading, setIsLoading] = useState(false); const [config, setConfig] = useState<GetConfigurationResponse>(); const [currentValues, setCurrentValues] = useState<{ [id: string]: [any, boolean] }>({}); + const [inConflictResolution, setInConflictResolution] = useState(0); + const [updatesToBeResolved, setUpdatesToBeResolved] = useState<{ [id: string]: any }>(); useEffect(() => { - if (showModal === false) return; + if (showConfigModal === false) return; setIsLoading(true); api.getOMConfiguration(object_type, object_id) .then((config) => { @@ -370,12 +538,12 @@ export function OMEdit({ }) .catch((err) => { showError(err, "Unable to load configuration"); - setShowModal(false); + setShowConfigModal(false); }) .then(() => { setIsLoading(false); }); - }, [api, object_type, object_id, showModal]); + }, [api, object_type, object_id, showConfigModal]); const updates: { [id: string]: any } = {}; const expectedValues: { [id: string]: any } = {}; @@ -389,46 +557,54 @@ export function OMEdit({ } const hasAnyChanges = Object.keys(updates).length > 0; - const saveChanges = () => { - if (!hasAnyChanges || !isValid) return; - + const sendUpdate = (updates: { [id: string]: any }, expected_vals: { [id: string]: any }) => { api.updateOMObject(object_type, object_id, { updates: updates, - expected_current_values: expectedValues, + expected_current_values: expected_vals, }) .then(() => { - setShowModal(false); + setShowConfigModal(false); setCurrentValues({}); setConfig(undefined); + setInConflictResolution(0); + setUpdatesToBeResolved(undefined); reloadFunc(); }) .catch((err) => { - showError(err, "Unable to update object"); + console.error(err, "Unable to update object"); + setShowConfigModal(false); + setInConflictResolution((x) => x + 1); + setUpdatesToBeResolved(updates); }); }; - const closeClicked = () => { + const saveChanges = () => { + if (!hasAnyChanges || !isValid) return; + sendUpdate(updates, expectedValues); + }; + + const closeConfigClicked = () => { if (!hasAnyChanges || confirm("Do you want to discard your changes?")) { - setShowModal(false); + setShowConfigModal(false); setCurrentValues({}); setConfig(undefined); } }; - const { className, ...otherProps } = props; return ( <> <button type="button" className={"btn btn-outline-primary m-1 " + (className ?? "")} - onClick={() => setShowModal(true)} + onClick={() => setShowConfigModal(true)} + disabled={showConfigModal} title="Edit" {...otherProps} > <i className="bi bi-pencil" /> </button> - <Modal show={showModal} size="xl" onHide={closeClicked}> + <Modal show={showConfigModal} size="xl" onHide={closeConfigClicked}> <LockEditMode /> <Modal.Header closeButton> <Modal.Title>Konfiguration</Modal.Title> @@ -464,20 +640,26 @@ export function OMEdit({ </thead> <tbody> {config!.fields?.map((field) => ( - <ListOMFieldComponent - key={field.description.id} - object_type={object_type} - object_id={object_id} - field_desc={field.description} - initialValue={field.value} - currentValue={currentValues[field.description.id][0]} - updateCurrentValue={(newValue: any, isValid: boolean) => - setCurrentValues((prev) => ({ - ...prev, - [field.description.id]: [newValue, isValid], - })) - } - /> + <tr key={field.description.id}> + <ListOMFieldComponent + object_type={object_type} + object_id={object_id} + field_desc={field.description} + initialValue={field.value} + currentValue={ + currentValues[field.description.id][0] + } + updateCurrentValue={( + newValue: any, + isValid: boolean, + ) => + setCurrentValues((prev) => ({ + ...prev, + [field.description.id]: [newValue, isValid], + })) + } + /> + </tr> ))} </tbody> </table> @@ -485,7 +667,7 @@ export function OMEdit({ )} </Modal.Body> <Modal.Footer> - <button className="btn btn-secondary" onClick={closeClicked}> + <button className="btn btn-secondary" onClick={closeConfigClicked}> {language.get("ui.generic.cancel")} </button> <button @@ -497,6 +679,19 @@ export function OMEdit({ </button> </Modal.Footer> </Modal> + + {inConflictResolution != 0 && ( + <ConflictModal + key={inConflictResolution} + show={inConflictResolution != 0} + size="xl" + saveConflictChanges={sendUpdate} + object_type={object_type} + object_id={object_id} + ourUpdates={updatesToBeResolved!} + closeClicked={() => setInConflictResolution(0)} + /> + )} </> ); } @@ -642,19 +837,23 @@ export function OMCreate({ </thead> <tbody> {config.fields.map((field_desc) => ( - <ListOMFieldComponent - key={field_desc.id} - object_type={object_type} - field_desc={field_desc} - currentValue={currentValues[field_desc.id][0]} - updateCurrentValue={(newValue: any, isValid: boolean) => - setCurrentValues((prev) => ({ - ...prev, - [field_desc.id]: [newValue, isValid], - })) - } - inCreation={true} - /> + <tr key={field_desc.id}> + <ListOMFieldComponent + object_type={object_type} + field_desc={field_desc} + currentValue={currentValues[field_desc.id][0]} + updateCurrentValue={( + newValue: any, + isValid: boolean, + ) => + setCurrentValues((prev) => ({ + ...prev, + [field_desc.id]: [newValue, isValid], + })) + } + inCreation={true} + /> + </tr> ))} </tbody> </table> @@ -679,19 +878,20 @@ export function OMCreate({ )} {chosenVariant && config?.variant_fields![chosenVariant].map((field_desc) => ( - <ListOMFieldComponent - key={field_desc.id} - object_type={object_type} - field_desc={field_desc} - currentValue={currentValues[field_desc.id][0]} - updateCurrentValue={(newValue: any, isValid: boolean) => - setCurrentValues((prev) => ({ - ...prev, - [field_desc.id]: [newValue, isValid], - })) - } - inCreation={true} - /> + <tr key={field_desc.id}> + <ListOMFieldComponent + object_type={object_type} + field_desc={field_desc} + currentValue={currentValues[field_desc.id][0]} + updateCurrentValue={(newValue: any, isValid: boolean) => + setCurrentValues((prev) => ({ + ...prev, + [field_desc.id]: [newValue, isValid], + })) + } + inCreation={true} + /> + </tr> ))} </Modal.Body> <Modal.Footer> diff --git a/src/components/TypeEditor.tsx b/src/components/TypeEditor.tsx index 18eafe008d749ed0e7cf7d87d09d8aad996b6fb0..698d5e38dfc6c54ed7df72b2c3d1eac547754029 100644 --- a/src/components/TypeEditor.tsx +++ b/src/components/TypeEditor.tsx @@ -23,7 +23,12 @@ type EditorArgs = { hideErrors?: boolean; }; -export function createOMFieldEditor(description: field_description, args: EditorArgs) { +export function OMFieldEditor({ + description, + ...args +}: { + description: field_description; +} & EditorArgs) { const { value } = args; switch (description.type) { case "string":