Plugin data not received by server side hook

Hello,

I am working on a plugin which requires a multi-select option field with the list of drop down option and search feature.

To achieve that I used the default HTML select with commonOptions and registerVideoField as provided by PeerTube. The registereVideoField will inject the HTML in the update page of the video so that on updating the video the plugin server hook 'action:api.video.updated' will get the data of the field in it’s body.pluginData.
I was able to inject the multiselect option with search feature but on updating the video the plugin server hook 'action:api.video.updated' only gets one element from the list of selected items from the dropdown in to its body.pluginData. Since I have made the field multi-select with setting its attribute to multiple it is not behaving like one.

Here is my implementation:


Screenshot_6

On updating the video the body.pluginData gets only the first item.

Here is my code:

  async function formFieldElement() {
    let commonOptions = {
      name: "allowed-users",
      label: "Allow Users",
      descriptionHTML: "",
      type: "select",
      options: [],
      default: "",
    };
    let response = await fetch(
      peertubeHelpers.getBaseRouterRoute() + "/userlist",
      {
        method: "GET",
        headers: peertubeHelpers.getAuthHeader(),
      }
    );
    let data = await response.json();
    data.forEach((user) => {
      commonOptions.options.push({
        value: user.username,
        label: user.username,
      });
    });
    return commonOptions;
  }


  const videoFormOptions = {
    tab: "main",
  };

  const commonOptions = await formFieldElement()

    for  (const type of [
      "upload",
      "import-url",
      "import-torrent",
      "update",
      "go-live",
    ]) {
      registerVideoField(commonOptions, { type, ...videoFormOptions });
    }


  function waitForElm(selector) {
    return new Promise((resolve) => {
      if (document.getElementById(selector)) {
        return resolve(document.getElementById(selector));
      }

      const observer = new MutationObserver((mutations) => {
        if (document.getElementById(selector)) {
          resolve(document.getElementById(selector));
          observer.disconnect();
        }
      });

      observer.observe(document.querySelector("my-dynamic-form-field"), {
        childList: true,
        subtree: true,
        attributes: true,
      });
    });
  }

  function addElem(elem) {
    elem.setAttribute("multiple", "");
    elem.setAttribute("multiselect-search", "true");
    elem.setAttribute("multiselect-select-all", "true");
    elem.setAttribute("multiselect-max-items", "3");
    MultiselectDropdown(window.MultiselectDropdownOptions);
  }

  registerHook({
    target: "action:video-edit.init",
    handler: () => {
      waitForElm("allowed-users").then((elem) => {
        addElem(elem);
      });
      const tabElem = document.querySelector("div.nav-tabs.nav");
      tabElem.onclick = function () {
        const checkActiveClass =
          tabElem.children[0].classList.contains("active");
        if (checkActiveClass) {
          waitForElm("allowed-users").then((elem) => {
            addElem(elem);
          });
        }
      };
    },
  });
1 « J'aime »

Hi,

What is MultiselectDropdown component?

MultiselectDropdown component adds the search feature to the HTML select.

Here is my code:

function MultiselectDropdown(options){
      const config={
        search:true,
        height:'15rem',
        placeholder:'select',
        txtSelected:'selected',
        txtAll:'All',
        txtRemove: 'Remove',
        txtSearch:'search',
        ...options
      };
      function newEl(tag,attrs){
        const e=document.createElement(tag);
        if(attrs!==undefined) Object.keys(attrs).forEach(k=>{
          if(k==='class') { Array.isArray(attrs[k]) ? attrs[k].forEach(o=>o!==''?e.classList.add(o):0) : (attrs[k]!==''?e.classList.add(attrs[k]):0)}
          else if(k==='style'){  
            Object.keys(attrs[k]).forEach(ks=>{
              e.style[ks]=attrs[k][ks];
            });
           }
          else if(k==='text'){attrs[k]===''?e.innerHTML=' ':e.innerText=attrs[k]}
          else e[k]=attrs[k];
        });
        return e;
      }
    
      console.log('hello', document.querySelectorAll("select[multiple]"))
      document.querySelectorAll("select[multiple]").forEach((el,k)=>{
        
        const div=newEl('div',{class:'multiselect-dropdown',style:{width:config.style?.width??el.clientWidth+'px',padding:config.style?.padding??''}});
        el.style.display='none';
        el.parentNode.insertBefore(div,el.nextSibling);
        const listWrap=newEl('div',{class:'multiselect-dropdown-list-wrapper'});
        const list=newEl('div',{class:'multiselect-dropdown-list',style:{height:config.height}});
        const search=newEl('input',{class:['multiselect-dropdown-search'].concat([config.searchInput?.class??'form-control']),style:{width:'100%',display:el.attributes['multiselect-search']?.value==='true'?'block':'none'},placeholder:config.txtSearch});
        listWrap.appendChild(search);
        div.appendChild(listWrap);
        listWrap.appendChild(list);
    
        el.loadOptions=()=>{
          list.innerHTML='';
          
          if(el.attributes['multiselect-select-all']?.value=='true'){
            const op=newEl('div',{class:'multiselect-dropdown-all-selector'})
            const ic=newEl('input',{type:'checkbox'});
            op.appendChild(ic);
            op.appendChild(newEl('label',{text:config.txtAll}));
      
            op.addEventListener('click',()=>{
              op.classList.toggle('checked');
              op.querySelector("input").checked=!op.querySelector("input").checked;
              
              const ch=op.querySelector("input").checked;
              list.querySelectorAll(":scope > div:not(.multiselect-dropdown-all-selector)")
                .forEach(i=>{if(i.style.display!=='none'){i.querySelector("input").checked=ch; i.optEl.selected=ch}});
      
              el.dispatchEvent(new Event('change'));
            });
            ic.addEventListener('click',(ev)=>{
              ic.checked=!ic.checked;
            });
            el.addEventListener('change', (ev)=>{
              let itms=Array.from(list.querySelectorAll(":scope > div:not(.multiselect-dropdown-all-selector)")).filter(e=>e.style.display!=='none')
              let existsNotSelected=itms.find(i=>!i.querySelector("input").checked);
              if(ic.checked && existsNotSelected) ic.checked=false;
              else if(ic.checked==false && existsNotSelected===undefined) ic.checked=true;
            });
      
            list.appendChild(op);
          }
    
          Array.from(el.options).map(o=>{
            const op=newEl('div',{class:o.selected?'checked':'',optEl:o})
            const ic=newEl('input',{type:'checkbox',checked:o.selected});
            op.appendChild(ic);
            op.appendChild(newEl('label',{text:o.text}));
    
            op.addEventListener('click',()=>{
              op.classList.toggle('checked');
              op.querySelector("input").checked=!op.querySelector("input").checked;
              op.optEl.selected=!!!op.optEl.selected;
              el.dispatchEvent(new Event('change'));
            });
            ic.addEventListener('click',(ev)=>{
              ic.checked=!ic.checked;
            });
            o.listitemEl=op;
            list.appendChild(op);
          });
          div.listEl=listWrap;
    
          div.refresh=()=>{
            div.querySelectorAll('span.optext, span.placeholder').forEach(t=>div.removeChild(t));
            const sels=Array.from(el.selectedOptions);
            if(sels.length>(el.attributes['multiselect-max-items']?.value??5)){
              div.appendChild(newEl('span',{class:['optext','maxselected'],text:sels.length+' '+config.txtSelected}));          
            }
            else{
              sels.map(x=>{
                const c=newEl('span',{class:'optext',text:x.text, srcOption: x});
                if((el.attributes['multiselect-hide-x']?.value !== 'true'))
                  c.appendChild(newEl('span',{class:'optdel',text:'🗙',title:config.txtRemove, onclick:(ev)=>{c.srcOption.listitemEl.dispatchEvent(new Event('click'));div.refresh();ev.stopPropagation();}}));
    
                div.appendChild(c);
              });
            }
            if(0==el.selectedOptions.length) div.appendChild(newEl('span',{class:'placeholder',text:el.attributes['placeholder']?.value??config.placeholder}));
          };
          div.refresh();
        }
        el.loadOptions();
        
        search.addEventListener('input',()=>{
          list.querySelectorAll(":scope div:not(.multiselect-dropdown-all-selector)").forEach(d=>{
            const txt=d.querySelector("label").innerText.toUpperCase();
            d.style.display=txt.includes(search.value.toUpperCase())?'block':'none';
          });
        });
    
        div.addEventListener('click',()=>{
          div.listEl.style.display='block';
          search.focus();
          search.select();
        });
        
        document.addEventListener('click', function(event) {
          if (!div.contains(event.target)) {
            listWrap.style.display='none';
            div.refresh();
          }
        });    
      });
    }


module.exports = {
    MultiselectDropdown
}```

Unfortunately I think we’re not compatible with select[multiple], because select.value (which is used by peertube) only returns one element.

1 « J'aime »

@Chocobozzz I also tried by setting the type as input in the commonOptions with using a library called tagify. This library transforms an input field into a Tags component.

The value from the tagify are automatically mapped to the default input element so making it as what peertube expects.On logging the value of the input field on the console we get what we required but on updating none value gets captured by the body.pluginData in the server side. I don’t know what to do here.


Code:

 const commonOptions = {
    name: "allowed-users",
    label: "Allow Users",
    descriptionHTML: "",
    type: "input",
    default: "",
  };

  const videoFormOptions = {
    tab: "main",
  };

  
  for (const type of [
    "upload",
    "import-url",
    "import-torrent",
    "update",
    "go-live",
  ]) {
    registerVideoField(commonOptions, { type, ...videoFormOptions });
  }
  
  function waitForElm(selector) {
    return new Promise((resolve) => {
      if (document.getElementById(selector)) {
        return resolve(document.getElementById(selector));
      }
      
      const observer = new MutationObserver((mutations) => {
        if (document.getElementById(selector)) {
          resolve(document.getElementById(selector));
          observer.disconnect();
        }
      });
      
      observer.observe(document.querySelector("my-dynamic-form-field"), {
        childList: true,
        subtree: true,
        attributes: true,
      });
    });
  }
  
  async function addElem(elem) { 
     elem.setAttribute("name", "tags");
    elem.setAttribute('placeholder', 'Allowed Users')
    let tagify = new Tagify(elem, {whitelist:[], originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')})
    elem.addEventListener('change', onchange)

    function onchange(e){
      // outputs original input value
      console.log('outputs a String', e.target.value)
    }
    let controller;
    console.log('dom',tagify.DOM)
    tagify.on('input', onInput)
    
      function onInput(e) {
        console.log("onInput: ", elem.value);
       let value = e.detail.value
       tagify.whitelist = null 
       
       controller && controller.abort()
       controller = new AbortController()

       tagify.loading(true).dropdown.hide()

       fetch(peertubeHelpers.getBaseRouterRoute() + "/userlist",
       {
         method: "GET",
         headers: peertubeHelpers.getAuthHeader(),
       }, {signal: controller.signal})
       .then(res => res.json())
       .then(function(newWhitelist){
        console.log('new data', newWhitelist)
        tagify.whitelist = newWhitelist
        tagify.loading(false).dropdown.show(value)
       })
      }

      
  }

  registerHook({
    target: "action:video-edit.init",
    handler: () => {
      waitForElm("allowed-users").then((elem) => {
        addElem(elem);
      });

      const tabElem = document.querySelector("div.nav-tabs.nav");
      tabElem.onclick = function () {
        const checkActiveClass =
          tabElem.children[0].classList.contains("active");
        if (checkActiveClass) {
          waitForElm("allowed-users").then((elem) => {
            addElem(elem);
          });
        }
      };
    },
  });

I’m sorry but I don’t know.

I think we need to develop a dedicated component on peertube side to create a « select multiple » input

@Chocobozzz Will it be added on next release?