factory_bot_instrumentation 2.8.0 → 3.0.0

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: 34f93a962c222be8a01f0f232f447f20f030515c417525d4a9738c4a98f7c534
4
- data.tar.gz: dde124c7cf487fbf8dd5863a606f3d76b95e9879be5b0a2dc6ec1eb56cbf48e9
3
+ metadata.gz: 3a38fbf7fbe98fed861a4fc524a10e5798e8e1abc918c742272365b11c476e81
4
+ data.tar.gz: a9b886fd2f52d856c264b34d8b2d4297df8ea12d1bb6bf1f84b1bbb6480b54ce
5
5
  SHA512:
6
- metadata.gz: e7f7820d29ba4f4f0a58620ae922d5e81c123fda95043cbe0ef88b48a1ba55a38d898b637f4620ba0625782acfab201ebe251d492744485d5eb394aa5c5fcc70
7
- data.tar.gz: fd60aeb4b353e3d54845f71723bb0892a522c799274af2be0978c2f4c26f1099bd72688566361eadff73fa2a8cd1f22c1906801494472fb29b3b9fb7e6702443
6
+ metadata.gz: 0bbbb1aaad25753d40cd97dfdd968c9935cfc9d6aded00ceb39aded62067707ebde83364a6b688700bf2cef4c3b43778a3c67c6f54940224a2d6be237aec48bb
7
+ data.tar.gz: 412673861a682f7506c9b56e2be255f56ac66eb604596348de122e29e6aacd6cc54dcbbc6f51ec3eee1dd4c35b30dc2494f96e6b995678245c0d8ac54b275ec1
@@ -26,12 +26,12 @@ jobs:
26
26
  settings: '${{ github.repository }}'
27
27
  target: ci/gem-test
28
28
 
29
- - name: Install Ruby 3.3
29
+ - name: Install Ruby 4.0
30
30
  uses: ruby/setup-ruby@v1
31
31
  with:
32
- ruby-version: 3.3
32
+ ruby-version: 4.0
33
33
  bundler-cache: true
34
- rubygems: '3.7.2'
34
+ rubygems: '4.0.11'
35
35
 
36
36
  - name: Switch to SSH remotes for the Git repository
37
37
  run: git-ssh-remotes
@@ -18,8 +18,8 @@ jobs:
18
18
  strategy:
19
19
  fail-fast: false
20
20
  matrix:
21
- ruby: ['3.3', '3.4', '4.0']
22
- rails: ['8.0', '8.1']
21
+ ruby: ['4.0']
22
+ rails: ['8.1']
23
23
  env:
24
24
  BUNDLE_GEMFILE: 'gemfiles/rails_${{ matrix.rails }}.gemfile'
25
25
  steps:
@@ -36,7 +36,7 @@ jobs:
36
36
  with:
37
37
  ruby-version: ${{ matrix.ruby }}
38
38
  bundler-cache: true
39
- rubygems: '3.7.2'
39
+ rubygems: '4.0.11'
40
40
 
41
41
  - name: Run the gem tests
42
42
  run: make test
data/.gitignore CHANGED
@@ -11,6 +11,7 @@
11
11
  /gemfiles/vendor/
12
12
  /Gemfile.lock
13
13
  *.gemfile.lock
14
+ /.claude
14
15
 
15
16
  # rspec failure tracking
16
17
  .rspec_status
data/.rubocop.yml CHANGED
@@ -17,8 +17,8 @@ AllCops:
17
17
  NewCops: enable
18
18
  SuggestExtensions: false
19
19
  DisplayCopNames: true
20
- TargetRubyVersion: 3.3
21
- TargetRailsVersion: 8.0
20
+ TargetRubyVersion: 4.0
21
+ TargetRailsVersion: 8.1
22
22
  Exclude:
23
23
  - bin/**/*
24
24
  - vendor/**/*
@@ -39,3 +39,7 @@ Rails/FilePath:
39
39
  # Because we just implemented the ActiveRecord API.
40
40
  Rails/SkipsModelValidations:
41
41
  Enabled: false
42
+
43
+ # Because we don't have a Rails environment here.
44
+ Rails/RakeEnvironment:
45
+ Enabled: false
data/Appraisals CHANGED
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- appraise 'rails-8.0' do
4
- gem 'rails', '~> 8.0.0'
5
- end
6
-
7
3
  appraise 'rails-8.1' do
8
4
  gem 'rails', '~> 8.1.0'
9
5
  end
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  * TODO: Replace this bullet point with an actual description of a change.
4
4
 
5
+ ### 3.0.0 (13 May 2026)
6
+
7
+ * Added automatic asset registering with Sprockets ([#46](https://github.com/hausgold/factory_bot_instrumentation/pull/46))
8
+
9
+ So you can remove the following lines from your `app/assets/config/manifest.js`:
10
+
11
+ ```js
12
+ //= link factory_bot_instrumentation/application.css
13
+ //= link factory_bot_instrumentation/application.js
14
+ ```
15
+
16
+ * Added automatic asset bundling to support Propshaft ([#46](https://github.com/hausgold/factory_bot_instrumentation/pull/46))
17
+
18
+ ### 2.9.0 (4 May 2026)
19
+
20
+ * Dropped Ruby 3.x and Rails <8.1 support ([#45](https://github.com/hausgold/factory_bot_instrumentation/pull/45))
21
+
5
22
  ### 2.8.0 (18 February 2026)
6
23
 
7
24
  * Added gemspec dependencies for all loaded gems ([#44](https://github.com/hausgold/factory_bot_instrumentation/pull/44))
data/Dockerfile CHANGED
@@ -1,8 +1,8 @@
1
- FROM hausgold/ruby:3.3
1
+ FROM hausgold/ruby:4.0
2
2
  LABEL org.opencontainers.image.authors="containers@hausgold.de"
3
3
 
4
4
  # Update system gem
5
- RUN gem update --system '3.7.2'
5
+ RUN gem update --system '4.0.11'
6
6
 
7
7
  # Install system packages and the latest bundler
8
8
  RUN apt-get update -yqqq && \
@@ -11,7 +11,7 @@ RUN apt-get update -yqqq && \
11
11
  ca-certificates \
12
12
  bash-completion inotify-tools && \
13
13
  echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && /usr/sbin/locale-gen && \
14
- gem install bundler -v '~> 2.7.2' --no-document --no-prerelease
14
+ gem install bundler -v '~> 4.0.11' --no-document --no-prerelease
15
15
 
16
16
  # Add new web user
17
17
  RUN mkdir /app && \
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gem 'bundler', '>= 2.6', '< 5'
13
13
  gem 'countless', '~> 2.2'
14
14
  gem 'factory_bot_rails', '~> 6.2'
15
15
  gem 'guard-rspec', '~> 4.7'
16
- gem 'railties', '>= 8.0'
16
+ gem 'railties', '>= 8.1'
17
17
  gem 'rspec-rails', '~> 7.1'
18
18
  gem 'rubocop'
19
19
  gem 'rubocop-rails'
data/Makefile CHANGED
@@ -28,6 +28,7 @@ HEAD ?= head
28
28
  ID ?= id
29
29
  MKDIR ?= mkdir
30
30
  RM ?= rm
31
+ SED ?= sed
31
32
  SORT ?= sort
32
33
  TEST ?= test
33
34
  XARGS ?= xargs
@@ -121,7 +122,7 @@ test-style: \
121
122
  test-style-ruby:
122
123
  # Run the static code analyzer (rubocop)
123
124
  @$(call run-shell,$(BUNDLE) exec $(RUBOCOP) -a \
124
- || ($(TEST) $$($(RUBY_VERSION)) != '3.3' && true))
125
+ || ($(TEST) $$($(RUBY_VERSION)) != '4.0' && true))
125
126
 
126
127
  clean:
127
128
  # Clean the dependencies
@@ -174,6 +175,11 @@ stats:
174
175
  # Print all the notes from the code
175
176
  @$(call run-shell,$(BUNDLE) exec $(RAKE) stats)
176
177
 
177
- release:
178
+ build:
179
+ # Build and prepare the gem for releasing
180
+ @$(call run-shell,$(BUNDLE) exec $(RAKE) bundle_assets)
181
+ @$(SED) -i '/spec.extensions/d' factory_bot_instrumentation.gemspec
182
+
183
+ release: build
178
184
  # Release a new gem version
179
185
  @$(BUNDLE) exec $(RAKE) release
data/Rakefile CHANGED
@@ -11,6 +11,17 @@ require 'countless/rake_tasks'
11
11
  APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
12
12
  load 'rails/tasks/engine.rake'
13
13
 
14
+ desc 'Bundle the engine JavaScript and CSS sources into application.{js,css}'
15
+ task :bundle_assets do
16
+ require 'factory_bot/instrumentation/asset_bundler'
17
+ FactoryBot::Instrumentation::AssetBundler.bundle_all!.each do |kind, files|
18
+ conf = FactoryBot::Instrumentation::AssetBundler.config(kind)
19
+ rel = files.map { |f| f.sub("#{conf[:source_dir]}/", '') }
20
+ puts "Bundled #{files.size} #{kind.upcase} source(s) into " \
21
+ "#{conf[:output_name]}: #{rel.join(', ')}"
22
+ end
23
+ end
24
+
14
25
  desc 'Run all specs in spec directory (excluding plugin specs)'
15
26
  RSpec::Core::RakeTask.new(spec: [
16
27
  'db:drop', 'db:create', 'db:migrate', 'db:setup'
@@ -18,6 +29,14 @@ RSpec::Core::RakeTask.new(spec: [
18
29
 
19
30
  task default: :spec
20
31
 
32
+ # Neuter Bundler's `release:guard_clean` check. The `make build` step prunes
33
+ # the unbundled JS/CSS sources from `app/assets/**/factory_bot_instrumentation/`
34
+ # after bundling them into `application.{js,css}`, which leaves the working
35
+ # tree dirty by design. The commit + tag are already in place before release,
36
+ # so the clean-tree guard would only block pushing the artefact we just built.
37
+ Rake::Task['release:guard_clean'].clear
38
+ task 'release:guard_clean'
39
+
21
40
  # Configure all code statistics directories
22
41
  Countless.configure do |config|
23
42
  config.stats_base_directories = [
@@ -1,16 +1,458 @@
1
- // This is a manifest file that'll be compiled into application.js, which will
2
- // include all the files listed below.
1
+ // !!! AUTO-GENERATED FILE - DO NOT EDIT !!!
3
2
  //
4
- // Any JavaScript/Coffee file within this directory, lib/assets/javascripts,
5
- // vendor/assets/javascripts, or any plugin's vendor/assets/javascripts
6
- // directory can be referenced here using a relative path.
3
+ // This file bundles every JS source under
4
+ // +app/assets/javascripts/factory_bot_instrumentation/+ into a single asset. It is regenerated:
5
+ // * on `gem install` / `bundle install` (via a RubyGems extension
6
+ // hook), and
7
+ // * during gem release (via the `bundle_assets` Rake task).
7
8
  //
8
- // It's not advisable to add code directly here, but if you do, it'll appear at
9
- // the bottom of the compiled file. JavaScript code in this file should be
10
- // added after the last require_* statement.
9
+ // To rebuild it manually, run:
11
10
  //
12
- // Read Sprockets README
13
- // (https://github.com/rails/sprockets#sprockets-directives) for details about
14
- // supported directives.
11
+ // $ bundle exec rake bundle_assets
12
+
13
+ // >>> create.js
14
+ window.CreateForm = CreateForm = function()
15
+ {
16
+ this.scope = '#generate';
17
+ this.form = new Form(this.scope);
18
+ this.scenarios = window.scenarios;
19
+ this.select = $(`${this.scope} .scenario`);
20
+ this.desc = $(`${this.scope} .description`);
21
+
22
+ let self = this;
23
+
24
+ this.form.errorContent = function(payload, output, cb)
25
+ {
26
+ window.utils.waterfallWithHooks({
27
+ data: {
28
+ alert: `An unexpected error occurred. Looks like something went wrong
29
+ while generating your new entity. This might be a bug, or an
30
+ unexpected feature. It could be a temporary issue. When this
31
+ is persistent contact your friendly API Instrumentation
32
+ administrator.`,
33
+ output: output,
34
+ payload: payload,
35
+ pre: '',
36
+ post: ''
37
+ },
38
+ pre: window.hooks.preCreateError,
39
+ post: window.hooks.postCreateError,
40
+ action: (payload, innerCb) => {
41
+ cb(null, `
42
+ ${payload.pre}
43
+ <div class="alert alert-danger" role="alert">${payload.alert}</div>
44
+ <pre id="data">${payload.output}</pre>
45
+ ${window.utils.clipboardButton()}
46
+ ${payload.post}
47
+ `);
48
+ innerCb(null, payload);
49
+ }
50
+ });
51
+ };
52
+
53
+ this.form.resultContent = function(payload, output, cb)
54
+ {
55
+ let card = window.utils.card({
56
+ body: `
57
+ <pre id="data">${output}</pre>
58
+ ${window.utils.clipboardButton()}
59
+ `
60
+ });
61
+
62
+ window.utils.waterfallWithHooks({
63
+ data: {
64
+ alert: `A new ${self.scenario().name.toLowerCase()} was created.`,
65
+ output: output,
66
+ payload: payload,
67
+ cards: [card],
68
+ pre: '',
69
+ post: '',
70
+ openCard: '#details'
71
+ },
72
+ pre: window.hooks.preCreateResult,
73
+ post: window.hooks.postCreateResult,
74
+ action: (payload, innerCb) => {
75
+ cb(null, `
76
+ ${payload.pre}
77
+ <div class="alert alert-success" role="alert">${payload.alert}</div>
78
+ <div class="accordion" id="response">
79
+ ${payload.cards.join(' ')}
80
+ </div>
81
+ ${payload.post}
82
+ `);
83
+ innerCb(null, payload);
84
+ if (payload.openCard) {
85
+ $(
86
+ `.accordion#response button[data-target="${payload.openCard}"]`
87
+ ).click();
88
+ }
89
+ }
90
+ });
91
+ };
92
+ };
93
+
94
+ CreateForm.prototype.updateDesc = function()
95
+ {
96
+ this.desc.html(this.scenario().desc);
97
+ };
98
+
99
+ CreateForm.prototype.activeScenario = function()
100
+ {
101
+ raw = this.select.find(':selected').val().split('/');
102
+ return { group: raw[0], index: raw[1] };
103
+ };
104
+
105
+ CreateForm.prototype.scenario = function()
106
+ {
107
+ scenario = this.activeScenario();
108
+ return this.scenarios[scenario.group][scenario.index];
109
+ };
110
+
111
+ CreateForm.prototype.bind = function()
112
+ {
113
+ this.form.bind((event) => {
114
+ this.submit();
115
+ });
116
+
117
+ this.select.on('change', this.updateDesc.bind(this));
118
+ this.updateDesc();
119
+ };
120
+
121
+ CreateForm.prototype.submit = function()
122
+ {
123
+ let form = this.form;
124
+ let conf = this.scenario();
125
+
126
+ window.utils.waterfallWithHooks({
127
+ data: {
128
+ factory: conf.factory,
129
+ traits: conf.traits,
130
+ overwrite: conf.overwrite
131
+ },
132
+ pre: window.hooks.preCreate,
133
+ post: window.hooks.postCreate,
134
+ action: (payload, cb) => {
135
+ window.utils.request({
136
+ url: window.createUrl,
137
+ data: JSON.stringify(payload)
138
+ }, (err, result) => {
139
+ if (err) { return cb && cb(err); }
140
+ cb(null, { request: payload, response: result });
141
+ });
142
+ }
143
+ }, function(err, result) {
144
+ if (err) { return form.showError(err, err.responseText); }
145
+ form.showResult(result, result.response);
146
+ });
147
+ };
148
+
149
+ // >>> hooks.js
150
+ // You can define some custom hooks to enhance the functionality. With the help
151
+ // of the following hooks you are able to customize the outputs, perform
152
+ // additional HTTP requests or anything you like.
15
153
  //
16
- //= require_tree .
154
+ // All the hooks are designed to passthrough a payload. They receive this
155
+ // payload as the first argument, and a callback function to signal the end of
156
+ // the hook. You MUST pass the payload as second parameter to the callback, or
157
+ // pass an error object as first argument. You can modify the payload as you
158
+ // wish, eg. adding some data from subsequent requests.
159
+ //
160
+ // Example hooks:
161
+ //
162
+ // // Error case
163
+ // window.hooks.postCreate.push((payload, cb) => {
164
+ // cb({ error: true});
165
+ // });
166
+ //
167
+ // // Happy case
168
+ // window.hooks.postCreate.push((payload, cb) => {
169
+ // cb(null, Object.assign(payload, { additional: { data: true } }));
170
+ // });
171
+ //
172
+ // Mind the fact that you can define multiple custom functions per hook type.
173
+ // They are executed after each other in a waterfall like flow. The order of
174
+ // the hooks array is therefore essential.
175
+ window.hooks = {
176
+ // With the help of the +perCreate+ hooks you can manipulate the create
177
+ // request parameters. Think of an additional handling which reads an
178
+ // overwrite form or a kind of trait checkboxes to customize the factory
179
+ // call. The +payload+ looks like this:
180
+ //
181
+ // {
182
+ // factory: 'user',
183
+ // traits: ['confirmed'],
184
+ // overwrite: { password: 'secret' }
185
+ // }
186
+ preCreate: [],
187
+
188
+ // The +postCreate+ hook allows you to perform subsequent requests to fetch
189
+ // additional data. Think of a user instrumentation where you want to request
190
+ // a one time token for this user. This token can be added to the payload and
191
+ // can be shown with the help of the +preCreateResult+ hook. The payload
192
+ // contains the request parameters and the response body from the
193
+ // instrumentation request. Here comes an example +payload+:
194
+ //
195
+ // {
196
+ // request: { factory: 'user', /* [..] */ },
197
+ // response: { /* [..] */ }
198
+ // }
199
+ postCreate: [],
200
+
201
+ // With the help of the +preCreateResult+ hook you can customize the output
202
+ // of the result. You could also perform some subsequent requests or some UI
203
+ // preparations. You can access the output options and the runtime payload
204
+ // with all its data and make modifications to them. This hook is triggered
205
+ // before the result is rendered. A sample payload comes here:
206
+ //
207
+ // {
208
+ // alert: 'Your alert text.',
209
+ // output: 'Formatted response',
210
+ // payload: { request: { /* [..] */ }, response: { /* [..] */ } },
211
+ // cards: [
212
+ // `The details accordion card,
213
+ // you can add more, remove the details card
214
+ // or reorder them`
215
+ // ],
216
+ // openCard: '#details', // Open a custom card, or none
217
+ // pre: 'Additinal HTML content before the alert.',
218
+ // post: 'Additinal HTML content after the formatted response output.'
219
+ // }
220
+ preCreateResult: [],
221
+
222
+ // In case you want to perform some logic after the result is rendered, you
223
+ // can use the +postCreateResult+ hook. You can access the output options and
224
+ // the runtime payload with all its data, but changes to them won't take
225
+ // effect. The +payload+ looks like this:
226
+ //
227
+ // {
228
+ // alert: 'Your alert text.',
229
+ // output: 'Formatted response',
230
+ // payload: { request: { /* [..] */ }, response: { /* [..] */ } },
231
+ // cards: [
232
+ // `The details accordion card,
233
+ // you can add more, remove the details card
234
+ // or reorder them`
235
+ // ],
236
+ // openCard: '#details', // Open a custom card, or none
237
+ // pre: 'Additinal HTML content before the alert.',
238
+ // post: 'Additinal HTML content after the formatted response output.'
239
+ // }
240
+ postCreateResult: [],
241
+
242
+ // With the help of the +preCreateError+ hook you can customize the output of
243
+ // the error. Furthermore you can perform some subsequent requests or
244
+ // whatever comes to your mind. You can access the output options and the
245
+ // runtime payload with all its data and make modifications to them. This
246
+ // hook is triggered before the error is rendered. A sample payload comes
247
+ // here:
248
+ //
249
+ // {
250
+ // alert: 'Your alert text.',
251
+ // output: 'Formatted response',
252
+ // payload: { request: { /* [..] */ }, response: { /* [..] */ } },
253
+ // pre: 'Additinal HTML content before the alert.',
254
+ // post: 'Additinal HTML content after the formatted response output.'
255
+ // }
256
+ preCreateError: [],
257
+
258
+ // In case you want to perform some magic after an error occurred, you can use
259
+ // the +postCreateError+ hook. You can access the output options and the
260
+ // runtime payload with all its data, but changes to them won't take effect
261
+ // because this hook is triggered after the error is rendered. The +payload+
262
+ // looks like this:
263
+ //
264
+ // {
265
+ // alert: 'Your alert text.',
266
+ // output: 'Formatted response',
267
+ // payload: { request: { /* [..] */ }, response: { /* [..] */ } },
268
+ // pre: 'Additinal HTML content before the alert.',
269
+ // post: 'Additinal HTML content after the formatted response output.'
270
+ // }
271
+ postCreateError: []
272
+ };
273
+
274
+ // >>> lib/form.js
275
+ window.Form = Form = function(scope)
276
+ {
277
+ this.button = $(scope).find('button');
278
+ this.result = $('#result');
279
+ this.spinner = $('#spinner');
280
+ };
281
+
282
+ Form.prototype.bind = function(action)
283
+ {
284
+ this.button.on('click', (event) => {
285
+ event.preventDefault();
286
+ this.hideResult();
287
+ action(event);
288
+ return false;
289
+ });
290
+ };
291
+
292
+ Form.prototype.hideResult = function()
293
+ {
294
+ this.result.hide();
295
+ this.button.prop('disabled', true);
296
+ this.spinner.show();
297
+ };
298
+
299
+ Form.prototype.showResultContainer = function(html)
300
+ {
301
+ this.result.html(html);
302
+ $('pre').each((i, block) => hljs.highlightBlock(block));
303
+ new ClipboardJS('.cb-copy');
304
+ this.result.show();
305
+ this.button.prop('disabled', false);
306
+ };
307
+
308
+ Form.prototype.showError = function(payload, output)
309
+ {
310
+ this.spinner.hide();
311
+ output = window.utils.prepareOutput(output);
312
+ this.errorContent(payload, output, (err, html) => {
313
+ this.showResultContainer(html);
314
+ });
315
+ };
316
+
317
+ Form.prototype.errorContent = function(payload, output, cb)
318
+ {
319
+ cb(null, `
320
+ <pre id="data">${output}</pre>
321
+ ${window.utils.clipboardButton()}
322
+ `);
323
+ };
324
+
325
+ Form.prototype.showResult = function(payload, output)
326
+ {
327
+ this.spinner.hide();
328
+ output = window.utils.prepareOutput(output);
329
+ this.resultContent(payload, output, (err, html) => {
330
+ this.showResultContainer(html);
331
+ });
332
+ };
333
+
334
+ Form.prototype.resultContent = function(payload, output, cb)
335
+ {
336
+ cb(null, `
337
+ <pre id="data">${output}</pre>
338
+ ${window.utils.clipboardButton()}
339
+ `);
340
+ };
341
+
342
+ // >>> lib/utils.js
343
+ window.utils = Utils = {};
344
+
345
+ Utils.pushWaterfallPayload = function(data)
346
+ {
347
+ return (cb) => cb(null, data);
348
+ };
349
+
350
+ Utils.waterfallWithHooks = function(opts, cb)
351
+ {
352
+ cb = cb || function(){};
353
+ opts = Object.assign({
354
+ pre: [],
355
+ post: [],
356
+ data: {},
357
+ action: (payload, cb) => cb(null, payload)
358
+ }, opts);
359
+
360
+ async.waterfall([
361
+ // Yield the data to pre hooks
362
+ Utils.pushWaterfallPayload(opts.data),
363
+ // Perform pre hooks
364
+ ...opts.pre,
365
+ // Perform the create request
366
+ opts.action,
367
+ // Perform post hooks
368
+ ...opts.post
369
+ ], cb);
370
+ };
371
+
372
+ Utils.request = function(opts, cb)
373
+ {
374
+ opts = Object.assign({
375
+ url: '/',
376
+ type: 'POST',
377
+ data: '{}',
378
+ dataType: 'json',
379
+ contentType: 'application/json; charset=utf-8',
380
+ ignoreErrors: false
381
+ }, opts || {}, {
382
+ success: (result) => cb(null, result)
383
+ });
384
+
385
+ errCb = (err) => cb(err);
386
+ if (opts.ignoreErrors) {
387
+ errCb = (err) => cb(null, err);
388
+ }
389
+
390
+ $.ajax(opts).fail(errCb);
391
+ };
392
+
393
+ Utils.escape = function(str)
394
+ {
395
+ return str.replace(/&/g, "&amp;")
396
+ .replace(/</g, "&lt;")
397
+ .replace(/>/g, "&gt;");
398
+ };
399
+
400
+ Utils.prepareOutput = function(output)
401
+ {
402
+ try {
403
+ if (typeof output !== 'object') { output = JSON.parse(output); }
404
+ output = JSON.stringify(output, null, 2);
405
+ } catch { }
406
+
407
+ return window.utils.escape(output);
408
+ };
409
+
410
+ Utils.clipboardButton = function(id)
411
+ {
412
+ id = id || 'data';
413
+ return `
414
+ <span class="btn btn-primary cb-copy"
415
+ data-clipboard-target="#${id}">
416
+ <i class="fas fa-paste"></i> Copy result to clipboard
417
+ </span>
418
+ `;
419
+ };
420
+
421
+ Utils.clipboardBadge = function(id)
422
+ {
423
+ id = id || 'data';
424
+ return `
425
+ <span class="badge badge-dark cb-copy" title="Copy result to clipboard"
426
+ data-clipboard-target="#${id}"><i class="fas fa-paste"></i>
427
+ </span>
428
+ `;
429
+ };
430
+
431
+ Utils.card = function(opts)
432
+ {
433
+ opts = Object.assign({
434
+ id: 'details',
435
+ icon: 'fa-asterisk',
436
+ title: 'Details',
437
+ body: ''
438
+ }, opts || {});
439
+
440
+ return `
441
+ <div class="card">
442
+ <div class="card-header">
443
+ <h5 class="mb-0">
444
+ <button class="btn btn-link collapsed" type="button"
445
+ data-toggle="collapse" data-target="#${opts.id}"
446
+ aria-expanded="false">
447
+ <i class="fas ${opts.icon}"></i> ${opts.title}</h5>
448
+ </button>
449
+ </h5>
450
+ </div>
451
+ <div id="${opts.id}" class="collapse" data-parent="#response">
452
+ <div class="card-body">
453
+ ${opts.body}
454
+ </div>
455
+ </div>
456
+ </div>
457
+ `;
458
+ };