ford.lambert / xa_designer_overlay

// ==UserScript==
// @name xa_designer_overlay
// @description Overlay with quality of life improvements
// @version  0.99.992
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @require https://raw.githubusercontent.com/JDMcKinstry/jQuery.winFocus/master/jquery.winFocus.js
// @require https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js
// @include /^https://.*-des\.f.v..d\.com//
// @license MIT
// @updateURL https://openuserjs.org/meta/ford.lambert/xa_designer_overlay.meta.js
// @downloadURL https://openuserjs.org/install/ford.lambert/xa_designer_overlay.user.js
// ==/UserScript==
// ==OpenUserJS==
// @author ford.lambert
// ==/OpenUserJS==
// jshint esversion: 6
// jshint jquery: true
// globals $, jQuery

(function(jQuery) {
    'use strict';
    jQuery.noConflict(); // do not use $ because it's also used by Prototype
    console.log(`%c Loading XA overlay`, 'color: #38C4CC')
    console.log(`%c Enable or disable XA overlay modules in it's menu on the top of the screen`, 'color: #38C4CC')

    let tabIsActive = true
    const modules = {
        regenMonitor: {
            active: getCookie('xa_regen_monitor').length ? ((getCookie('xa_regen_monitor') == 'true' || getCookie('xa_regen_monitor') == true) ? true : false) : true,
        	cookie: 'xa_regen_monitor',
        	label: 'Regen monitor',
        	title: 'IceBlue Style required - Show production monitor infos with a colored logo on top left corner, green: done, yellow: running. Go inactive/active when tab loose/gain focus'
        },
        shortcuts: {
        	active: getCookie('xa_shortcuts').length ? ((getCookie('xa_shortcuts') == 'true' || getCookie('xa_shortcuts') == true) ? true : false) : true,
        	cookie: 'xa_shortcuts',
        	label: 'Keyboard shortcuts',
        	title: 'Enable some keyboard shortcuts: ctrl + shift + f -> global fv search, ctrl + shift + s -> save and quit, ctrl + s -> save and continue'
        },
        editorQol: { 
        	active: getCookie('xa_editor_qol').length ?  ((getCookie('xa_editor_qol') == 'true' || getCookie('xa_editor_qol') == true) ? true : false) : true,
        	cookie: 'xa_editor_qol',
        	label: "Editor QOL",
        	title: "Add some quality of life display when editing something, status, spec reference and level of sharing"
        },
        generalLisibility: { 
        	active: getCookie('xa_lisibility').length ?  ((getCookie('xa_lisibility') == 'true' || getCookie('xa_lisibility') == true) ? true : false) : true,
        	cookie: 'xa_lisibility',
        	label: "General lisibility",
        	title: "Add some little things to boost lisibility: partial names are colored, always show lightened lightbulbs, show scss var colors"
        },
        flash: { 
        	active: getCookie('xa_flash').length ?  ((getCookie('xa_flash') == 'true' || getCookie('xa_flash') == true) ? true : false) : true,
        	cookie: 'xa_flash',
        	label: "Flash expire",
        	title: "Remove flash messages after 5sec of display"
        },
        fieldPresence: { 
        	active: getCookie('xa_field_presence').length ? ((getCookie('xa_field_presence') == 'true' || getCookie('xa_field_presence') == true) ? true : false) : true,
        	cookie: 'xa_field_presence',
        	label: "Field presence alert",
        	title: "Display a warning if a model does not have a created_at and updated_at"
        },
        userStory: { 
        	active: getCookie('xa_user_story').length ? ((getCookie('xa_user_story') == 'true' || getCookie('xa_user_story') == true) ? true : false) : false,
        	cookie: 'xa_user_story',
        	label: "User story warning",
        	title: "Experimental, display a warning if you have no user story assigned"
        },
        editorSize: {
            active: getCookie('xa_editor_size').length ? ((getCookie('xa_editor_size') == 'true' || getCookie('xa_editor_size') == true) ? true : false) : false,
        	cookie: 'xa_editor_size',
        	label: "Ace editor default size",
        	title: "Save your last editor height and set it on load"
        },
        regenButton: {
            active: getCookie('xa_regen_button').length ? ((getCookie('xa_regen_button') == 'true' || getCookie('xa_regen_button') == true) ? true : false) : true,
        	cookie: 'xa_regen_button',
        	label: "Add a regen button",
        	title: "Add a regen button on top of models pages next to the Maveoc name, does exactly like the drag and drop of fields without the risk of missclick."
        },
        dragDropLocker: {
            active: getCookie('xa_drag_drop_locker').length ? ((getCookie('xa_drag_drop_locker') == 'true' || getCookie('xa_drag_drop_locker') == true) ? true : false) : true,
        	cookie: 'xa_drag_drop_locker',
        	label: "Add a drag and drop locking button",
        	title: "Add a button to 'lock' model and graphical views therefore avoiding missdrop. Locked by default. Also move the reminders away from this area to avoid their removing"
        },
        qualityFramework: {
            active: getCookie('xa_quality_framework').length ? ((getCookie('xa_quality_framework') == 'true' || getCookie('xa_quality_framework') == true) ? true : false) : false,
        	cookie: 'xa_quality_framework',
        	label: "Experimental - Quality framework",
        	title: "Experimental, add functionnalities to force the use of quality tools around the app (status / description...)"
        },
        experimentalInterface: {
            active: getCookie('xa_experimental_interface').length ? ((getCookie('xa_experimental_interface') == 'true' || getCookie('xa_experimental_interface') == true) ? true : false) : false,
        	cookie: 'xa_experimental_interface',
        	label: "Experimental - Apply UI improvements",
        	title: "The functionnality in this section will be merged elsewhere after user review. Currently: change certain select into chosens"
        },
    }

    console.log({ modules })

    const initOverlay = () => {
        setXaMenu()
        addKeybordShortcuts()
        limitFlashDuration()
        displayRequiredFieldNotice()
        displayUserStoryWarning()
        displayEditorInfos()
        displayVisibilityBoost()
        resizeEditor()
        addRegenButton()
        addDragDropLocker()
        initQualityFramework()
        initExperimentalUi()
        delay(1000).then(() => addRegenWatcher())
    }

    // XA Menu ------>
    const setXaMenu = () =>  {
		const $xaMenuButton = jQuery('<button></button>')
		.attr('id', 'xa-menu-button')
		.text('XA - Menu')
		.css('margin',  '0 20px')

		const $xaMenuBox = jQuery('<div></div>')
		.attr('id', 'xa-menu')
        .css('position', 'fixed')
        .css('top', '20vh')
        .css('left', '42vw')
        .css('padding', '10px')
        .css('box-shadow', '0px 0px 900px 50px black')
        .css('z-index', '9000')
        .css('background-color', 'white')
        .css('color', 'black')
        .css('visibility', 'hidden')
        .css('max-height', '60vh')
        .css('overflow', 'auto')

		const $xaMenu = jQuery('<form></form>')
		.attr('method', 'dialog')

		const $info = jQuery('<p></p>')
		.css('width', '350px')
		.css('font-size', '1.2em')
		.css('font-style', 'italic')
		.text('Changing the value a checkbox immediatly update a related cookie for enabling/disabling the module. Reload needed to apply effect (consider your background work before doing so...).')
		$xaMenuBox.append($info)

		for (const [key, module] of Object.entries(modules)) {
			const $xaMenuEntrie = jQuery('<div></div>')
			.css('display', 'flex')
			.css('justify-content', 'center')
			.css('align-items', 'center')
			.css('margin-bottom', '10px')
			.css('border-bottom', '1px solid grey')

			const $label = jQuery('<label></label>')
			.text(module.label)
			.css('width', '150px')
			.css('font-size', '1.2em')
			.attr('for', `xa-${module.cookie}-input`)

			const $input = jQuery('<input />')
			.attr('type', 'checkbox')
			.prop('checked', module.active)
			.css('margin', '5px 10px')
			.attr('id', `xa-${module.cookie}-input`)
			
			$input.on('change', function() {
                setLocalCookie(module.cookie, this.checked)
            })

			const $info = jQuery('<img />')
			.attr('src', '/images/std/help.png?1199643545')
			.attr('alt', 'Help')
			.attr('title', module.title)

			$xaMenuEntrie.append($label)
			$xaMenuEntrie.append($input)
			$xaMenuEntrie.append($info)
			$xaMenu.append($xaMenuEntrie)
			$xaMenuBox.append($xaMenu);
		}

		const $menu = jQuery('<menu></menu>')
		.css('padding', '0')
		.css('display', 'flex')
		.css('justify-content', 'center')

		const $closeButton = jQuery('<button></button>')
		.attr('value', 'cancel')
		.text('Cancel')
		.css('margin', '10px')

		const $reloadButton = jQuery('<button></button>')
		.text('Reload to apply')
		.css('margin', '10px')
		.attr('onClick', 'location.reload()')

		$menu.append($closeButton)
		$menu.append($reloadButton)
		$xaMenu.append($menu)

		jQuery('#header-elts').append($xaMenuBox)
		jQuery('#header-elts').prepend($xaMenuButton)

		$xaMenuButton.on('click', () => {
            toggleModal(jQuery('#xa-menu'))
		})

        $closeButton.on('click', () => {
            toggleModal(jQuery('#xa-menu'))
        })
    }

    const toggleModal = ($modal) => {
        if($modal.css('visibility') == 'visible') {
            $modal.css('visibility', 'hidden')
        } else {
            $modal.css('visibility', 'visible')
        }
    }
    // XA Menu <------

    // Regen watcher ------>
    const addRegenWatcher = () => {
        if(modules.regenMonitor.active && !window.location.href.includes('/tasks')) {
            const hasCustomStyle = jQuery('#title').find('h1').css('display') == 'none'
            displayLogo(hasCustomStyle)
            pollRegen()

            let monitor_pu = new Ajax.PeriodicalUpdater('production_monitor', '/changes/current_tasks', {method:'post', frequency:3, decay:2})

            jQuery.winFocus({
                //  will only fire when current tab loses focus
                blur: () => {
                    disableRegenWatcher(monitor_pu)
                    tabIsActive = false
                },
                //  will only fire when current tab gains focus
                focus: () => {
                    tabIsActive = true
                    monitor_pu = new Ajax.PeriodicalUpdater('production_monitor', '/changes/current_tasks', {method:'post', frequency:3, decay:2})
                    pollRegen()
                }
            });
        }
    }

    const disableRegenWatcher = (monitor_pu) => {
        monitor_pu.stop();
        monitor_pu = undefined;
    }

    const pollRegen = () => {
        const inactiveLogo = "https://storage.gra.cloud.ovh.net/v1/AUTH_0b4eb0b702894162b8bcdc31088fb7dd/hexa-public/designer-script/default.png"
        const doneLogo = "https://storage.gra.cloud.ovh.net/v1/AUTH_0b4eb0b702894162b8bcdc31088fb7dd/hexa-public/designer-script/ready.png"
        const processingLogo = "https://storage.gra.cloud.ovh.net/v1/AUTH_0b4eb0b702894162b8bcdc31088fb7dd/hexa-public/designer-script/running.png"

        let text = ''
        jQuery('#production_monitor').find('td').each(function() {
            text += jQuery(this).text().trim()
        })
        const isProcessing = text.length > 0

        if(isProcessing) {
            changeLogo(processingLogo)
        } else {
            changeLogo(doneLogo)
        }

        setTimeout(function () {
            if(tabIsActive) {
                pollRegen()
            } else {
                changeLogo(inactiveLogo)
            }
        }, 2000)

    }

    const changeLogo = (logoPath) => {
        jQuery('#header').find('#title').css("background-image", "url(" + logoPath + ")")
    }

    const displayLogo = (hasCustomStyle) => {
        const logoPath = hasCustomStyle ? 'https://storage.gra.cloud.ovh.net/v1/AUTH_0b4eb0b702894162b8bcdc31088fb7dd/hexa-public/designer-script/default.png' : '/images/logo/logo.png'
        const $header = jQuery('#header')
        const $title = $header.find('#title')
        $title.css("background-image", "url(" + logoPath + ")")
        const $titleInfos = jQuery('<span></span>')
        .addClass('sub-title')
        .text($title.text())
        $header.append($titleInfos)
    }

    // Regen watcher <------

    // KEYBOARD SHORTCUTS ------>
    const addKeybordShortcuts = () => {
        if(modules.shortcuts.active) {
        	jQuery(document).on('keydown', (e) => {
        		const isMac = navigator.platform.match('Mac')
        		const ctrlLikeKey = isMac ? e.metaKey : e.ctrlKey;
        		const saveKeyCode = 83
        		const searchKeyCode = 70
        		
        		if(isMac) {
        		    if (ctrlLikeKey && e.shiftKey && e.keyCode == saveKeyCode) {
        		        e.preventDefault(); // prevent "save html page" default event
            		    submitEditor()
        		    }
        		} else {
        		    if (ctrlLikeKey && e.keyCode == saveKeyCode) {
        		        e.preventDefault(); // prevent "save html page" default event
            		    submitEditor()
        		    }
        		}
        		
        		if (ctrlLikeKey && e.shiftKey && e.keyCode == searchKeyCode) {
            		jQuery('#global_search_field').focus()
            	}
        	})
        }
    }

    const submitEditor = () => {
		// const $saveContinue = jQuery('#new_create_form_submit').length ? jQuery('#new_create_form_submit') : jQuery('#continue_editing_form_submit');
		// stayOnPage ? $saveContinue.click() : jQuery('#update_form_submit').click();
		jQuery('#continue_editing_form_submit').click()
	}
    // <------ KEYBOARD SHORTCUTS

    // Info display ------>
    const limitFlashDuration = () => {
        if(modules.flash.active) {
            // After reload on save, a sticky flash stay on the top left, remove it after some time.
            setTimeout(function() {
              jQuery("#flash").hide()
            }, 5000)
        }
    }
    // <------ Info display
    
    // ------> Alert for basic fields presence (created_at, updated_at)
    // This script is brought to you by Aloïs, thanks to him and his amazing cat :)
    const displayRequiredFieldNotice = () => {
        if(modules.fieldPresence.active) {
            const created_at = document.querySelector("[title^='created_at']");
            const updated_at = document.querySelector("[title^='updated_at']");
            const sidebarNotes = document.querySelector("#field_types span");
            const maveocName = document.querySelector("#content h2").innerText;
            const existingStorage = JSON.parse(JSON.parse(window.localStorage.getItem("disabled_notices")));
            const disableButtons = document.querySelectorAll(".disable-notice");
            
            if (created_at && updated_at) return;
            if (sidebarNotes) {
                if ((!created_at && existingStorage == null) || (!created_at && (existingStorage !== null && !existingStorage.includes(maveocName)))) {
                    sidebarNotes.innerHTML += '<p class="date_fields_warning" style="color:red">You do not have a created_at field! <img alt="Del" class="disable-notice" style="cursor:pointer;" title="Disable ALL notice messages!" src="/images/faveod/del.png?1596181992"></p>';
                };
                
                if ((!updated_at && existingStorage == null) || (!updated_at && (existingStorage !== null && !existingStorage.includes(maveocName)))) {
                    sidebarNotes.innerHTML += '<p class="date_fields_warning" style="color:red">You do not have an updated_at field! <img alt="Del" class="disable-notice" style="cursor:pointer;" title="Disable ALL notice messages!" src="/images/faveod/del.png?1596181992"></p>';
                };
            }
            
            disableButtons.forEach(button => button.addEventListener('click', removeNotices));
        }
    }
    
    const removeRequiredFieldNotice = () => {
        const notices = document.querySelectorAll(".date_fields_warning");
        notices.forEach(notice => notice.hide());
        let value = "";
        if (existingStorage !== null) {
            existingStorage.push(maveocName);
            value = JSON.stringify(existingStorage);
        } else {
            value = JSON.stringify([maveocName]);
        }
        window.localStorage.setItem(`disabled_notices`, value);
    }
    // <------ Alert for basic fields presence
    
    // ------> User Story warning
        const displayUserStoryWarning = () => {
            // Initially started this one to encourage always working with a user story
            // Enable or not depending of the project, disabled by default
            
            if(modules.userStory.active) {
                const $userStoryIndicator = jQuery('#working_on');
    
                if(!$userStoryIndicator.length) {
                    
                    
                	const $userStoryWarning = jQuery('<span></span>')
                	.attr('id', 'user-story-warning')
                	.text('WARNING ! You are currently working without any User story !')
                	.css('color', '#D04158')
                	.css('font-weight', 'bold')
                	.css('font-size', '1.1em')

                    const $warningLogo = jQuery('<span></span>')
                    .attr('id', 'user-story-warning-logo')
                    .css('background-image', 'url(https://i.imgur.com/JW2wKDs.png)')
                    .css('height', '20px')
                    .css('width', '20px')
                    .css('display', 'inline-block')
                    .css('background-position', 'center')
                    .css('background-size', 'contain')
                    .css('margin-bottom', '-4px')
                    .css('margin-right', '4px')
                    
                    jQuery('#header-elts').prepend($userStoryWarning);
                    $userStoryWarning.prepend($warningLogo);
                    
                    let counter = 1;
                	const isOdd = (x) => { return x & 1 }
                	
                	setInterval(function() {
                		if(isOdd(counter) == 1) {
                			$warningLogo.css('background-image', 'url(https://i.imgur.com/JW2wKDs.png)')
                			counter += 1;
                		} else {
                			$warningLogo.css('background-image', 'url(https://i.imgur.com/iqfdJGd.png)')
                			counter += 1;
                		}
                	}, 1000);
                }
            }
        }
    // <------ User stroy warning
    
    // ------> Editor QOL
    const displayEditorInfos = () => {
        if(modules.editorQol.active) {
            displayStatus()
            displaySpecReference()
    		displaySharing()
        }
    }
    
    const displayStatus = () => {
    	const $originalStatus = jQuery("select[id$='_status']")
    	
    	if(!$originalStatus.length) { return }
    	
    	const $newStatus = $originalStatus.clone()
    	$newStatus.attr('id', 'new-select')
    	.val($originalStatus.val())
    	.css('margin-right', '10px')
    	.css('border-radius', '5px')
    
    	$newStatus.on('change', function() {
    		$originalStatus.val(jQuery(this).val())
    	})
    
    	$originalStatus.on('change', function() {
    		$newStatus.val(jQuery(this).val())
    	})
    
    	jQuery('#content h2.title').prepend($newStatus)
    }
    
    const displaySpecReference = () => {
    	const $originalSpecRef = jQuery("input[id$='_spec_ref']")
    	
    	if(!$originalSpecRef.length) { return }
    	
    	const $newSpecRef = $originalSpecRef.clone()
    	$newSpecRef.attr('id', 'new-specref')
    	.attr('placeholder', "US Reference")
    	.css('text-align', 'center')
    	.css('border-radius', '5px')
    	.css('margin-left', '10px')
    	.css('display', 'inline-block')
    
    	$newSpecRef.on('change', function() {
    		$originalSpecRef.val(jQuery(this).val())
    	})
    
    	$originalSpecRef.on('change', function() {
    		$newSpecRef.val(jQuery(this).val())
    	})
    
    	jQuery('#content h2.title').append($newSpecRef)
    }
    
    const displaySharing = () => {
        const currentAvailability = jQuery("select[id$='_shared_for']").val()
        const label = jQuery("select[id$='_shared_for'] option:selected").text()
        
        if(!jQuery("select[id$='_shared_for']").length) { return }
    
    	const $wrapper = jQuery('<p></p>')
        const $availability = jQuery('<span></span>')
    	.text(`Available for ${label}`)
    	.css('border', '1px solid black')
    	.css('background-color', 'grey')
    	.css('color', 'white')
    	.css('padding', '3px')
    	.css('border-radius', '5px')
    	.css('margin-left', '10px')
    
    	if(currentAvailability == 3) {
    		$wrapper.text('This code need manual regeneration to be applied')
    		const $warningSign = jQuery('<i />')
    		.addClass('muted glyphicon glyphicon-exclamation-sign')
    		.css('color', 'orange')
    		.css('margin-right', '5px')
    		$wrapper.prepend($warningSign)
    	}
    	
    	$wrapper.append($availability)
    	jQuery('#content h2.title').append($wrapper)
    }
    // <------ Editor QOL
    
    // ------> General lisibility
    const displayVisibilityBoost = () => {
        if(modules.generalLisibility.active) {
            /* ----- Always show lightened lightbulbs ----- */
			const wantedSrc = '/images/page_icons/lightbulb.png?1199643502';
			jQuery('td').find(`img[src="${wantedSrc}"]`).parents('td').css('opacity', '1')
			
			/*------- Add a color if a view is a partial ------*/
			jQuery('.partial').find('a').css('color', 'rgba(233, 96, 42, 0.7)')
    		
    		/* show color for scss variables */
    		const $variableTable = jQuery('#style_variables_table');
    
            if($variableTable.length) {
                $variableTable.find('tbody').find('tr').each(function() {
                    jQuery(this).find('td:nth-child(2)').css('background-color', jQuery(this).find('td:nth-child(4)').text())
                })
            }
            
            /* Add separator in search */
            document.getElementById('global_search_field').addEventListener('input', () => {
                setTimeout(function(){
                    const separatorStyle = '1px dashed black'
                    const resultBox = document.getElementById('gs_results')
                    const entries = []

                    entries.push(resultBox.querySelector('.config_initializers'))
                    entries.push(resultBox.querySelector('.cfg_dependencies'))
                    entries.push(resultBox.querySelector('.use_cases'))
                    entries.push(resultBox.querySelector('.style_variables'))
                    entries.push(resultBox.querySelector('.javascripts'))
                    entries.push(resultBox.querySelector('.js_frameworks'))
                    entries.push(resultBox.querySelector('.js_functions'))
                    entries.push(resultBox.querySelector('.styles'))
                    entries.push(resultBox.querySelector('.layouts'))
                    entries.push(resultBox.querySelector('.notifications'))
                    entries.push(resultBox.querySelector('.aktions'))
                    entries.push(resultBox.querySelector('.field_actions'))
                    entries.push(resultBox.querySelector('.action_attribute_definitions'))
                    entries.push(resultBox.querySelector('.field_formatters'))
                    entries.push(resultBox.querySelector('.field_behaviours'))
                    entries.push(resultBox.querySelector('.field_styles'))
                    entries.push(resultBox.querySelector('.field_views'))
                    entries.push(resultBox.querySelector('.view_helpers'))
                    entries.push(resultBox.querySelector('.views'))
                    entries.push(resultBox.querySelector('.model_tests'))
                    entries.push(resultBox.querySelector('.model_logics'))
                    entries.push(resultBox.querySelector('.fields'))

                    entries.map((entry) => {
                        if(entry != null) {
                            entry.parentNode.style.borderTop = separatorStyle
                        }
                    })
                }, 1000);
            })
        }
    }
    
    // <------ General lisibility

    // ------> Editor Size
    const resizeEditor = () => {
        if(modules.editorSize.active) {
            let height = getCookie('text_editor_height').length ? getCookie('text_editor_height') : '454px'
            const $editor = jQuery('.ace_editor')
            $editor.css('height', height)

            window.addEventListener('beforeunload', function() {
                setLocalCookie('text_editor_height', $editor.css('height'))
            })
        }
    }
    // <------ Editor Size
    
    // ------> Regen Button
    const addRegenButton = () => {
        // This script is brought to you by Thibault, thanks to him :)
        if(modules.regenButton.active && location.href.includes('/model/')) {
            const titles = document.querySelectorAll('h2.title')
            const title = titles[titles.length - 1]
            title.insertAdjacentHTML('beforeend', `<span id="regen">${String.fromCodePoint(0x1f504)}</span>`)
            const regen = document.querySelector('#regen')
            regen.style.cursor = 'pointer'
            let divs = document.querySelectorAll('fieldset div.moved_field_objects')
            let maveocId = document.querySelector('#content').data('in_use')
            let fields = []

            divs.forEach((mfo, i )=> {
                if (divs[i + 1] == mfo.nextElementSibling && fields.length < 2) {
                    fields.push(mfo.id.split('_')[1])
                    fields.push(divs[i + 1].id.split('_')[1])
                }
                return fields
            })

            regen.addEventListener('click', () => {
                if (!confirm("Do you want to regen this Maveoc ?")) return
                
                fetch("/fields/move_dropped", {
                    "headers": {
                        "accept": "text/javascript, text/html, application/xml, text/xml, */*",
                        "accept-language": "en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
                        "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"96\", \"Google Chrome\";v=\"96\"",
                        "sec-ch-ua-mobile": "?0",
                        "sec-ch-ua-platform": "\"macOS\"",
                        "sec-fetch-dest": "empty",
                        "sec-fetch-mode": "cors",
                        "sec-fetch-site": "same-origin",
                        "x-csrf-token": `${document.querySelector('[name="csrf-token"]').content}`,
                        "x-prototype-version": "1.6.1",
                        "x-requested-with": "XMLHttpRequest"
                    },
                    "referrerPolicy": "strict-origin-when-cross-origin",
                    "body": `model=${maveocId}&receiver=${fields[0]}&id=${fields[1]}&_=`,
                    "method": "POST",
                    "mode": "cors",
                    "credentials": "include"
                }).then(console.log(`%c Starting Maveoc regen...`, 'color: #38C4CC'))
            })
        }
    }
    // <------ Regen Button
    
    // ------> DragDrop Locker
    const addDragDropLocker = () => {
        if(modules.dragDropLocker.active) {
            const addLocker = () => {
                return new Promise((resolve, reject) => {
                    // add html button
                    const titles = document.querySelectorAll('h2.title')
                    const title = titles[titles.length - 1]

                    const lockerBox = document.createElement('div')
                    lockerBox.classList.add('locker-box')
                    lockerBox.style.display = 'flex'
                    lockerBox.style.alignItems = 'center'
                    lockerBox.style.justifyContent = 'center'
                    const lockerInput = document.createElement('input')
                    lockerInput.id = 'js-dragdrop-input'
                    lockerInput.setAttribute('type', 'checkbox')
                    lockerInput.style.display = 'none'
                    const lockerButton = document.createElement('label')
                    lockerButton.classList.add('btn')
                    lockerButton.id = 'js-dragdrop-label'
                    lockerButton.innerHTML = "DragDrop Lock"
                    lockerButton.setAttribute('for', 'js-dragdrop-input')
                    lockerButton.style.border = '1px solid darkblue'
                    lockerButton.style.cursor = 'pointer'
                    lockerButton.style.padding = '5px'

                    lockerBox.append(lockerInput)
                    lockerBox.append(lockerButton)
                    title.append(lockerBox)

                    // If we set it manually or by default anyway
                    const isLocked = (getCookie('dragDropLock') == 'true' || getCookie('dragDropLock') == '')

                    lockerInput.checked = isLocked

                    lockerInput.addEventListener('change', (e) => {
                        const label = document.getElementById('js-dragdrop-label')
                        e.target.checked ? lock(label) : unlock(label, true)
                    })

                    resolve(isLocked)
                })
            }
            
            const lock = (label) => {
                label.innerHTML = 'Drag&Drop locked, click to unlock'
                setLocalCookie('dragDropLock', 'true', 365, location.pathname)
                const fields = document.querySelectorAll('.moved_field_objects')
                const dragDropPanel = document.querySelector('td.drag_objects')
                dragDropPanel.remove()
                // const rows = document.querySelectorAll('#view_parts_table fieldset')

                Array.from(fields).map((field) => {
                    const newElement = field.cloneNode(true)
                    field.parentNode.replaceChild(newElement, field)
                })

                const actions = document.querySelectorAll('.field_actions')
                Array.from(actions).map((action) => action.style.display = 'block')
                Array.from(document.querySelectorAll('[data-indicator="model_indicator"]')).map((arrow) => arrow.style.display = 'none')
            }
            
            const unlock = (label, reload=false) => {
                if(reload && !confirm('This will reload current page, proceed ?')) return
                label.innerHTML = 'Drag&Drop unlocked, click to lock'
                setLocalCookie('dragDropLock', 'false', 365, location.pathname)
                if(reload) window.location.reload()
            }
            
            const moveReminders = () => {
                const tabBox = document.getElementById('mvc_tabs')
                const helpButton = document.querySelector('#field_types img[alt="Help"]').parentNode
                const infoButton = document.querySelector('#field_types img[alt="Information"]').parentNode
        
                helpButton.querySelector('img').style.marginRight = '5px'
                infoButton.querySelector('img').style.marginRight = '5px'
        
                const helpTab = document.createElement('li')
                helpTab.classList.add('help-tab')
                helpTab.style.float = 'right'
        
                const helpLabel = document.createElement('span')
                helpLabel.innerText = 'Help'
        
                const infoTab = document.createElement('li')
                infoTab.classList.add('info-tab')
                infoTab.style.float = 'right'
        
                const infoLabel = document.createElement('span')
                infoLabel.innerText = 'Infos'
        
                helpButton.appendChild(helpLabel)
                infoButton.appendChild(infoLabel)
                helpTab.appendChild(helpButton)
                infoTab.appendChild(infoButton)
                tabBox.appendChild(infoTab)
                tabBox.appendChild(helpTab)
            }

            if(location.href.includes('/model/')) {
                moveReminders()
                addLocker().then((isLocked) => {
                    const label = document.getElementById('js-dragdrop-label')
                    isLocked ? lock(label) : unlock(label)
                })
            }
        }
    }
    // <------ DragDrop Locker
    
    // ------> Quality Framework
    const initQualityFramework = () => {
        if(modules.qualityFramework.active && !window.location.pathname.includes('/use_cases/')) {
            const initDevalidationWarning = () => {
        	    const submitButtons = [...document.querySelectorAll('input[type="submit"]')]
                const originalStatus = document.querySelector("select[id$='_status']")

                if(!submitButtons || !originalStatus) return

                const isValidated = (originalStatus.value == 8)
                const isDraft = (originalStatus.value == 1)

                if(isDraft) {
                    setTimeout(function(){
                        initDevalidationWarning()
                        return
                    }, 800);
                }
                if(!isValidated) { return }

                submitButtons.map((button) => {
                    button.addEventListener('click', function(e) {
                        if(!button.classList.contains('confirmed')) {
                            e.preventDefault()
                            if (!confirm('Saving a "validated" code will make it regress to "waiting for validation". Continue ?')) return
                            originalStatus.value = 7
                            button.classList.add('confirmed')
                            button.click()
                        }
                    })
                })
            }

            const initFieldsRequired = () => {
                const description = document.querySelector("textarea[id$='_description']")
                // const commit = document.querySelector("textarea[id$='_commit_message']")
                const submitButtons = [...document.querySelectorAll('input[type="submit"]')]

                if(!submitButtons || !description) return

                if(description.value.trim().length < 10) {
                    submitButtons.map((button) => {
                        disableElement(button, 'A description of at least 10 characters is required')
                    })
                }

                description.addEventListener('input', () => {
                    handleRequiredFields(submitButtons, description)
                })

                // commit.addEventListener('input', () => {
                //     handleRequiredFields(submitButtons, description, commit)
                // })
            }

            const handleRequiredFields = (buttons, description) => {
                if(description.value.trim().length < 10) {
                    buttons.map((button) => {
                        disableElement(button, 'A description and a commit message of at least 10 characters is required')
                    })
                } else {
                    buttons.map((button) => {
                        button.removeAttribute('disabled')
                        button.removeAttribute('title')
                        button.removeAttribute('style')
                    })
                }
            }

            initDevalidationWarning()
            initFieldsRequired()
        }
    }
    // <------ Quality Framework

    // ------> Experimental UI
    const initExperimentalUi = () => {
        if(modules.experimentalInterface.active) {
            console.log('%c Loading experimental interface', 'color: #38C4CC')
            const link = document.createElement('link');
            link.type = 'text/css';
            link.rel = 'stylesheet';

            document.head.appendChild(link);
            link.href = 'https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.css';

            jQuery('select[multiple="multiple"]').chosen()
            // jQuery("select[id*='field_format_views_matching_']").chosen()

            jQuery('.chosen-container-multi')
                .css('width', '100%')

            jQuery('.chosen-choices')
            .css('overflow', 'auto')
            .css('max-height', '35vh')

            console.log('test', jQuery('#review_filters').find('select'))
            jQuery('#review_filters').find('select').chosen()
        }
    }
    // <----- Experimental UI
    
    jQuery(document).ready(function() {
        setTimeout(function () {
            initOverlay()
        }, 800);
    });
    
})(jQuery);

// ------> HELPERS (overlay  global)
function setLocalCookie(name, value, days, path = '/') {
    let expires = '';
    if (days) {
        const date = new Date();
        date.setTime(date.getTime() + (days*24*60*60*1000));
        expires = '; expires=' + date.toUTCString();
    }
    document.cookie = name + '=' + (value || '')  + expires + `; path=${path}`;
}

function getCookie(cname) {
    const decodedCookie = decodeURIComponent(document.cookie);
    const ca = decodedCookie.split(';');
    const match = ca.filter(cookie => { return cookie.includes(cname) })
    return match[0] ? match[0].replace(`${cname}=`, '').trim() : '';
}

function disableElement(element, title='') {
    element.setAttribute('disabled', 'disabled')
    element.setAttribute('title', title)
    element.style.color = 'grey'
    element.style.borderColor = 'grey'
    element.style.cursor = 'not-allowed'
}

function delay(time) {
    return new Promise(resolve => setTimeout(resolve, time))
}
// <------ HELPERS