Skip to content
Snippets Groups Projects
form.ts 10.4 KiB
Newer Older
  • Learn to ignore specific revisions
  •   CreateOperation,
      CreateOperationOptions,
    
      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,
    
    export function useCreateBehaviour<T extends APIObject, TData = Partial<Omit<T, 'id'>>>(
      create: CreateOperation<T, TData>,
    ) {
      const isSaving = ref(false)
      const error = ref<Error | undefined>()
      const { fieldErrorMap: errorMap } = useServerErrors(error)
    
      const _create: CreateOperation<T, TData> = async function (
        data,
        options?: CreateOperationOptions | undefined,
      ) {
        error.value = undefined
        isSaving.value = true
    
        try {
          return await create(data, options)
        } catch (e) {
          error.value = e instanceof Error ? e : new Error(String(e))
          throw e
        } finally {
          isSaving.value = false
        }
      }
    
      return {
        errorMap,
        isSaving,
        create: _create,
      }
    }
    
    
    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
    
      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 })