mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: James Brunton <[email protected]>
167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
/**
|
|
* Helper utilities for handling settings with pending changes that require restart.
|
|
*
|
|
* Backend returns settings in this format:
|
|
* {
|
|
* "enableLogin": false, // Current active value
|
|
* "csrfDisabled": true,
|
|
* "_pending": { // Optional - only present if there are pending changes
|
|
* "enableLogin": true // Value that will be active after restart
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
export interface SettingsWithPending<T = any> {
|
|
_pending?: Partial<T>;
|
|
[key: string]: any;
|
|
}
|
|
|
|
/**
|
|
* Merge pending changes into the settings object.
|
|
* Returns a new object with pending values overlaid on top of current values.
|
|
*
|
|
* @param settings Settings object from backend (may contain _pending block)
|
|
* @returns Merged settings with pending values applied
|
|
*/
|
|
export function mergePendingSettings<T extends SettingsWithPending>(settings: T): Omit<T, '_pending'> {
|
|
if (!settings || !settings._pending) {
|
|
// No pending changes, return as-is (without _pending property)
|
|
const { _pending, ...rest } = settings || {};
|
|
return rest as Omit<T, '_pending'>;
|
|
}
|
|
|
|
// Deep merge pending changes
|
|
const merged = deepMerge(settings, settings._pending);
|
|
|
|
// Remove _pending from result
|
|
const { _pending, ...result } = merged;
|
|
return result as Omit<T, '_pending'>;
|
|
}
|
|
|
|
/**
|
|
* Check if a specific field has a pending change awaiting restart.
|
|
*
|
|
* @param settings Settings object from backend
|
|
* @param fieldPath Dot-notation path to the field (e.g., "oauth2.clientSecret")
|
|
* @returns True if field has pending changes
|
|
*/
|
|
export function isFieldPending<T extends SettingsWithPending>(
|
|
settings: T | null | undefined,
|
|
fieldPath: string
|
|
): boolean {
|
|
if (!settings?._pending) {
|
|
return false;
|
|
}
|
|
|
|
// Navigate the pending object using dot notation
|
|
const value = getNestedValue(settings._pending, fieldPath);
|
|
return value !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if there are any pending changes in the settings.
|
|
*
|
|
* @param settings Settings object from backend
|
|
* @returns True if there are any pending changes
|
|
*/
|
|
export function hasPendingChanges<T extends SettingsWithPending>(
|
|
settings: T | null | undefined
|
|
): boolean {
|
|
return settings?._pending !== undefined && Object.keys(settings._pending).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Get the pending value for a specific field, or undefined if no pending change.
|
|
*
|
|
* @param settings Settings object from backend
|
|
* @param fieldPath Dot-notation path to the field
|
|
* @returns Pending value or undefined
|
|
*/
|
|
export function getPendingValue<T extends SettingsWithPending>(
|
|
settings: T | null | undefined,
|
|
fieldPath: string
|
|
): any {
|
|
if (!settings?._pending) {
|
|
return undefined;
|
|
}
|
|
|
|
return getNestedValue(settings._pending, fieldPath);
|
|
}
|
|
|
|
/**
|
|
* Get the current active value for a field (ignoring pending changes).
|
|
*
|
|
* @param settings Settings object from backend
|
|
* @param fieldPath Dot-notation path to the field
|
|
* @returns Current active value
|
|
*/
|
|
export function getCurrentValue<T extends SettingsWithPending>(
|
|
settings: T | null | undefined,
|
|
fieldPath: string
|
|
): any {
|
|
if (!settings) {
|
|
return undefined;
|
|
}
|
|
|
|
// Get from settings, ignoring _pending
|
|
const { _pending, ...activeSettings } = settings;
|
|
return getNestedValue(activeSettings, fieldPath);
|
|
}
|
|
|
|
// ========== Helper Functions ==========
|
|
|
|
/**
|
|
* Deep merge two objects. Second object takes priority.
|
|
*/
|
|
function deepMerge(target: any, source: any): any {
|
|
if (!source) return target;
|
|
if (!target) return source;
|
|
|
|
const result = { ...target };
|
|
|
|
for (const key in source) {
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
const sourceValue = source[key];
|
|
const targetValue = result[key];
|
|
|
|
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
|
result[key] = deepMerge(targetValue, sourceValue);
|
|
} else {
|
|
result[key] = sourceValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get nested value using dot notation.
|
|
*/
|
|
function getNestedValue(obj: any, path: string): any {
|
|
if (!obj || !path) return undefined;
|
|
|
|
const parts = path.split('.');
|
|
let current = obj;
|
|
|
|
for (const part of parts) {
|
|
if (current === null || current === undefined) {
|
|
return undefined;
|
|
}
|
|
current = current[part];
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a plain object (not array, not null, not Date, etc.)
|
|
*/
|
|
function isPlainObject(value: any): boolean {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
value.constructor === Object
|
|
);
|
|
}
|