Source

client/core/modules/selection.js

/**
 * 
 * ## A simple selection manager
 * 
 * - keeps track of selected records for a specific view
 * - allow simple operations like add/delete/get/reset
 * - store in a localStorage object, which key is the viewId
 * - works in combination with datatables or other data components with selection capabilities
 * 
 * @namespace
 * 
 */
kiss.selection = {
    /**
     * Insert one record into the view selection
     * 
     * @param {string} viewId 
     * @param {string} recordId 
     */
    insertOne(viewId, recordId) {
        let selection = localStorage.getItem("config-selection-" + viewId)
        if (!selection) {
            localStorage.setItem("config-selection-" + viewId, recordId)
            return
        }

        let records = selection.split(",")
        if (records.indexOf(recordId) != -1) return

        localStorage.setItem("config-selection-" + viewId, selection + "," + recordId)
    },

    /**
     * Insert many records into the view selection
     * 
     * @param {string} viewId 
     * @param {string[]} recordIds
     */    
    insertMany(viewId, recordIds) {
        recordIds.forEach(recordId => {
            kiss.selection.insertOne(viewId, recordId)
        })
    },

    /**
     * Delete a record from the view selection
     * 
     * @param {string} viewId 
     * @param {string} recordId 
     */    
    delete(viewId, recordId) {
        let selection = localStorage.getItem("config-selection-" + viewId)
        if (!selection) return

        let records = selection.split(",").remove(recordId)
        records = records.filter(recordId => recordId != "")
        localStorage.setItem("config-selection-" + viewId, records.join(","))
    },

    /**
     * Reset the selection of a view
     * 
     * @param {string} viewId 
     */       
    reset(viewId) {
        let selection = localStorage.getItem("config-selection-" + viewId)
        if (!selection) return

        localStorage.removeItem("config-selection-" + viewId)
    },    

    /**
     * Get the current selection for a view
     * 
     * @param {string} viewId
     * @returns {string[]} The list of ids of the selected records
     */    
    get(viewId) {
        let selection = localStorage.getItem("config-selection-" + viewId)
        if (!selection) return []

        return selection.split(",")
    },

    /**
     * Get the current selection for the active view
     * 
     * @param {string} viewId
     * @returns {string[]} The list of ids of the selected records
     */    
    getFromActiveView() {
        const viewId = kiss.context.viewId
        if (!viewId) return []
        let selection = localStorage.getItem("config-selection-" + viewId)
        if (!selection) return []

        return selection.split(",")
    },    

    /**
     * Get the selected records in a view
     * 
     * @param {string} viewId
     * @returns {object[]} The list of selected records
     */      
    async getRecords(viewId) {
        const recordIds = kiss.selection.get(viewId)
        const viewCollection = $(viewId).collection
        return await viewCollection.findById(recordIds)
    },

    /**
     * Get the selected records in the active view
     * 
     * @returns {object[]} The list of selected records
     */
    async getRecordsFromActiveView() {
        const viewId = kiss.context.viewId
        return await kiss.selection.getRecords(viewId)
    },

    /**
     * Opens a window to batch update the selected records
     */
    updateSelectedRecords() {
        const ids = kiss.selection.getFromActiveView()
        if (ids.length == 0) return createNotification(txtTitleCase("#no selection"))

        const model = kiss.app.models[kiss.context.modelId]
        if (!model) return

        const fields = model.getBatchableFields()

        createPanel({
            id: "selection-batch-update",
            title: txtTitleCase("update selected documents"),
            modal: true,
            closable: true,
            draggable: true,
            icon: "fas fa-bolt",
            align: "center",
            verticalAlign: "center",
            layout: "vertical",
            width: 400,


            items: [
                // Field to update
                {
                    id: "batch-field",
                    type: "select",
                    label: txtTitleCase("field to update"),
                    labelPosition: "top",
                    width: "100%",
                    options: fields.map(field => {
                        return {
                            value: field.id,
                            label: field.label
                        }
                    }),
                    events: {
                        change: function() {
                            const fieldId = this.getValue()
                            $("selection-batch-update").updateInput(fieldId)
                        }
                    }
                },
                // Value to set (will be replaced by the input field)
                {
                    id: "batch-value-container",
                },
                // Button to update the records
                {
                    hidden: true,
                    id: "batch-update-button",
                    type: "button",
                    text: txtTitleCase("update"),
                    icon: "fas fa-bolt",
                    iconColor: "var(--yellow)",
                    margin: "5px 5px 0 5px",
                    action: () => $("selection-batch-update").updateRecords(ids)
                }
            ],

            methods: {
                updateRecords(ids) {
                    // Warn the user that the operation is irreversible
                    createDialog({
                        title: txtTitleCase("update selected documents"),
                        type: "danger",
                        buttonOKPosition: "left",
                        message: txtTitleCase("#warning update docs", null, {
                            n: ids.length
                        }),
                        action: async () => {
                            const fieldId = $("batch-field").getValue()
                            const value = $("batch-value").getValue()

                            if (value == "") {
                                // Warn the user that the value is empty
                                createDialog({
                                    title: txtTitleCase("empty value"),
                                    type: "danger",
                                    buttonOKPosition: "left",
                                    message: txtTitleCase("#warning empty value"),
                                    action: async () => {
                                        await kiss.selection._updateRecords(fieldId, value)
                                        $("selection-batch-update").close()
                                    }
                                })  
                            }
                            else {
                                kiss.selection._updateRecords(fieldId, value)
                                $("selection-batch-update").close()
                            }

                            const viewId = kiss.context.viewId
                            $(viewId).deselectAll()
                        }
                    })
                },

                /**
                 * Update the input field according to the selected field
                 * 
                 * @param {string} fieldId 
                 */
                updateInput(fieldId) {
                    let currentField = $("batch-value")
                    if (currentField) currentField.deepDelete()

                    const fieldConfig = this.buildInput(fieldId)
                    let input = fieldConfig.renderer(fieldConfig)
                    input.render()

                    $("batch-update-button").show()
                },

                /**
                 * Build an input field according to the selected field
                 * 
                 * @param {string} fieldId 
                 * @returns {object} The input field configuration
                 */
                buildInput(fieldId) {
                    const fieldConfig = fields.find(field => field.id == fieldId)
                    let fieldType = fieldConfig.type
                    let fieldBuilderFunction = createField
                    let allowValuesNotInList = true
                    let iconColorOn
                    let checked = false
                    let options = []
                    let optionsFilter
                    let shape = ""
                    let roles = []
                    let multiple
                    let unit
                    let min
                    let max
                    let rows
            
                    switch (fieldType) {
                        case "textarea":
                        case "aiTextarea":
                            fieldType = "textarea"
                            rows = 5
                            break
                        case "select":
                            options = fieldConfig.options
                            optionsFilter = fieldConfig.optionsFilter
                            multiple = fieldConfig.multiple
                            allowValuesNotInList = fieldConfig.allowValuesNotInList
                            fieldBuilderFunction = createSelect
                            break
                        case "checkbox":
                            shape = fieldConfig.shape
                            iconColorOn = fieldConfig.iconColorOn
                            fieldBuilderFunction = createCheckbox
                            break
                        case "slider":
                            min = fieldConfig.min || 0
                            max = fieldConfig.max || 100
                            fieldBuilderFunction = createSlider
                            break
                        case "rating":
                            shape = fieldConfig.shape || "star"
                            min = fieldConfig.min || 0
                            max = fieldConfig.max || 10
                            iconColorOn = fieldConfig.iconColorOn
                            fieldBuilderFunction = createRating
                            break
                        case "color":
                            fieldBuilderFunction = createColorField
                            break
                        case "icon":
                            fieldBuilderFunction = createIconField
                            break
                        case "directory":
                            roles = ["userId"]
                            multiple = fieldConfig.multiple
                            allowValuesNotInList = false
                            fieldBuilderFunction = createDirectory
                            break
                    }
            
                    // Create an input field configuration with the right type
                    return {
                        id: "batch-value",
                        target: "batch-value-container",
                        type: fieldType,
                        label: txtTitleCase("new field value"),
                        labelPosition: "top",
                        width: "100%",

                        // Field options
                        rows,
                        min,
                        max,
                        unit,
                        checked,
                        multiple,
                        allowValuesNotInList,
            
                        // Special fields options
                        renderer: fieldBuilderFunction, // renderer
                        options, // select
                        optionsFilter, // select
                        optionsColor: model.color,
                        roles, // directory
                        shape, // checkbox
                        iconColorOn, // checkbox
                        iconSize: "20px", // checkbox
                    }
                }
            }
        }).render()
    },

    /**
     * Private utility function to update records from a view
     * 
     * @private
     * @ignore
     * @param {string} fieldLabel 
     * @param {*} value 
     */
    async _updateRecords(fieldLabel, value) {
        const model = kiss.app.models[kiss.context.modelId]
        if (!model) return

        const field = model.getFieldByLabel(fieldLabel)
        if (!field) return

        const viewId = kiss.context.viewId
        if (!$(viewId)) return

        const recordIds = kiss.selection.get(viewId)
        const viewCollection = $(viewId).collection
        const selectedRecords = await viewCollection.findById(recordIds)
    
        // Prevent the view from refreshing while batch updates are in progress
        kiss.global.preventViewRefresh = true
        
        const loadingId = kiss.loadingSpinner.show()
        let counter = 0

        for (let record of selectedRecords) {
            await record.update({
                [field.id]: value
            })
            
            counter++
            createNotification(counter + " / " + selectedRecords.length)

            // Refresh the view when the last record has been updated
            if (counter == selectedRecords.length - 1) {
                kiss.global.preventViewRefresh = false
            }
        }
        kiss.loadingSpinner.hide(loadingId)
    },

    /**
     * Delete the selected records
     */
    deleteSelectedRecords() {
        const model = kiss.app.models[kiss.context.modelId]
        if (!model) return

        const ids = this.getFromActiveView()
        if (ids.length == 0) return createNotification(txtTitleCase("#no selection"))

        createDialog({
            title: txtTitleCase("delete selected documents"),
            type: "danger",
            buttonOKPosition: "left",
            message: txtTitleCase("#warning delete docs", null, {
                n: ids.length
            }),
            action: async () => {
                await kiss.db.deleteMany(model.id, {
                    _id: {
                        "$in": ids
                    }
                }, true)

                const viewId = kiss.context.viewId
                $(viewId).deselectAll()
            }
        })
    }    
}

;