All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ducks.datasource.store.ts Maven / Gradle / Ivy

The newest version!
import { createSlice } from '@reduxjs/toolkit'
import { isEmpty, omit } from 'lodash'
import merge from 'deepmerge'

import { ModelPrefix, SortDirection } from '../../core/datasource/const'
import { Meta } from '../../sagas/types'
import { ValidationsKey } from '../../core/validation/types'
import { Meta as N2OMeta } from '../Action'
import { removeFieldFromArray } from '../models/store'
import { RemoveFieldFromArrayAction } from '../models/Actions'

import type {
    AddComponentAction,
    ChangePageAction,
    ChangeSizeAction,
    DataRequestAction,
    FailRequestAction,
    FailValidateAction,
    ResetDatasourceAction,
    RegisterAction,
    RemoveAction,
    RemoveComponentAction,
    ResolveRequestAction,
    SetAdditionalInfoAction,
    SetFieldSubmitAction,
    SetSortDirectionAction,
    StartValidateAction,
    SubmitAction,
    UpdatePagingAction,
} from './Actions'
import type { DataSourceState } from './DataSource'
import { DataSource } from './DataSource'
import {
    Provider,
    QueryResult,
    ProviderType,
    SubmitProvider,
    Paging,
} from './Provider'

export const initialState: Record = {}

export const datasource = createSlice({
    name: 'n2o/datasource',
    initialState,
    reducers: {
        register: {
            prepare(id: string, initProps: DataSourceState) {
                return ({
                    type: '',
                    payload: { id, initProps },
                })
            },
            reducer(state, action: RegisterAction) {
                const { id, initProps } = action.payload
                const { provider: propsProvider } = initProps
                let provider

                if (propsProvider) {
                    provider = {
                        ...propsProvider,
                        type: propsProvider.type || ProviderType.service,
                    }
                }

                const defaultState = state[id] || DataSource.defaultState

                const datasource = { ...merge(defaultState, initProps), provider }

                state[id] = datasource
            },
        },

        remove: {
            prepare(id: string) {
                return ({
                    payload: { id },
                })
            },
            reducer(state, action: RemoveAction) {
                delete state[action.payload.id]
            },
        },

        addComponent: {
            prepare(id: string, componentId: string) {
                return ({
                    payload: { id, componentId },
                })
            },

            reducer(state, action: AddComponentAction) {
                const { id, componentId } = action.payload

                const datasource = state[id] || DataSource.defaultState // fixme добавление виджета не должно быть до его регистрации

                if (datasource.components.includes(componentId)) { return }

                state[id] = {
                    ...datasource,
                    components: [...datasource.components, componentId],
                }
            },
        },

        removeComponent: {
            // eslint-disable-next-line sonarjs/no-identical-functions
            prepare(id: string, componentId: string) {
                return ({
                    payload: { id, componentId },
                })
            },

            reducer(state, action: RemoveComponentAction) {
                const { id, componentId } = action.payload

                const datasource = state[id]

                // После закрытия оверлея удаление компонента из ds может прилететь позже удаления самого ds
                if (datasource) {
                    state[id] = {
                        ...datasource,
                        components: datasource.components.filter(idFromDataSource => idFromDataSource !== componentId),
                    }
                }
            },
        },

        dataRequest: {
            prepare(id: string, options: Partial<{ size: number, page: number }> = {}, meta: N2OMeta = {}) {
                return ({
                    payload: { id, options },
                    meta,
                })
            },
            reducer(state, action: DataRequestAction) {
                const { id } = action.payload

                if (!state[id]) {
                    // eslint-disable-next-line no-console
                    console.warn(`Attempt to request data from a non-existent source: ${id}`)

                    return
                }

                delete state[id].error

                state[id].loading = true
            },
        },

        resolveRequest: {
            prepare(id: string, json: QueryResult) {
                return ({
                    payload: { id, query: json },
                    meta: json.meta,
                })
            },
            reducer(state, action: ResolveRequestAction) {
                const { id, query } = action.payload

                state[id].loading = false
                state[id].paging = {
                    ...state[id].paging,
                    ...query.paging,
                }
            },
        },

        rejectRequest: {
            prepare(id: string, error?, meta?) {
                return ({
                    payload: { id, error },
                    meta,
                })
            },
            reducer(state, action: FailRequestAction) {
                const { id, error } = action.payload

                if (!state[id]) { return }

                state[id].loading = false
                state[id].error = error
            },
        },

        setSorting: {
            prepare(id: string, field: string, direction: SortDirection) {
                return ({
                    payload: { id, field, direction },
                })
            },
            reducer(state, action: SetSortDirectionAction) {
                const { id, field, direction } = action.payload

                if (direction === SortDirection.none) {
                    state[id].sorting = {}
                } else {
                    state[id].sorting = { [field]: direction }
                }
            },
        },

        setAdditionalInfo: {
            prepare(id: string, additionalInfo: object) {
                return ({
                    payload: {
                        id,
                        additionalInfo,
                    },
                })
            },

            reducer(state, action: SetAdditionalInfoAction) {
                const { id, additionalInfo } = action.payload

                state[id].additionalInfo = additionalInfo
            },
        },

        updatePaging: {
            prepare(id: string, paging: Partial) {
                return {
                    payload: { id, paging },
                }
            },
            reducer(state, { payload }: UpdatePagingAction) {
                const { id, paging } = payload

                state[id].paging = {
                    ...state[id].paging,
                    ...paging,
                }
            },
        },

        changePage: {
            prepare(id: string, page: number, options?: object) {
                return ({
                    payload: { id, page, options },
                })
            },
            reducer(state, action: ChangePageAction) {
                const { id, page } = action.payload

                state[id].paging.page = page
            },
        },

        changeSize: {
            prepare(id: string, size: number) {
                return ({
                    payload: { id, size },
                })
            },
            reducer(state, action: ChangeSizeAction) {
                const { id, size } = action.payload

                state[id].paging.size = size
            },
        },

        startValidate: {
            prepare(
                id: string,
                validationsKey = ValidationsKey.Validations,
                prefix = ModelPrefix.active,
                fields?: [],
                meta = {},
            ) {
                return ({
                    payload: { id, validationsKey, prefix, fields },
                    meta,
                })
            },
            reducer(state, action: StartValidateAction) {
                // empty reducer, action for saga
            },
        },

        failValidate: {
            prepare(id, fields, prefix = ModelPrefix.active, meta = {}) {
                return ({
                    payload: { id, fields, prefix },
                    meta,
                })
            },
            reducer(state, action: FailValidateAction) {
                const { id, fields, prefix } = action.payload
                const datasource = state[id]

                datasource.errors[prefix] = {
                    ...datasource.errors[prefix],
                    ...fields,
                }
            },
        },

        reset: {
            // eslint-disable-next-line sonarjs/no-identical-functions
            prepare(id: string) {
                return ({
                    payload: { id },
                })
            },

            reducer(state, action: ResetDatasourceAction) {
                const { id } = action.payload
                const datasource = state[id]

                // reset pagination to default
                datasource.paging.page = 1
                datasource.paging.count = 0

                // reset all errors
                Object.values(ModelPrefix).forEach((prefix) => {
                    const model = datasource.errors[prefix]

                    if (model) {
                        datasource.errors[prefix] = {}
                    }
                })
            },
        },

        resetValidation: {
            // @ts-ignore поправить типы
            prepare(id, fields, prefix = ModelPrefix.active) {
                return ({
                    payload: { id, fields, prefix },
                })
            },
            reducer(state, action: StartValidateAction) {
                const { id, fields = [], prefix } = action.payload
                const datasource = state[id]

                if (datasource) {
                    datasource.errors[prefix] = isEmpty(fields)
                        ? {}
                        : omit(datasource.errors[prefix], fields)
                }
            },
        },

        setFieldSubmit: {
            prepare(id: string, field: string, provider: Provider) {
                return ({
                    payload: { id, field, provider },
                })
            },
            reducer(state, action: SetFieldSubmitAction) {
                const { id, field, provider: propsProvider } = action.payload
                const datasource = state[id]

                datasource.fieldsSubmit[field] = {
                    ...propsProvider,
                    // @ts-ignore FIXME разобраться с типизацией
                    //  (непонятно почему может быть разный type, если ожидается строго service)
                    type: propsProvider.type || ProviderType.service,
                }
            },
        },
        // eslint-disable-next-line @typescript-eslint/naming-convention
        DATA_REQUEST: {
            prepare(datasource: string, options = {}, meta: N2OMeta = {}) {
                return ({
                    payload: { datasource, options },
                    meta,
                })
            },
            reducer(state, action: { payload: { datasource: string, options: unknown } }) {
                // Экшен приходит с сервера. В сагах надо сделать просто ремап
            },
        },

        submit: {
            prepare(id: string, provider?: SubmitProvider, meta = {}) {
                return ({
                    payload: { id, provider },
                    meta,
                })
            },
            reducer(state, action: SubmitAction) {
                // empty reducer, action for saga
            },
        },

        submitSuccess: {
            prepare(meta?: Meta) {
                return ({
                    payload: {},
                    meta,
                })
            },
            reducer() {
                // empty reducer, action for saga
            },
        },

        submitFail: {
            prepare(error: unknown, meta?: Meta) {
                return ({
                    payload: error,
                    meta,
                })
            },
            reducer() {
                // empty reducer, action for saga
            },
        },
    },
    extraReducers: {
        [removeFieldFromArray.type](state, action: RemoveFieldFromArrayAction) {
            const { key, field, start, end = 1, prefix } = action.payload
            const datasource = state[key]
            const errors: Partial = {}

            const mask = new RegExp(`${field}\\[(\\d+)]\\.(.+)`)

            for (const [key, messages] of Object.entries(datasource.errors[prefix] || {})) {
                const match = key.match(mask)

                if (match) {
                    const [, i, name] = match
                    const matchedIndex = Number(i)

                    // index before removed elements
                    if (matchedIndex < start) {
                        errors[key] = messages

                        // eslint-disable-next-line no-continue
                        continue
                    }

                    // removed elements: ignore it
                    if ((matchedIndex >= start) && (matchedIndex < start + end)) {
                        // eslint-disable-next-line no-continue
                        continue
                    }

                    // after removed: shift index
                    const newIndex = matchedIndex - end

                    errors[`${field}[${newIndex}].${name}`] = messages
                } else {
                    // not multi-set fields
                    errors[key] = messages
                }
            }

            datasource.errors[prefix] = errors
        },
    },
})

export default datasource.reducer

// Actions
export const {
    register,
    remove,
    dataRequest,
    resolveRequest,
    rejectRequest,
    setSorting,
    setAdditionalInfo,
    startValidate,
    resetValidation,
    reset,
    failValidate,
    changePage,
    changeSize,
    addComponent,
    removeComponent,
    setFieldSubmit,
    DATA_REQUEST,
    submit,
    submitSuccess,
    submitFail,
    updatePaging,
} = datasource.actions




© 2015 - 2024 Weber Informatics LLC | Privacy Policy