satis 2.1.46 → 2.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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