import { addSubscription } from '../../utils/subscription.utils'; import Services from '../../services/service'; import { getCorrectDOM } from '~/utils/html.utils'; import { Process } from 'pkg/sdk_client'; // Initialize function, create initial tokens with itens that are already selected by the user export async function init() { const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const element = container.querySelector('select') as HTMLSelectElement; // Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside const wrapper = document.createElement('div'); if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper); wrapper.classList.add('multi-select-component'); wrapper.classList.add('input-field'); // Create elements of search const search_div = document.createElement('div'); search_div.classList.add('search-container'); const input = document.createElement('input'); input.classList.add('selected-input'); input.setAttribute('autocomplete', 'off'); input.setAttribute('tabindex', '0'); if (input) { addSubscription(input, 'keyup', inputChange); addSubscription(input, 'keydown', deletePressed); addSubscription(input, 'click', openOptions); } const dropdown_icon = document.createElement('a'); dropdown_icon.classList.add('dropdown-icon'); if (dropdown_icon) addSubscription(dropdown_icon, 'click', clickDropdown); const autocomplete_list = document.createElement('ul'); autocomplete_list.classList.add('autocomplete-list'); search_div.appendChild(input); search_div.appendChild(autocomplete_list); search_div.appendChild(dropdown_icon); // set the wrapper as child (instead of the element) element.parentNode?.replaceChild(wrapper, element); // set element as child of wrapper wrapper.appendChild(element); wrapper.appendChild(search_div); addPlaceholder(wrapper); } function removePlaceholder(wrapper: HTMLElement) { const input_search = wrapper.querySelector('.selected-input'); input_search?.removeAttribute('placeholder'); } function addPlaceholder(wrapper: HTMLElement) { const input_search = wrapper.querySelector('.selected-input'); const tokens = wrapper.querySelectorAll('.selected-wrapper'); if (!tokens.length && !(document.activeElement === input_search)) input_search?.setAttribute('placeholder', '---------'); } // Listener of user search function inputChange(e: Event) { const target = e.target as HTMLInputElement; const wrapper = target?.parentNode?.parentNode; const select = wrapper?.querySelector('select') as HTMLSelectElement; const dropdown = wrapper?.querySelector('.dropdown-icon'); const input_val = target?.value; if (input_val) { dropdown?.classList.add('active'); populateAutocompleteList(select, input_val.trim()); } else { dropdown?.classList.remove('active'); const event = new Event('click'); dropdown?.dispatchEvent(event); } } // Listen for clicks on the wrapper, if click happens focus on the input function clickOnWrapper(e: Event) { const wrapper = e.target as HTMLElement; if (wrapper.tagName == 'DIV') { const input_search = wrapper.querySelector('.selected-input'); const dropdown = wrapper.querySelector('.dropdown-icon'); if (!dropdown?.classList.contains('active')) { const event = new Event('click'); dropdown?.dispatchEvent(event); } (input_search as HTMLInputElement)?.focus(); removePlaceholder(wrapper); } } function openOptions(e: Event) { const input_search = e.target as HTMLElement; const wrapper = input_search?.parentElement?.parentElement; const dropdown = wrapper?.querySelector('.dropdown-icon'); if (!dropdown?.classList.contains('active')) { const event = new Event('click'); dropdown?.dispatchEvent(event); } e.stopPropagation(); } // Function that create a token inside of a wrapper with the given value function createToken(wrapper: HTMLElement, value: any) { const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const search = wrapper.querySelector('.search-container'); const inputInderline = container.querySelector('.selected-processes'); // Create token wrapper const token = document.createElement('div'); token.classList.add('selected-wrapper'); const token_span = document.createElement('span'); token_span.classList.add('selected-label'); token_span.innerText = value; const close = document.createElement('a'); close.classList.add('selected-close'); close.setAttribute('tabindex', '-1'); close.setAttribute('data-option', value); close.setAttribute('data-hits', '0'); close.innerText = 'x'; if (close) addSubscription(close, 'click', removeToken); token.appendChild(token_span); token.appendChild(close); inputInderline?.appendChild(token); } // Listen for clicks in the dropdown option function clickDropdown(e: Event) { const dropdown = e.target as HTMLElement; const wrapper = dropdown?.parentNode?.parentNode; const input_search = wrapper?.querySelector('.selected-input') as HTMLInputElement; const select = wrapper?.querySelector('select') as HTMLSelectElement; dropdown.classList.toggle('active'); if (dropdown.classList.contains('active')) { removePlaceholder(wrapper as HTMLElement); input_search?.focus(); if (!input_search?.value) { populateAutocompleteList(select, '', true); } else { populateAutocompleteList(select, input_search.value); } } else { clearAutocompleteList(select); addPlaceholder(wrapper as HTMLElement); } } // Clears the results of the autocomplete list function clearAutocompleteList(select: HTMLSelectElement) { const wrapper = select.parentNode; const autocomplete_list = wrapper?.querySelector('.autocomplete-list'); if (autocomplete_list) autocomplete_list.innerHTML = ''; } // Populate the autocomplete list following a given query from the user function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) { const { autocomplete_options } = getOptions(select); let options_to_show; if (dropdown) { let messagingCounter = 1; const messagingOptions = select.querySelectorAll('option[value="messaging"]'); options_to_show = autocomplete_options.map(option => { if (option === 'messaging') { // Récupérer l'élément option correspondant au compteur actuel const currentOption = messagingOptions[messagingCounter - 1]; const processId = currentOption?.getAttribute('data-process-id'); console.log(`Mapping messaging ${messagingCounter} with processId:`, processId); const optionText = `messaging ${messagingCounter}`; messagingCounter++; // Stocker le processId dans un attribut data sur le select select.setAttribute(`data-messaging-id-${messagingCounter - 1}`, processId || ''); return optionText; } return option; }); } else { options_to_show = autocomplete(query, autocomplete_options); } const wrapper = select.parentNode; const input_search = wrapper?.querySelector('.search-container'); const autocomplete_list = wrapper?.querySelector('.autocomplete-list'); if (autocomplete_list) autocomplete_list.innerHTML = ''; const result_size = options_to_show.length; if (result_size == 1) { const li = document.createElement('li'); li.innerText = options_to_show[0]; li.setAttribute('data-value', options_to_show[0]); if (li) addSubscription(li, 'click', selectOption); autocomplete_list?.appendChild(li); if (query.length == options_to_show[0].length) { const event = new Event('click'); li.dispatchEvent(event); } } else if (result_size > 1) { for (let i = 0; i < result_size; i++) { const li = document.createElement('li'); li.innerText = options_to_show[i]; li.setAttribute('data-value', options_to_show[i]); if (li) addSubscription(li, 'click', selectOption); autocomplete_list?.appendChild(li); } } else { const li = document.createElement('li'); li.classList.add('not-cursor'); li.innerText = 'No options found'; autocomplete_list?.appendChild(li); } } // Listener to autocomplete results when clicked set the selected property in the select option function selectOption(e: any) { console.log('🎯 Click event:', e); console.log('🎯 Target value:', e.target.dataset.value); const wrapper = e.target.parentNode.parentNode.parentNode; const select = wrapper.querySelector('select'); const input_search = wrapper.querySelector('.selected-input'); const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`); console.log('🎯 Selected option:', option); console.log('🎯 Process ID:', option?.getAttribute('data-process-id')); if (e.target.dataset.value.includes('messaging')) { const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]); const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`); console.log('🚀 Dispatching newMessagingProcess event:', { processId, processName: `Messaging Process ${processId}` }); // Dispatch l'événement avant la navigation document.dispatchEvent(new CustomEvent('newMessagingProcess', { detail: { processId: processId, processName: `Messaging Process ${processId}` } })); // Navigation vers le chat const navigateEvent = new CustomEvent('navigate', { detail: { page: 'chat', processId: processId || '' } }); document.dispatchEvent(navigateEvent); return; } option.setAttribute('selected', ''); createToken(wrapper, e.target.dataset.value); if (input_search.value) { input_search.value = ''; } showSelectedProcess(e.target.dataset.value); input_search.focus(); e.target.remove(); const autocomplete_list = wrapper.querySelector('.autocomplete-list'); if (!autocomplete_list.children.length) { const li = document.createElement('li'); li.classList.add('not-cursor'); li.innerText = 'No options found'; autocomplete_list.appendChild(li); } const event = new Event('keyup'); input_search.dispatchEvent(event); e.stopPropagation(); } // function that returns a list with the autcomplete list of matches function autocomplete(query: string, options: any) { // No query passed, just return entire list if (!query) { return options; } let options_return = []; for (let i = 0; i < options.length; i++) { if (query.toLowerCase() === options[i].slice(0, query.length).toLowerCase()) { options_return.push(options[i]); } } return options_return; } // Returns the options that are selected by the user and the ones that are not function getOptions(select: HTMLSelectElement) { // Select all the options available const all_options = Array.from(select.querySelectorAll('option')).map((el) => el.value); // Get the options that are selected from the user const options_selected = Array.from(select.querySelectorAll('option:checked')).map((el: any) => el.value); // Create an autocomplete options array with the options that are not selected by the user const autocomplete_options: any[] = []; all_options.forEach((option) => { if (!options_selected.includes(option)) { autocomplete_options.push(option); } }); autocomplete_options.sort(); return { options_selected, autocomplete_options, }; } // Listener for when the user wants to remove a given token. function removeToken(e: Event) { // Get the value to remove const target = e.target as HTMLSelectElement; const value_to_remove = target.dataset.option; const wrapper = target.parentNode?.parentNode?.parentNode; const input_search = wrapper?.querySelector('.selected-input'); const dropdown = wrapper?.querySelector('.dropdown-icon'); // Get the options in the select to be unselected const option_to_unselect = wrapper?.querySelector(`select option[value="${value_to_remove}"]`); option_to_unselect?.removeAttribute('selected'); // Remove token attribute (target.parentNode as any)?.remove(); dropdown?.classList.remove('active'); const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const process = container.querySelector('#' + target.dataset.option); process?.remove(); } // Listen for 2 sequence of hits on the delete key, if this happens delete the last token if exist function deletePressed(e: Event) { const input_search = e.target as HTMLInputElement; const wrapper = input_search?.parentNode?.parentNode; const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode; const tokens = wrapper?.querySelectorAll('.selected-wrapper'); if (tokens?.length) { const last_token_x = tokens[tokens.length - 1].querySelector('a'); let hits = +(last_token_x?.dataset?.hits || 0); if (key == 8 || key == 46) { if (!input_search.value) { if (hits > 1) { // Trigger delete event const event = new Event('click'); last_token_x?.dispatchEvent(event); } else { if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2'; } } } else { if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0'; } } return true; } // Dismiss on outside click addSubscription(document, 'click', () => { // get select that has the options available const select = document.querySelectorAll('[data-multi-select-plugin]'); for (let i = 0; i < select.length; i++) { if (event) { var isClickInside = select[i].parentElement?.parentElement?.contains(event.target as Node); if (!isClickInside) { const wrapper = select[i].parentElement?.parentElement; const dropdown = wrapper?.querySelector('.dropdown-icon'); const autocomplete_list = wrapper?.querySelector('.autocomplete-list'); //the click was outside the specifiedElement, do something dropdown?.classList.remove('active'); if (autocomplete_list) autocomplete_list.innerHTML = ''; addPlaceholder(wrapper as HTMLElement); } } } }); async function showSelectedProcess(elem: MouseEvent) { const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; if (elem) { const cardContent = container.querySelector('.process-card-content'); const processes = await getProcesses(); const process = processes.find((process: any) => process[1].title === elem); if (process) { const processDiv = document.createElement('div'); processDiv.className = 'process'; processDiv.id = process[0]; const titleDiv = document.createElement('div'); titleDiv.className = 'process-title'; titleDiv.innerHTML = `${process[1].title} : ${process[1].description}`; processDiv.appendChild(titleDiv); for (const zone of process.zones) { const zoneElement = document.createElement('div'); zoneElement.className = 'process-element'; const zoneId = process[1].title + '-' + zone.id; zoneElement.setAttribute('zone-id', zoneId); zoneElement.setAttribute('process-title', process[1].title); zoneElement.setAttribute('process-id', `${process[0]}_${zone.id}`); zoneElement.innerHTML = `${zone.title}: ${zone.description}`; addSubscription(zoneElement, 'click', select); processDiv.appendChild(zoneElement); } if (cardContent) cardContent.appendChild(processDiv); } } } function select(event: Event) { const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const target = event.target as HTMLElement; const oldSelectedProcess = container.querySelector('.selected-process-zone'); oldSelectedProcess?.classList.remove('selected-process-zone'); if (target) { target.classList.add('selected-process-zone'); } const name = target.getAttribute('zone-id'); console.log('🚀 ~ select ~ name:', name); } function goToProcessPage() { const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const target = container.querySelector('.selected-process-zone'); console.log('🚀 ~ goToProcessPage ~ event:', target); if (target) { const process = target?.getAttribute('process-id'); console.log('=======================> going to process page', process); // navigate('process-element/' + process); document.querySelector('process-list-4nk-component')?.dispatchEvent( new CustomEvent('processSelected', { detail: { process: process, }, }), ); } } (window as any).goToProcessPage = goToProcessPage; async function createMessagingProcess(): Promise { console.log('Creating messaging process'); const service = await Services.getInstance(); const otherMembers = [ { sp_addresses: [ "tsp1qqd7snxfh44am8f7a3x36znkh4v0dcagcgakfux488ghsg0tny7degq4gd9q4n4us0cyp82643f2p4jgcmtwknadqwl3waf9zrynl6n7lug5tg73a", "tsp1qqvd8pak9fyz55rxqj90wxazqzwupf2egderc96cn84h3l84z8an9vql85scudrmwvsnltfuy9ungg7pxnhys2ft5wnf2gyr3n4ukvezygswesjuc" ] }, { sp_addresses: [ "tsp1qqgl5vawdey6wnnn2sfydcejsr06uzwsjlfa6p6yr8u4mkqwezsnvyqlazuqmxhxd8crk5eq3wfvdwv4k3tn68mkj2nj72jj39d2ngauu4unfx0q7", "tsp1qqthmj56gj8vvkjzwhcmswftlrf6ye7ukpks2wra92jkehqzrvx7m2q570q5vv6zj6dnxvussx2h8arvrcfwz9sp5hpdzrfugmmzz90pmnganxk28" ] }, { sp_addresses: [ "tsp1qqwjtxr9jye7d40qxrsmd6h02egdwel6mfnujxzskgxvxphfya4e6qqjq4tsdmfdmtnmccz08ut24q8y58qqh4lwl3w8pvh86shlmavrt0u3smhv2", "tsp1qqwn7tf8q2jhmfh8757xze53vg2zc6x5u6f26h3wyty9mklvcy0wnvqhhr4zppm5uyyte4y86kljvh8r0tsmkmszqqwa3ecf2lxcs7q07d56p8sz5" ] } ]; await service.checkConnections(otherMembers); const relayAddress = service.getAllRelays().pop(); if (!relayAddress) { throw new Error('Empty relay address list'); } const feeRate = 1; setTimeout(async () => { const createProcessReturn = await service.createMessagingProcess(otherMembers, relayAddress.spAddress, feeRate); const updatedProcess = createProcessReturn.updated_process.current_process; if (!updatedProcess) { console.error('Failed to retrieved new messaging process'); return; } const processId = updatedProcess.states[0].commited_in; const stateId = updatedProcess.states[0].state_id; await service.handleApiReturn(createProcessReturn); const createPrdReturn = await service.createPrdUpdate(processId, stateId); await service.handleApiReturn(createPrdReturn); const approveChangeReturn = service.approveChange(processId, stateId); await service.handleApiReturn(approveChangeReturn); }, 500) } async function getDescription(processId: string, process: Process): Promise { const service = await Services.getInstance(); // Get the `commited_in` value of the last state and remove it from the array const currentCommitedIn = process.states.pop()?.commited_in; if (currentCommitedIn === undefined) { return null; // No states available } // Find the last state where `commited_in` is different let lastDifferentState = process.states.findLast( state => state.commited_in !== currentCommitedIn ); if (!lastDifferentState) { // It means that we only have one state that is not commited yet, that can happen with process we just created // let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation lastDifferentState = process.states.pop(); } // Take the description out of the state, if any const description = lastDifferentState!.pcd_commitment['description']; if (description) { const userDiff = await service.getDiffByValue(description); if (userDiff) { console.log("Successfully retrieved userDiff:", userDiff); return userDiff.new_value; } else { console.log("Failed to retrieve a non-null userDiff."); } } return null; } async function getProcesses(): Promise { const service = await Services.getInstance(); const processes = await service.getProcesses(); const res = Object.entries(processes).map(([key, value]) => ({ key, value, })); return res; }