Attachments in form

Hi I am creating my own package, I need to create App.ControllForm in a modal window that will have options to add attachments, what api should I use and form tags.
currently I have it done like this

class AssetModal extends App.ControllerModal
  head: __('Model')
  shown: true
  buttonSubmit: true
  buttonCancel: true
  large: true

  content: ->
    false

  render: ->
    super
    @form_id = App.ControllerForm.formId()

    content = $(App.view('asset/form')())

    @form = new App.ControllerForm(
      el: content.find('.js-form')
      model:
        configure_attributes: [
          { name: 'name', display: __('Nazwa'), tag: 'input', null: false, },
          { name: 'serial_number', display: __('Numer seryjny'), tag: 'input',null: false },
          { name: 'organization_id', display: __('Organization'), tag: 'autocompletion_ajax_customer_organization', multiple: false, null: false, relation: 'Organization', },
          { name: 'user_ids', display: __('User'), tag: 'autocompletion_ajax', relation: 'User', null: true, },
          { name: 'invoice', display: __('Faktura'), tag: 'file_upload',form_id: @form_id, class: 'input', null: true,  },
          { name: 'attachments', display: __('Załączniki'), tag: 'file_upload', form_id: @form_id, class: 'input', null: true,  },
          { name: 'asset_model_id', display: __('Aktywo'), tag: 'autocompletion_ajax', relation: 'AssetModel', null: true, },
          { name: 'seller_model_id', display: __('Sprzedawca'), tag: 'autocompletion_ajax', relation: 'SellerModel', null: true,},
          { name: 'purchase_date', display: __('Data kupna'), tag: 'date', class: 'input', null: true },
          { name: 'purchase_cost', display: __('Koszt kupna'), tag: 'decimal', null: true},
          { name: 'description', display: __('Opis'), tag: 'textarea',upload:true, class: 'input', null: true},
        ]
      params: @data
    )

    callback = (data) =>
      @objects = data.items
      return if data.items.length >= data.total_count

    @el.find('.modal-body').html(content)
      
  onSubmit: (e) =>
    params = @formParam(e.target)
    error = @form.validate(params)

    if error
      @formValidate(form: e.target, errors: error)
      return false

    isEditing = Boolean(@data)
  
    if isEditing
      @ajax({
      id: 'assets_edit',
      type: 'PUT',
      url: "#{@apiPath}/assets/#{@data.id}",
      data: JSON.stringify(params),
      processData: true,
      success: (data) =>
        @close()
        @successCallback()
      })
      return
    @ajax({
      id: 'assets_add',
      type: 'POST',
      url: "#{@apiPath}/assets/",
      data: JSON.stringify(params),
      processData: true,
      success: (data) =>
        @close()
        @successCallback()
    })

# coffeelint: disable=camel_case_classes
class App.UiElement.file_upload
  @render: (attributeConfig, params, form) ->
    console.log form
    attribute = $.extend(true, {}, attributeConfig)
    attachments = []
    item = $( App.view('generic/file_upload')(attribute: attribute) )
    @form_id = attribute.form_id

    renderFile = (file) ->
      item.find('.attachments').append(App.view('generic/attachment_item')(file))
      attachments.push file

    item.find('.attachments').on('click', '.js-delete', (e) ->
      id = $(e.currentTarget).data('id')
      attachments = _.filter(
        attachments,
        (item) ->
          return if item.id.toString() is id.toString()
          item
      )


      # delete attachment from storage
      App.Ajax.request(
        type:        'DELETE'
        url:         "#{App.Config.get('api_path')}/asset_attachments/#{@form_id}"
        processData: false
      )

      element = $(e.currentTarget).closest('.attachments')
      $(e.currentTarget).closest('.attachment').remove()
      if element.find('.attachment').length == 0
        element.empty()
    )

    new App.Html5Upload(
      uploadUrl:              "#{App.Config.get('api_path')}/asset_attachments/"
      dropContainer:          item.find("[name=#{attribute.name}]")
      cancelContainer:        item.find('.js-cancel')
      inputField:             item.find('input')
      data:
        form_id: @form_id
      onFileCompletedCallback: (response) ->
        renderFile(response.data)
      attachmentPlaceholder: item.find('.attachmentPlaceholder')
      attachmentUpload:      item.find('.attachmentUpload')
      progressBar:           item.find('.attachmentUpload-progressBar')
      progressText:          item.find('.js-percentage')
    ).render()
    item
class AssetAttachmentsController < ApplicationController
  prepend_before_action :authentication_check, except: %i[show destroy]
  prepend_before_action :authentication_check_only, only: %i[show destroy]

  def show

    view_type = params[:preview] ? 'preview' : nil
    send_data(
      download_file.content(view_type),
      filename:    download_file.filename,
      type:        download_file.content_type,
      disposition: download_file.disposition
    )
  end

  def create
    file = params[:File]
    content_type = file.content_type

    if !content_type || content_type == 'application/octet-stream'
      content_type = if MIME::Types.type_for(file.original_filename).first
                       MIME::Types.type_for(file.original_filename).first.content_type
                     else
                       'application/octet-stream'
                     end
    end

    headers_store = {
      'Content-Type' => content_type
    }

    store = Store.create!(
      object:      'Asset::Attachemnt',
      o_id:        params[:form_id],
      data:        file.read,
      filename:    file.original_filename,
      preferences: headers_store
    )

    render json: {
      success: true,
      data:    {
        id:          store.id,
        filename:    file.original_filename,
        size:        store.size,
        contentType: store.preferences['Content-Type']
      }
    }
  end

  def destroy
    Store.remove_item(download_file.id)

    render json: {
      success: true,
    }
  end

  def destroy_form
    Store.remove(
      object: 'Asset::Attachemnt',
      o_id:   params[:form_id],
    )

    render json: {
      success: true,
    }
  end

  private

  def user_not_authorized(e)
    not_found(e)
  end
end

Hi @Caufland ,

I think the issue with your questions are that you are posting very big code snippets which are incomplete and not testable. If you need help you should try to break down your code in a small working code snippet which other developers can implement and test. Even if they are willing to help you, I don’t think that with your code examples this is even possible. And so is my answer, but I will try to give you some infos about the handling of attachments in zammad.

If you want to use attachments zammad-like I think they are working via form_id.

Normally a form has a form_id. It is used to connect attachments to the form you are using to create an object. Zammad is storing the attachments temporarily over this controller:

zammad/app/controllers/upload_caches_controller.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

With that it is possible to save the current temp state of the creation or the update related to a form_id. On creation of the object then the form_id is used to read the temp data from the upload cache. An example can be found here:
zammad/app/controllers/concerns/creates_ticket_articles.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

For the model itself AFAIK there is a concern which is helping for the handling of the attachments:
zammad/app/models/application_model/has_attachments.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

Hope this helps a bit.

Thanks for the answer, next time I will try to improve as to the construction of my questions. I have a question about the uploadCache controller because I don’t see in the code that it has anywhere where it adds items to postgres and yet this happens is it normal behavior or not?

The upload cache controller uses the UploadCache:

zammad/app/controllers/upload_caches_controller.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

zammad/app/controllers/upload_caches_controller.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

This is a lib:
zammad/lib/upload_cache.rb at 45c9b70f98e952ec8d9dc93009c3ff4fac5468d3 · zammad/zammad · GitHub

And the foundation of that are the Store & Store::File.

You should find this also in your Postgres, e.g. If you upload a file in the ticket creation mask:

`

Thanks, everything is correct. And I have one more problem I’m trying to add a preview as the user has in the left menu and I was able to do it only after unviewing the page the preview keeps loading and when I enter the page and change the tab it loads without any problem I get an ERROR, cant find

organization_id App.Organization.find(2) for ‘Asset’ TEest123

error in the console I don’t know what’s going on because I have the relationship done in the asset class but I don’t have it done in the organization class and is there any other option than editing the base organization file because I suspect that’s the problem

Preview works without a problem when I enter it right away:
image

And this is what it looks like after refreshing the page (and I get that error what I stated above):
image

class App.AssetOverview extends App.Controller
  @requiredPermission: ['ticket.agent', 'admin.user']

  constructor: (params) ->
    super
    App.Asset.full(@asset_id, @render)

  meta: =>
    meta =
      url: @url()
      id:  @asset_id

    if App.Asset.exists(@asset_id)
      asset = App.Asset.find(@asset_id)
      icon = asset.icon()

      meta.head       = asset.displayName()
      meta.title      = asset.displayName()
      meta.iconClass  = icon
      meta.active     = true
    meta

  url: =>
    '#orders/assets/overview/' + @asset_id

  show: =>
    App.OnlineNotification.seen('AssetOverview', @asset_id)
    @navupdate(url: '#', type: 'menu')

  changed: ->
    false

  render: (asset) =>
    if !@doNotLog
      @doNotLog = 1
      @recentView('AssetOverview', @asset_id)

    elLocal = $(App.view('asset_overview/index')())

    new App.AssetOverviewTabs(
      el = elLocal
    );

    @html elLocal

    new App.UpdateTastbar(
      genericObject: asset
    )

  setPosition: (position) =>
    @$('.assetOverview').scrollTop(position)

  currentPosition: =>
    @$('.assetOverview').scrollTop()

class Router extends App.ControllerPermanent
  @requiredPermission: ['admin.assets']

  constructor: (params) ->
    super

    # check authentication
    @authenticateCheckRedirect()

    # cleanup params
    clean_params = 
      asset_id: params.asset_id

    App.TaskManager.execute(
      key: "AssetOverview-#{@asset_id}"
      controller: 'AssetOverview'
      params: clean_params
      show: true
    )

App.Config.set('orders/assets/overview/:asset_id', Router, 'Routes')