satis 2.1.46 → 2.1.47

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f84e47bc124b6590087e4afa8f9ef40a2dcb31664310001fa3a743aaa1a47f1
4
- data.tar.gz: 87dbbcaccfa0f50446157e77b469303891396686620052827aa223b7a331687a
3
+ metadata.gz: 8b4aa4a7fdd94865ec844e8389cee2d3d19009f817a48d48bdb5e44b1ab7d12f
4
+ data.tar.gz: 6b6eaa2df3376c21b24bd32ef981963c1a6c6a564f6a7ccb2f8ffa8fe51d699f
5
5
  SHA512:
6
- metadata.gz: 8544a8f0d11eaceb756422d707c05e06c381bb5f6e1e86fac5ecd65ea62607d3f917cb463efe4ad6804243709b3d7314fa7c2d607a7c1ce83cc3d9b8bf62ebab
7
- data.tar.gz: 0f449812c72007560734829ebb52a21fcb50e9b569d25fece66b8e2489839e30244f770990309e6142486a70e489c246644a2e89ca8666bbbec5dc3a62616abb
6
+ metadata.gz: a3bf96d40e2775fb1042a21be50b0796db96d424a8bd2414934e246675ac60bb8cdadd55b27cb82b232f9f6d0e1bd961e154cf9e44902f14732dbd747fd23cff
7
+ data.tar.gz: 8ebb4d890726d4e1e3d76b65650091bbafa273e7756c01835f30aec4157e1c488539cd04e971158e96aa7b7651f1c50264ac64cf0ed4daa0c638c8a16bd41d03
@@ -1,4 +1,5 @@
1
1
  @import '../../../components/satis/appearance_switcher/component.css';
2
+ @import '../../../components/satis/attachments/component.css';
2
3
  @import '../../../components/satis/breadcrumbs/component.css';
3
4
  @import '../../../components/satis/card/component.css';
4
5
  @import '../../../components/satis/call_to_action/component.css';
@@ -0,0 +1,136 @@
1
+ .attachments__group {
2
+ @apply grid gap-6 mt-10 justify-start;
3
+ grid-template-columns: repeat(auto-fill, 200px);
4
+ }
5
+
6
+ .attachments__attachment {
7
+ @apply relative w-[200px] h-[200px] bg-cover bg-center bg-no-repeat rounded-lg shadow-md overflow-hidden;
8
+ @apply dark:bg-gray-900;
9
+ }
10
+
11
+ .attachments__attachment:hover {
12
+ @apply opacity-90;
13
+ }
14
+
15
+ .attachments__attachment .attachments__button {
16
+ @apply hidden absolute top-2.5 bg-white bg-opacity-80 p-1.5 rounded-full;
17
+ @apply dark:bg-gray-700 dark:text-white;
18
+ }
19
+
20
+ .attachments__attachment .attachments__button:first-of-type {
21
+ @apply left-2.5;
22
+ }
23
+
24
+ .attachments__attachment .attachments__button:last-of-type {
25
+ @apply right-2.5;
26
+ }
27
+
28
+ .attachments__attachment:hover .attachments__button {
29
+ @apply block;
30
+ }
31
+
32
+ .attachments__attachment .attachments__filename {
33
+ @apply hidden absolute bottom-0 left-0 right-0 bg-black bg-opacity-70 text-white p-1.5 text-center text-xs;
34
+ @apply dark:bg-gray-900 dark:bg-opacity-90;
35
+ }
36
+
37
+ .attachments__attachment:hover .attachments__filename {
38
+ @apply block;
39
+ }
40
+
41
+ .preview-text {
42
+ @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white bg-opacity-70 p-2.5 rounded text-sm text-gray-800;
43
+ @apply dark:bg-gray-800 dark:bg-opacity-70 dark:text-gray-200;
44
+ }
45
+
46
+ @media (max-width: 640px) {
47
+ .attachments__group {
48
+ grid-template-columns: repeat(auto-fill, 150px);
49
+ }
50
+
51
+ .attachments__attachment {
52
+ @apply w-[150px] h-[150px];
53
+ }
54
+ }
55
+
56
+ .icon.uploading {
57
+ display: none; /* Ensure this is hidden by default */
58
+ }
59
+
60
+ .uploading .icon.uploading {
61
+ display: inline-block; /* Show only when uploading */
62
+ }
63
+
64
+ .icon.upload {
65
+ display: inline-block; /* Ensure the upload icon is visible */
66
+ }
67
+
68
+ .uploading .icon.upload {
69
+ display: none; /* Hide the upload icon during upload */
70
+ }
71
+
72
+ .attachment-upload-button {
73
+ display: inline-block;
74
+ }
75
+
76
+ .upload-btn {
77
+ @apply dark:bg-gray-900 dark:text-white;
78
+ align-items: center;
79
+ padding: 10px 15px;
80
+ background-color: #f0f0f0;
81
+ border: 2px dashed #ccc;
82
+ border-radius: 5px;
83
+ cursor: pointer;
84
+ transition: all 0.3s ease;
85
+ font-family: Arial, sans-serif;
86
+ color: #333;
87
+ }
88
+
89
+ .upload-btn:hover {
90
+ @apply dark:bg-gray-900 dark:text-white dark:border-gray-700;
91
+ background-color: #e0e0e0;
92
+ border-color: #999;
93
+ }
94
+
95
+ .upload-btn:focus {
96
+ outline: none;
97
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
98
+ }
99
+
100
+ .upload-btn .icon {
101
+ margin-right: 10px;
102
+ font-size: 18px;
103
+ }
104
+
105
+ .upload-btn .icon.upload {
106
+ color: #4a90e2;
107
+ }
108
+
109
+ .upload-btn .icon.uploading {
110
+ display: none;
111
+ color: #f39c12;
112
+ }
113
+
114
+ .uploading .upload-btn .icon.upload {
115
+ display: none;
116
+ }
117
+
118
+ .uploading .upload-btn .icon.uploading {
119
+ display: inline-block;
120
+ }
121
+
122
+ .upload-btn .button-text {
123
+ font-size: 14px;
124
+ }
125
+
126
+ .dragging .upload-btn {
127
+ background-color: #e8f0fe;
128
+ border-color: #4285f4;
129
+ }
130
+
131
+ .uploading .upload-btn {
132
+ @apply dark:bg-gray-800 dark:border-gray-600 dark:text-white;
133
+ background-color: #fcf8e3;
134
+ border-color: #f39c12;
135
+ cursor: not-allowed;
136
+ }
@@ -0,0 +1,26 @@
1
+ div.attachment-upload.upload-btn data-controller="attachment-upload" data-attachment-upload-url="#{Satis::Engine.routes.url_helpers.attachments_path(sgid: model_sgid, attribute: attribute)}" data-attachment-upload-param-name="attachments[]"data-attachment-upload-extra-data='{}' data-turbo="true" turbo-method="post"
2
+ span.icon.upload
3
+ i.fas.fa-upload
4
+ span.icon.uploading
5
+ i.fal.fa-circle-notch.fa-spin
6
+ span.button-text Drag or click to attach files
7
+
8
+
9
+
10
+ div.attachments__group
11
+ - attachments = model_has_images ? model.images : model.attachments
12
+ - attachments.each do |attachment|
13
+ div.attachments__attachment(style= attachment_style(attachment))
14
+ - unless attachment.representable?
15
+ div.preview-text
16
+ i.fas.fa-file(aria-hidden="true")
17
+
18
+ = link_to Satis::Engine.routes.url_helpers.attachment_path(attachment, sgid: model_sgid, attribute: attribute), data: {turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this attachment?"}, class: 'attachments__button' do
19
+ i.fas.fa-xmark
20
+
21
+
22
+ = link_to Rails.application.routes.url_helpers.rails_blob_url(attachment, host: request.host + ":#{request.port}"), data: { turbo: true, turbolinks: false }, class: 'attachments__button', download: true do
23
+ i.fas.fa-download
24
+
25
+ span.attachments__filename
26
+ = attachment.filename
@@ -0,0 +1,34 @@
1
+ module Satis
2
+ module Attachments
3
+ class Component < Satis::ApplicationComponent
4
+ include Rails.application.routes.url_helpers
5
+
6
+ attr_reader :model, :attribute, :attachments_options
7
+
8
+ def initialize(model, attribute, **options)
9
+ super()
10
+ @model = model
11
+ @attribute = attribute
12
+ @attachments_options = options
13
+ end
14
+
15
+ def model_has_images
16
+ model.respond_to?(:images)
17
+ end
18
+
19
+ def attachment_style(attachment)
20
+ if attachment.representable?
21
+ url = attachment.representation(resize_to_limit: [200, 200]).processed.url
22
+ "background-image: url(#{url})"
23
+ else
24
+ "background-color: f0f0f0"
25
+ end
26
+ end
27
+
28
+
29
+ def model_sgid
30
+ @model.to_sgid(expires_in: nil, for: 'satis_attachments')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.selector 'div.attachments'
4
+ json.html render partial: 'satis/attachments/index', layout: false, formats: [:html],
5
+ locals: { attachments: @model.images, upload_url: polymorphic_path([@model, :attachments]) }
@@ -2,11 +2,11 @@
2
2
  @apply bg-white h-full sm:rounded-lg sm:shadow dark:bg-gray-800 overflow-hidden;
3
3
 
4
4
  &__header {
5
- @apply px-4 py-5 sm:px-6 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-300;
5
+ @apply px-4 py-5 sm:px-6 dark:bg-gray-900 bg-white dark:border-gray-700 dark:text-gray-300;
6
6
  }
7
7
 
8
8
  &__tabs {
9
- @apply bg-white px-4 border-b border-gray-200 sm:px-5 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-300;
9
+ @apply bg-white px-4 border-b border-gray-200 sm:px-5 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-gray-300;
10
10
 
11
11
  a:first {
12
12
  @apply ml-4;
@@ -1,6 +1,6 @@
1
1
  .sts-card data-controller="satis-tabs" data-satis-tabs-persist-value=persist data-satis-tabs-key-value=key id=identifier
2
2
  - if header?
3
- .sts-card__header class="#{tabs? ? '' : 'border-b border-gray-200'} #{header_background_color[:light]} dark:#{header_background_color[:dark]}"
3
+ .sts-card__header class="#{tabs? ? '' : 'border-b border-gray-200'}"
4
4
  .-ml-4.-mt-4.flex.justify-between.items-center.flex-wrap.sm:flex-nowrap
5
5
  - if icon
6
6
  .ml-4.mt-4.flex-shrink-0.text-primary-600.dark:text-gray-300
@@ -520,9 +520,7 @@ export default class DropdownComponentController extends ApplicationController {
520
520
  let matches = []
521
521
  this.itemTargets.forEach((item) => {
522
522
  const text = item.getAttribute("data-satis-dropdown-item-text")
523
- const matched = this.needsExactMatchValue ?
524
- searchValue.localeCompare(text, undefined, {sensitivity: 'base'}) === 0:
525
- new RegExp(searchValue, "i").test(text)
523
+ const matched = this.needsExactMatchValue ? searchValue.localeCompare(text, undefined, {sensitivity: 'base'}) === 0 : text.toLowerCase().includes(searchValue.toLowerCase())
526
524
 
527
525
  const isHidden = item.classList.contains("hidden")
528
526
  if (!isHidden) {
@@ -622,8 +620,7 @@ export default class DropdownComponentController extends ApplicationController {
622
620
  this.itemTargets.forEach((item) => {
623
621
  const text = item.getAttribute("data-satis-dropdown-item-text")
624
622
  const matched = this.needsExactMatchValue
625
- ? searchValue.localeCompare(text, undefined, { sensitivity: "base" }) === 0
626
- : new RegExp(searchValue, "i").test(text)
623
+ ? searchValue.localeCompare(text, undefined, { sensitivity: "base" }) === 0 : text.toLowerCase().includes(searchValue.toLowerCase())
627
624
 
628
625
  const isHidden = item.classList.contains("hidden")
629
626
  if (!isHidden) {
@@ -3,7 +3,7 @@
3
3
  }
4
4
 
5
5
  .cm-editor{
6
- @apply dark:bg-gray-900 dark:border dark:border-opacity-25 dark:border-gray-50 ;
6
+ @apply dark:bg-gray-900 dark:border dark:border-opacity-25 dark:text-white dark:border-gray-50 ;
7
7
  }
8
8
 
9
9
  .cm-editor .cm-gutter{
@@ -39,6 +39,10 @@
39
39
  display: none;
40
40
  }
41
41
 
42
+ .sidebar.close .sts-sidebar-menu-item:hover > .sts-sidebar-menu-item__link {
43
+ width: 40px;
44
+ }
45
+
42
46
 
43
47
  .sidebar.close .sts-sidebar-menu-item [data-satis-sidebar-menu-item-target="submenu"] .sts-sidebar-menu-item{
44
48
  display: none;
@@ -5,7 +5,7 @@
5
5
  - tabs.each do |tab|
6
6
  option selected=tab.selected? = ct(".#{tab.name}", scope: :tab)
7
7
  .hidden.sm:block
8
- .border-b.border-gray-200.dark:border-opacity-25.dark:bg-gray-900.dark:bg-opacity-50
8
+ .border-b.border-gray-200.bg-white.dark:border-opacity-25.dark:bg-gray-900
9
9
  nav.sts-tabs__nav aria-label="Tabs"
10
10
  - tabs.each.with_index do |tab, index|
11
11
  - id = tab.id.present? ? tab.id : tab.name
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Satis
4
+ class AttachmentsController < ApplicationController
5
+ before_action :set_objects
6
+
7
+
8
+ def index
9
+ @attachments = @model.public_send(@attachment_type)
10
+ render json: @attachments
11
+ end
12
+
13
+ def create
14
+ params[:attachments].each do |file|
15
+ @model.public_send(@attachment_type).attach(file)
16
+ end
17
+ redirect_to request.referer || root_path, notice: "Attachment created successfully."
18
+
19
+ end
20
+
21
+ def destroy
22
+ attachment = @model.public_send(@attachment_type).find_by(id: params[:id])
23
+ attachment&.purge
24
+
25
+ redirect_to request.referer || root_path, notice: "Attachment deleted successfully."
26
+ end
27
+
28
+ private
29
+
30
+ def set_objects
31
+ @attachment_type = params[:attribute] || 'attachments'
32
+ @model = GlobalID::Locator.locate_signed(params[:sgid], for: 'satis_attachments')
33
+ end
34
+ end
35
+ end
@@ -3,13 +3,5 @@ module Satis
3
3
  def sts
4
4
  @_satis_helpers_container ||= Satis::Helpers::Container.new(self)
5
5
  end
6
-
7
- def method_missing(method, *args, **kwargs, &block)
8
- if method.to_s.ends_with?('_url') || method.to_s.ends_with?('_path') && main_app.respond_to?(method)
9
- main_app.send(method, *args, **kwargs, &block)
10
- else
11
- super
12
- end
13
- end
14
6
  end
15
7
  end
@@ -0,0 +1,49 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import AttachmentUploadController from "./attachment_upload_controller";
3
+ import { get } from '@rails/request.js'
4
+
5
+ /***
6
+ * Delete attachments controller
7
+ *
8
+ * Deletes an attachments
9
+ */
10
+ export default class AttachmentDeleteController extends Controller {
11
+ connect() {}
12
+
13
+ delete(event) {
14
+ const self = this
15
+
16
+ event.stopPropagation()
17
+ event.preventDefault()
18
+
19
+ const formData = new FormData()
20
+ formData.append("_method", "DELETE")
21
+
22
+ fetch(self.element.getAttribute("href"), {
23
+ method: "POST",
24
+ headers: {
25
+ "X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content,
26
+ },
27
+ body: formData,
28
+ })
29
+ .then(self.handleErrors)
30
+ .then((response) => {
31
+ window.location.reload(true);
32
+ response.json().then(function (data) {
33
+ let node = document.querySelector(data.selector)
34
+ if (node) {
35
+ node.innerHTML = data.html
36
+ }
37
+ })
38
+ })
39
+ .catch((error) => {
40
+ console.log(error)
41
+ })
42
+ return false
43
+ }
44
+
45
+ handleErrors(response) {
46
+ if (!response.ok) throw new Error(response.status)
47
+ return response
48
+ }
49
+ }
@@ -0,0 +1,109 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class AttachmentUploadController extends Controller {
4
+ static targets = ["input"]
5
+
6
+ connect() {
7
+ this.createFileInput()
8
+ this.addEventListeners()
9
+ }
10
+
11
+ createFileInput() {
12
+ const input = document.createElement("input")
13
+ input.setAttribute("name", this.data.get("param-name") || "file")
14
+ input.setAttribute("type", "file")
15
+ input.setAttribute("multiple", "multiple")
16
+ input.style.display = "none"
17
+ this.element.appendChild(input)
18
+ this.fileInput = input
19
+
20
+ if (!this.data.has("param-name")) {
21
+ console.warn(this.element, "has no data-upload-param attribute, uploads may not work")
22
+ }
23
+ }
24
+
25
+ addEventListeners() {
26
+ this.element.addEventListener("click", this.handleClick.bind(this))
27
+ this.fileInput.addEventListener("change", this.handleChange.bind(this))
28
+ this.element.addEventListener("dragover", this.handleDragOver.bind(this))
29
+ this.element.addEventListener("dragleave", this.handleDragLeave.bind(this))
30
+ this.element.addEventListener("dragenter", this.handleDragEnter.bind(this))
31
+ this.element.addEventListener("drop", this.handleDrop.bind(this))
32
+ }
33
+
34
+ handleClick(event) {
35
+ event.preventDefault()
36
+ this.fileInput.click()
37
+ }
38
+
39
+ handleChange(event) {
40
+ this.upload(event.target.files)
41
+ }
42
+
43
+ handleDragOver(event) {
44
+ event.preventDefault()
45
+ this.element.classList.add("dragging")
46
+ }
47
+
48
+ handleDragLeave(event) {
49
+ event.preventDefault()
50
+ this.element.classList.remove("dragging")
51
+ }
52
+
53
+ handleDragEnter(event) {
54
+ event.preventDefault()
55
+ this.element.classList.add("dragging")
56
+ }
57
+
58
+ handleDrop(event) {
59
+ event.preventDefault()
60
+ this.element.classList.remove("dragging")
61
+ if (event.dataTransfer.files.length > 0) {
62
+ this.upload(event.dataTransfer.files)
63
+ }
64
+ }
65
+
66
+ upload(files) {
67
+ // Only proceed if files are selected
68
+ if (files.length === 0) return
69
+
70
+ let formData = new FormData()
71
+ if (this.data.has("extra-data")) {
72
+ for (let [key, value] of Object.entries(JSON.parse(this.data.get("extra-data")))) {
73
+ formData.append(key, value)
74
+ }
75
+ }
76
+
77
+ for (let i = 0; i < files.length; i++) {
78
+ formData.append(this.data.get("param-name"), files[i])
79
+ }
80
+
81
+ this.element.classList.add("uploading")
82
+
83
+ fetch(this.data.get("url"), {
84
+ method: 'POST',
85
+ body: formData,
86
+ headers: {
87
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
88
+ 'Accept': 'text/html, application/json'
89
+ },
90
+ redirect: 'follow' // Important: follow redirects
91
+ }).then((response) => {
92
+ // Check if the response is a redirect
93
+ if (response.type === 'opaqueredirect' || response.redirected) {
94
+ window.location.href = response.url
95
+ return
96
+ }
97
+
98
+ if (response.ok) {
99
+ this.element.classList.remove("uploading")
100
+ window.location.reload(true)
101
+ } else {
102
+ throw new Error(response.statusText)
103
+ }
104
+ }).catch((error) => {
105
+ console.log(error)
106
+ this.element.classList.remove("uploading")
107
+ })
108
+ }
109
+ }
@@ -56,6 +56,13 @@ application.register("satis-link", LinkController);
56
56
  import FieldsForController from "satis/controllers/fields_for_controller";
57
57
  application.register("satis-fields-for", FieldsForController);
58
58
 
59
+ import AttachmentUploadController from "satis/controllers/attachment_upload_controller";
60
+ application.register("satis-fields-for", AttachmentUploadController);
61
+
62
+ import AttachmentDeleteController from "satis/controllers/attachment_delete_controller";
63
+ application.register("satis-fields-for", AttachmentDeleteController);
64
+
65
+
59
66
  import FormController from "satis/controllers/form_controller";
60
67
  application.register("satis-form", FormController);
61
68
 
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  Satis::Engine.routes.draw do
2
2
  resources :user_data, only: %i[show update]
3
3
  resources :dialogs, only: %[show], constraints: { id: /[A-Za-z0-9\_\-\/]+/ }
4
+ resources :attachments, only: [:index, :create, :destroy]
4
5
 
5
6
  unless Rails.env.production?
6
7
  namespace :documentation do
@@ -15,3 +16,4 @@ Satis::Engine.routes.draw do
15
16
  resources :documentation
16
17
  end
17
18
  end
19
+
@@ -20,6 +20,7 @@ module Satis
20
20
  add_helper :menu, Satis::Menu::Component
21
21
  add_helper :page, Satis::Page::Component
22
22
  add_helper :sidebar_menu, Satis::SidebarMenu::Component
23
+ add_helper :attachments, Satis::Attachments::Component
23
24
  add_helper :tabs, Satis::Tabs::Component
24
25
  add_helper :input, Satis::Input::Component
25
26
  add_helper :progress_bar, Satis::ProgressBar::Component
data/lib/satis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Satis
2
- VERSION = "2.1.46"
2
+ VERSION = "2.1.47"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: satis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.46
4
+ version: 2.1.47
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-09 00:00:00.000000000 Z
11
+ date: 2025-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: browser
@@ -751,6 +751,10 @@ files:
751
751
  - app/components/satis/appearance_switcher/component.rb
752
752
  - app/components/satis/appearance_switcher/component_controller.js
753
753
  - app/components/satis/application_component.rb
754
+ - app/components/satis/attachments/component.css
755
+ - app/components/satis/attachments/component.html.slim
756
+ - app/components/satis/attachments/component.rb
757
+ - app/components/satis/attachments/create.json.jbuilder
754
758
  - app/components/satis/avatar/component.html.slim
755
759
  - app/components/satis/avatar/component.rb
756
760
  - app/components/satis/breadcrumbs/component.css
@@ -853,6 +857,7 @@ files:
853
857
  - app/components/satis/tabs/component.rb
854
858
  - app/components/satis/tabs/component_controller.js
855
859
  - app/controllers/satis/application_controller.rb
860
+ - app/controllers/satis/attachments_controller.rb
856
861
  - app/controllers/satis/dialogs_controller.rb
857
862
  - app/controllers/satis/documentation/avatars_controller.rb
858
863
  - app/controllers/satis/documentation/cards_controller.rb
@@ -866,6 +871,8 @@ files:
866
871
  - app/javascript/satis/application.js
867
872
  - app/javascript/satis/controllers/application.js
868
873
  - app/javascript/satis/controllers/application_controller.js
874
+ - app/javascript/satis/controllers/attachment_delete_controller.js
875
+ - app/javascript/satis/controllers/attachment_upload_controller.js
869
876
  - app/javascript/satis/controllers/fields_for_controller.js
870
877
  - app/javascript/satis/controllers/file_controller.js
871
878
  - app/javascript/satis/controllers/form_controller.js