Files
nimbus/resources/js/composables/ui/useKeyValueParameters.ts
Mazen Touati c2aa6895d6 feat: initial alpha release
This commit represents the complete foundational codebase for Nimbus Alpha, a Laravel package that provides an integrated, in-browser API client with automatic schema discovery from validation rules.

IMPORTANT: This is a squashed commit representing the culmination of extensive development, refactoring, and architectural iterations. All previous commit history has been intentionally removed to provide a clean foundation for the public alpha release.

The development of Nimbus involved:
- Multiple architectural refactorings
- Significant structural changes
- Experimental approaches that were later abandoned
- Learning iterations on the core concept
- Migration between different design patterns

This messy history would:
- Make git blame confusing and unhelpful
- Obscure the actual intent behind current implementation
- Create noise when reviewing changes
- Reference deleted or refactored code

If git blame brought you to this commit, it means you're looking at code that was part of the initial alpha release. Here's what to do:

1. Check Current Documentation
   - See `/wiki/contribution-guide/README.md` for architecture details
   - Review the specific module's README if available
   - Look for inline comments explaining the reasoning

2. Look for Related Code
   - Check other files in the same module
   - Look for tests that demonstrate intended behavior
   - Review interfaces and contracts

3. Context Matters
   - This code may have been updated since alpha
   - Check git log for subsequent changes to this file
   - Look for related issues or PRs on GitHub

---

This commit marks the beginning of Nimbus's public journey. All future
commits will build upon this foundation with clear, traceable history.

Thank you for using or contributing to Nimbus!
2025-10-23 00:16:28 +02:00

399 lines
12 KiB
TypeScript

import { keyValueParametersConfig } from '@/config';
import { ExtendedParameter, ParametersExternalContract } from '@/interfaces/ui';
import { useCounter, watchDebounced } from '@vueuse/core';
import { computed, onBeforeMount, reactive, ref, Ref } from 'vue';
/**
* Manages key-value parameter state.
*/
export function useKeyValueParameters(model: Ref<ParametersExternalContract[]>) {
const { count: nextParameterId, inc: incrementParametersId } = useCounter();
const parameters = ref<ExtendedParameter[]>([]);
const isUpdatingFromParentModel = ref(false);
const createParameterSkeleton = (id: number): ExtendedParameter => ({
type: 'text',
id,
key: '',
value: '',
enabled: true,
});
const convertExternalToInternal = (
external: ParametersExternalContract,
id: number,
): ExtendedParameter => ({
type: external.type ?? 'text',
id,
key: external.key,
value: String(external.value ?? ''),
enabled: true,
});
/**
* Converts internal UI parameter data back to external format.
*
* Removes UI-specific properties to provide clean data for external clients.
*/
const convertInternalToExternal = (
internal: ExtendedParameter,
): ParametersExternalContract => ({
type: internal.type,
key: internal.key,
value: internal.value,
});
/**
* Creates a Map for efficient parameter lookup by key.
*
* Used for O(1) duplicate detection instead of O(n²) nested loops.
*/
const createParameterKeyMap = (
parameters: ExtendedParameter[],
): Map<string, ExtendedParameter> => {
return new Map(parameters.map(parameter => [parameter.key, parameter]));
};
/**
* Finds duplicate parameters efficiently using Map lookup.
*
* Returns parameters from incoming array that have keys matching existing parameters.
*/
const findDuplicateParameters = (
existing: ExtendedParameter[],
incoming: ExtendedParameter[],
): ExtendedParameter[] => {
const existingKeyMap = createParameterKeyMap(existing);
return incoming.filter(param => existingKeyMap.has(param.key));
};
/**
* Removes parameters with duplicate keys from existing array.
*
* Filters out parameters whose keys exist in the duplicates array.
*/
const removeDuplicateParameters = (
existing: ExtendedParameter[],
duplicates: ExtendedParameter[],
): ExtendedParameter[] => {
const duplicateKeys = new Set(duplicates.map(param => param.key));
return existing.filter(param => !duplicateKeys.has(param.key));
};
/*
* Deletion state management.
*/
interface DeletionState {
deleting: boolean;
timeoutId?: number;
}
const deletionStatesForParameters = reactive(new Map<number, DeletionState>());
const bulkDeletionState: Ref<DeletionState> = ref({ deleting: false });
/**
* Initiates deletion confirmation for a parameter.
* Returns true if this is the confirmation click (second click).
*/
const initiateParameterDeletion = (parameterId: number): boolean => {
const state = deletionStatesForParameters.get(parameterId);
if (state?.deleting) {
clearParameterDeletionState(parameterId);
return true;
}
setParameterDeletionState(parameterId);
return false;
};
/**
* Checks if a parameter is marked for deletion.
*/
const isParameterMarkedForDeletion = (parameterId: number): boolean => {
return deletionStatesForParameters.get(parameterId)?.deleting ?? false;
};
/**
* Clears deletion state for a parameter.
*/
const clearParameterDeletionState = (parameterId: number): void => {
const state = deletionStatesForParameters.get(parameterId);
if (state?.timeoutId) {
clearTimeout(state.timeoutId);
}
deletionStatesForParameters.delete(parameterId);
};
/**
* Sets deletion state with automatic timeout.
*/
const setParameterDeletionState = (parameterId: number): void => {
const timeoutId: number = window.setTimeout(() => {
deletionStatesForParameters.delete(parameterId);
}, keyValueParametersConfig.DELETION_CONFIRMATION_TIMEOUT);
deletionStatesForParameters.set(parameterId, {
deleting: true,
timeoutId,
});
};
/**
* Initiates bulk deletion confirmation.
* Returns true if this is the confirmation click (second click).
*/
const initiateBulkDeletion = (): boolean => {
if (bulkDeletionState.value.deleting) {
clearBulkDeletionState();
return true;
}
markForBulkDeletion();
return false;
};
/**
* Checks if bulk deletion is marked for confirmation.
*/
const isBulkDeletionMarked = (): boolean => {
return bulkDeletionState.value.deleting;
};
/**
* Clears bulk deletion state.
*/
const clearBulkDeletionState = (): void => {
if (bulkDeletionState.value.timeoutId) {
clearTimeout(bulkDeletionState.value.timeoutId);
}
bulkDeletionState.value = { deleting: false };
};
/**
* Sets bulk deletion state with automatic timeout.
*/
const markForBulkDeletion = (): void => {
const timeoutId = window.setTimeout(() => {
bulkDeletionState.value = { deleting: false };
}, keyValueParametersConfig.DELETION_CONFIRMATION_TIMEOUT);
bulkDeletionState.value = { deleting: true, timeoutId };
};
/**
* Clears all deletion states.
*/
const clearAllDeletionStates = (): void => {
deletionStatesForParameters.forEach((_, id) => clearParameterDeletionState(id));
clearBulkDeletionState();
};
/*
* Computed.
*/
const areAllParametersDisabled = computed(() =>
parameters.value.every(parameter => !parameter.enabled),
);
const deletingAll = computed(() => isBulkDeletionMarked());
// Sync changes back to parent model with debouncing to avoid excessive updates.
// Skip syncing when we are applying updates that originate from the parent model.
watchDebounced(
parameters,
() => {
if (isUpdatingFromParentModel.value) {
return;
}
syncParametersBackToModel();
},
{
deep: true,
debounce: keyValueParametersConfig.SYNC_DEBOUNCE_DELAY,
},
);
// Initialize parameters from parent model and ensure at least one empty parameter exists
onBeforeMount(() => {
updateParametersFromParentModel();
addNewEmptyParameter();
});
/*
* Actions.
*/
/**
* Expands minimal external parameters to full internal structure for command support.
*
* Bridges external client data with internal parameter state by adding UI-specific
* properties like IDs, enabled state, and deletion tracking.
*/
const expandExternalParameters = (
externalParameters: ParametersExternalContract[],
): ExtendedParameter[] => {
return externalParameters.map(
(externalEntity: ParametersExternalContract): ExtendedParameter => {
incrementParametersId();
return convertExternalToInternal(externalEntity, nextParameterId.value);
},
);
};
/**
* Merges new parameters with existing ones, removing duplicates
*/
const mergeParametersWithoutDuplicates = (
newParameters: ExtendedParameter[],
existingParameters: ExtendedParameter[],
): ExtendedParameter[] => {
const duplicates = findDuplicateParameters(existingParameters, newParameters);
const cleanedExisting = removeDuplicateParameters(existingParameters, duplicates);
return [...newParameters, ...cleanedExisting];
};
/**
* Syncs parameters from parent model back to the component while preserving existing parameters.
*
* Parent parameters override existing ones with matching keys to prevent duplicates,
* but we preserve user-added parameters that don't conflict.
*/
const updateParametersFromParentModel = (): void => {
if (!model.value?.length) {
return;
}
// Suppress outbound sync while we incorporate parent-provided parameters to prevent
// a feedback loop where our update triggers a debounced write back to the parent.
isUpdatingFromParentModel.value = true;
const newParameters = expandExternalParameters(model.value);
parameters.value = mergeParametersWithoutDuplicates(
newParameters,
parameters.value,
);
// Clear the suppression after the debounce window to ensure no stale writes occur.
window.setTimeout(() => {
isUpdatingFromParentModel.value = false;
}, keyValueParametersConfig.SYNC_DEBOUNCE_DELAY + 10);
};
/**
* Filters out empty keys and disabled items before syncing to the parent model.
*
* Ensures parent components receive only valid, enabled key-value pairs without UI-specific state.
*/
const syncParametersBackToModel = (): void => {
model.value = parameters.value
.filter(parameter => parameter.key !== '' && parameter.enabled)
.map(convertInternalToExternal);
};
/**
* Adds a new empty parameter to the list for user input.
*/
const addNewEmptyParameter = (): void => {
incrementParametersId();
const newParameter = createParameterSkeleton(nextParameterId.value);
parameters.value.push(newParameter);
};
/**
* Toggles all parameters between enabled/disabled states.
*/
const toggleAllParametersEnabledState = () => {
const shouldEnableAll = areAllParametersDisabled.value;
parameters.value.forEach(
(parameter: ExtendedParameter) => (parameter.enabled = shouldEnableAll),
);
};
/**
* Handles two-step deletion confirmation for individual parameters.
*
* Prevents accidental deletions by requiring a second click within the configured timeout.
* First click marks for deletion, second click removes immediately.
*/
const triggerParameterDeletion = (
parameters: ExtendedParameter[],
index: number,
): void => {
const parameter = parameters[index];
if (!parameter) {
return;
}
const shouldDelete = initiateParameterDeletion(parameter.id);
if (!shouldDelete) {
return;
}
parameters.splice(index, 1);
};
/**
* Handles two-step deletion confirmation for all parameters.
*
* Prevents accidental bulk deletion by requiring confirmation within the configured timeout.
*/
const deleteAllParameters = (): void => {
const shouldDelete = initiateBulkDeletion();
if (!shouldDelete) {
return;
}
parameters.value = [];
};
/**
* Checks if a parameter is marked for deletion
*/
const checkParameterDeletion = (id: number): boolean => {
return isParameterMarkedForDeletion(id);
};
return {
// State
parameters,
deletingAll,
areAllParametersDisabled,
// Actions
addNewEmptyParameter,
toggleAllParametersEnabledState,
triggerParameterDeletion,
deleteAllParameters,
updateParametersFromParentModel,
// Utilities
isParameterMarkedForDeletion: checkParameterDeletion,
clearAllDeletionStates,
};
}