import { APIObject, ErrorDetail, ErrorMap, RetrieveMultipleOperation, RetrieveOperation, } from '@rokoli/bnb' import { ID, PartialUpdateOperation, useServerErrorFields, useServerErrors } from '@rokoli/bnb/drf' import { extendRef, watchDebounced } from '@vueuse/core' import { sort } from 'fast-sort' import { cloneDeep, isEqual } from 'lodash' import { computed, ComputedRef, MaybeRefOrGetter, reactive, readonly, Ref, ref, shallowRef, toRef, toValue, watchEffect, WritableComputedRef, } from 'vue' import { createRelationManager } from '@/api' import { PickOfType } from '@/types' const DEFAULT_DEBOUNCE_SECONDS = 0.5 type UseUpdateBehaviourResult<UpdateType> = { errorMap: ComputedRef<ErrorMap> errors: ComputedRef<ErrorDetail[]> isSaving: Readonly<Ref<boolean>> update: UpdateType } type UseUpdateBehaviourOptions< T extends APIObject, K extends keyof T, ObjectType = MaybeRefOrGetter<T>, > = { obj?: ObjectType key?: K | K[] } export function useUpdateBehaviour<T extends APIObject, K extends keyof T>( partialUpdate: PartialUpdateOperation<T, Partial<T>>, options: UseUpdateBehaviourOptions<T, K, undefined>, ): UseUpdateBehaviourResult<PartialUpdateOperation<T, Partial<T>>> export function useUpdateBehaviour<T extends APIObject, K extends keyof T>( partialUpdate: PartialUpdateOperation<T, Partial<T>>, options: UseUpdateBehaviourOptions<T, K>, ): UseUpdateBehaviourResult<(data: Partial<T> | FormData) => Promise<T>> export function useUpdateBehaviour<T extends APIObject, K extends keyof T>( partialUpdate: PartialUpdateOperation<T, Partial<T>>, options: { key?: K; obj?: MaybeRefOrGetter<T> } = {}, ) { const keys = typeof options?.key === 'string' || Array.isArray(options.key) ? [options.key] : [] const object = options?.obj const isSaving = ref(false) const error = ref<Error | undefined>() const { fieldErrorMap } = useServerErrors(error) const [errors] = useServerErrorFields(fieldErrorMap, ...keys) const _update: PartialUpdateOperation<T, Partial<T>> = async function (id, data) { error.value = undefined isSaving.value = true try { return await partialUpdate(id, data) } catch (e) { error.value = e instanceof Error ? e : new Error(String(e)) throw e } finally { isSaving.value = false } } const update = object ? (data: Partial<T> | FormData) => _update(toValue(object).id, data) : _update return { errorMap: fieldErrorMap, errors, isSaving: readonly(isSaving), update, } } function _sort<T extends { isActive?: boolean }>(items: T[], keys: (keyof T)[]): T[] { return sort(items).by([ { asc: (item) => ('isActive' in item ? (item.isActive ? 0 : 1) : 0) }, ...keys.map((key) => ({ asc: (item: T) => item[key] })), ]) } type SelectionItem = APIObject & { name?: string; isActive?: boolean } type SaveSelection<T> = (ids: T[]) => unknown type UseItemSelectionOptions<T extends SelectionItem, Save = undefined> = { save?: Save sortBy?: MaybeRefOrGetter<(keyof T)[]> debounce?: number } function useItemSelection<T extends SelectionItem, K = T['id']>( objectMap: MaybeRefOrGetter<Map<K, T>>, selectedIds: MaybeRefOrGetter<K[]>, options: UseItemSelectionOptions<T, SaveSelection<K>>, ): { value: WritableComputedRef<T[]> valueIds: WritableComputedRef<T['id'][]> choices: ComputedRef<T[]> } function useItemSelection<T extends SelectionItem, K = T['id']>( objectMap: MaybeRefOrGetter<Map<K, T>>, selectedIds: MaybeRefOrGetter<K[]>, options: UseItemSelectionOptions<T>, ): { value: ComputedRef<T[]>; valueIds: ComputedRef<T['id'][]>; choices: ComputedRef<T[]> } function useItemSelection<T extends { id: ID; name?: string; isActive?: boolean }, K = T['id']>( objectMap: MaybeRefOrGetter<Map<K, T>>, selectedIds: MaybeRefOrGetter<K[]>, options: UseItemSelectionOptions<T, SaveSelection<K> | undefined> = {}, ) { const canSave = typeof options?.save !== 'undefined' const { sortBy, ...copyOptions } = options const sortByKeysList = computed(() => toValue(sortBy ?? (['name'] as (keyof T)[]))) const choices = computed<T[]>(() => { const choices = Array.from(toValue(objectMap).values()) return _sort( choices.filter((item) => item?.isActive !== false), sortByKeysList.value, ) }) const selectedIdsCopy = useCopy(() => toValue(selectedIds), { shallow: true, ...copyOptions, }) function getSelected() { const objects = toValue(objectMap) return _sort( selectedIdsCopy.value.filter((id) => objects.has(id)).map((id) => objects.get(id) as T), sortByKeysList.value, ) } function saveSelected(value: T[]) { setIds(value.map((item) => item.id) as K[]) } function getIds() { return toValue(selectedIdsCopy) } function setIds(value: K[]) { selectedIdsCopy.value = value } const value = canSave ? computed({ get: getSelected, set: saveSelected, }) : computed(getSelected) const valueIds = canSave ? computed({ get: getIds, set: setIds }) : computed(getIds) return { value, valueIds, choices } } export function useRelationList< ObjectType extends APIObject, ItemType extends APIObject, VKey extends keyof PickOfType<ObjectType, ID[]> = keyof PickOfType<ObjectType, ID[]>, >( objectStore: { partialUpdate: PartialUpdateOperation<ObjectType, Partial<ObjectType>> }, object: MaybeRefOrGetter<ObjectType>, key: VKey, itemStore: { itemMap: Map<ItemType['id'], ItemType> retrieveMultiple: RetrieveMultipleOperation<ItemType> }, options: Omit<UseItemSelectionOptions<ItemType, ItemType['id']>, 'save'> = {}, ) { const _object = toRef(object) as Ref<ObjectType> const { update, isSaving, errors } = useUpdateBehaviour(objectStore.partialUpdate, { key }) const updateRelation = createRelationManager<ObjectType>(update, _object) function save(ids: ItemType['id'][]) { // TODO: typecheck // @ts-expect-error Investigate why key is not assignable here return updateRelation({ [key]: ids }) } const { value, valueIds, choices } = useItemSelection<ItemType>( () => itemStore.itemMap, () => _object.value[key] as ItemType['id'][], { save, ...options }, ) watchEffect(() => { void itemStore.retrieveMultiple(_object.value[key] as ItemType['id'][], { useCached: true }) }) return reactive({ value, valueIds, choices, isSaving, errors }) } export function useRelation< ObjectType extends APIObject, ItemType extends APIObject, VKey extends keyof PickOfType<ObjectType, ID | null> = keyof PickOfType<ObjectType, ID | null>, >( objectStore: { partialUpdate: PartialUpdateOperation<ObjectType, Partial<ObjectType>> }, object: MaybeRefOrGetter<ObjectType>, key: VKey, itemStore: { itemMap: Map<ItemType['id'], ItemType>; retrieve: RetrieveOperation<ItemType> }, options: Omit<UseItemSelectionOptions<ItemType, ItemType['id']>, 'save'> = {}, ) { const _object = toRef(object) as Ref<ObjectType> const { update, isSaving, errors } = useUpdateBehaviour(objectStore.partialUpdate, { obj: _object, key, }) function save(ids: ItemType['id'][]) { // TODO: typecheck // @ts-expect-error Investigate why key is not assignable here return update({ [key]: ids[0] ?? null }) } const { value: valueList, valueIds, choices, } = useItemSelection<ItemType>( () => itemStore.itemMap, () => [_object.value[key]] as ItemType['id'][], { save, ...options }, ) const value = computed<ItemType | null>({ get() { return valueList.value[0] ?? null }, set(item: ItemType | null) { valueList.value = item ? [item] : [] }, }) const valueId = computed<ItemType['id'] | null>({ get() { return value.value?.id ?? null }, set(id: ItemType['id'] | null) { valueIds.value = id ? [id] : [] }, }) watchEffect(() => { const id = _object.value[key] as ItemType['id'] if (id) void itemStore.retrieve(id, { useCached: true }) }) return reactive({ value, valueId, choices, isSaving, errors }) } type UseCopyOptions<T> = { clone?: (v: T) => T isEqual?: (v1: T, v2: T) => boolean save?: (value: T) => unknown noAutoSave?: boolean shallow?: boolean debounce?: number } export function useCopy<T>(state: MaybeRefOrGetter<T>, options: UseCopyOptions<T> = {}) { const shallow = options?.shallow ?? false const debounce = options?.debounce ?? DEFAULT_DEBOUNCE_SECONDS * 1000 const _clone = options?.clone ?? cloneDeep const _isEqual = options?.isEqual ?? isEqual const _save = options?.save const value = (shallow ? shallowRef() : ref()) as Ref<T> watchEffect(() => { value.value = _clone(toValue(state)) }) function triggerSave() { const _value = value.value if (_save && !_isEqual(_value, toValue(state))) { _save(_value) } } if (!options.noAutoSave) { watchDebounced(value, triggerSave, { deep: true, debounce }) } return extendRef(value, { triggerSave }, { enumerable: true }) } export function useAPIObjectFieldCopy<T extends APIObject, K extends keyof T = keyof T, V = T[K]>( store: { partialUpdate: PartialUpdateOperation<T, Partial<T>> }, obj: MaybeRefOrGetter<T>, key: K, options: Omit<UseCopyOptions<V>, 'save'> = {}, ) { const _object = toRef(obj) as Ref<T> const { update, isSaving, errors } = useUpdateBehaviour<T, K>(store.partialUpdate, { obj: _object, key, }) function save(value: V) { // TODO: typecheck // @ts-expect-error Investigate why key is not assignable here return update({ [key]: value }) } const state = computed<V>(() => _object.value[key] as V) const value = useCopy<V>(state, { ...options, save }) function triggerSave() { value.triggerSave() } return reactive({ value, save: triggerSave, isSaving, errors }) }