mensa 0.9.3 → 0.9.5

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: d4dc7c940f4eff95458542c2ac2d54a71df5735105019fb09d2fc4b70f588124
4
- data.tar.gz: 6f43f89a33e27295b268b6c45c4a018599e313418b1aeb4b3ae758de63180211
3
+ metadata.gz: 486efef62711b15401ec2a9704476bcd3768a7c856c2d21e050b478e23ada619
4
+ data.tar.gz: 710ba8aa3365038772f309bd74fd13c7271c1a67068615c13c33fac6700ae804
5
5
  SHA512:
6
- metadata.gz: 0a038582389be24ec077d6234974d30700bcef87ca6d3524abdecfed97bcb09280fcbbb46095af18fac2339243ebcf3f39e3cc54adad0e96519183e6e5435a67
7
- data.tar.gz: 2803b2f1fa2e42b19ca2ed2aff842e7bdd2bc1315f2fef13b590010105725746f832113fec55861e8ea911eeaf18e2454b9798822d0330ad73068b472f6b7dce
6
+ metadata.gz: cbb1d6a9a8a4bee3f1ef4ea7db1dddf44cee1d00432ab59bbed77be3ba506bcf5c586c3e33d799b54dce3cfc1275968b6f303346b62b5597867b94b888e3d042
7
+ data.tar.gz: 4e6818cc69caed0ffc36163b69aa604d1d593eb144c684c4e71866d73a79879809acb5ff0cd1a627efbcb0c4c8636b2a103f06dd410f2ef0a13460f0597b6465
@@ -24,11 +24,16 @@
24
24
  "source=/run/host-services/ssh-auth.sock,target=/ssh-agent,type=bind,consistency=cached",
25
25
  "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,consistency=cached"
26
26
  ],
27
+ "initializeCommand": ".devcontainer/initialize.sh",
27
28
  "postStartCommand": ".devcontainer/postStart.sh",
28
29
  "containerEnv": {
29
30
  "VISUAL": "vi",
30
31
  "SSH_AUTH_SOCK": "/ssh-agent"
31
32
  },
33
+ // Credentials for the GitHub Packages gem source in the Gemfile
34
+ "remoteEnv": {
35
+ "BUNDLE_RUBYGEMS__PKG__GITHUB__COM": "${localEnv:BUNDLE_RUBYGEMS__PKG__GITHUB__COM}"
36
+ },
32
37
  "customizations": {},
33
- "workspaceFolder": "/workspaces/mensa"
38
+ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
34
39
  }
@@ -1,11 +1,17 @@
1
- name: mensa
1
+ name: ${COMPOSE_PROJECT_NAME:-mensa}
2
2
 
3
3
  services:
4
4
  app:
5
5
  image: ghcr.io/rails/devcontainer/images/ruby:4.0.5
6
6
 
7
7
  volumes:
8
- - ../..:/workspaces
8
+ # .env is written by initialize.sh, the defaults are for the main checkout
9
+ - ${WORKSPACES_ROOT:-../..}:/workspaces
10
+ # A worktree's .git file points here by absolute path
11
+ - ${GIT_COMMON_DIR:-../.git}:${GIT_COMMON_DIR:-/workspaces/mensa/.git}
12
+
13
+ environment:
14
+ CAPYBARA_APP_HOST: http://app.${COMPOSE_PROJECT_NAME:-mensa}.orb.local
9
15
 
10
16
  # Overrides default command so things don't shut down after the process ends.
11
17
  command: sleep infinity
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # Runs on the host before the containers start, see initializeCommand.
3
+ # Writes the .env docker compose reads, so a git worktree gets its own
4
+ # containers and can use git inside them.
5
+ set -e
6
+
7
+ cd "$(dirname "$0")/.."
8
+ checkout="$(pwd -P)"
9
+ name="$(basename "$checkout")"
10
+
11
+ if [ -f .git ]; then
12
+ # A worktree: its .git file points into the main checkout's .git directory
13
+ project="mensa-$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9_-\n' '-')"
14
+ else
15
+ project="mensa"
16
+ fi
17
+
18
+ cat > .devcontainer/.env <<EOF
19
+ COMPOSE_PROJECT_NAME=$project
20
+ WORKSPACES_ROOT=$(dirname "$checkout")
21
+ GIT_COMMON_DIR=$(cd "$(git rev-parse --git-common-dir)" && pwd -P)
22
+ EOF
@@ -11,3 +11,5 @@ sudo chmod 666 /ssh-agent
11
11
  "$MISE" exec -- bundle install
12
12
  "$MISE" exec --cd test/dummy -- rake db:prepare
13
13
  "$MISE" exec --cd test/dummy -- rake tailwindcss:config
14
+ # Propshaft only serves app/assets/builds when it exists at boot
15
+ "$MISE" exec --cd test/dummy -- rake tailwindcss:build
data/.gitignore CHANGED
@@ -22,3 +22,6 @@ test/dummy/config/tailwind.config.js
22
22
  **/**/.DS_Store
23
23
  mise.toml
24
24
  todo.md
25
+
26
+ # Written by .devcontainer/initialize.sh
27
+ .devcontainer/.env
data/.shipkit.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  ---
2
- previous_version: 0.9.2
2
+ previous_version: 0.9.4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mensa (0.9.2)
4
+ mensa (0.9.3)
5
5
  csv
6
6
  importmap-rails
7
7
  pagy (>= 43)
@@ -7,6 +7,7 @@
7
7
  data-mensa-table-target="filterList"
8
8
  data-mensa-filter-pill-list-table-name-value="<%= table.name %>"
9
9
  data-mensa-filter-pill-list-storage-key-value="<%= table.storage_key %>"
10
+ data-mensa-filter-pill-list-filters-customized-value="<%= table.filters_customized? %>"
10
11
  data-mensa-filter-pill-list-mensa-table-outlet=".mensa-table#table-<%= table.table_id %>"
11
12
  data-mensa-filter-pill-list-mensa-filter-pill-outlet=".mensa-filter-pill"
12
13
  data-mensa-filter-pill-list-mensa-add-filter-outlet="[data-controller=mensa-add-filter]">
@@ -9,6 +9,7 @@ export default class FilterPillListComponentController extends ApplicationContro
9
9
  supportsViews: Boolean,
10
10
  tableName: String,
11
11
  storageKey: String,
12
+ filtersCustomized: Boolean,
12
13
  };
13
14
 
14
15
  connect() {
@@ -180,7 +181,10 @@ export default class FilterPillListComponentController extends ApplicationContro
180
181
  page: "",
181
182
  };
182
183
  this.persistState(state);
183
- // The selected view brings its own grouping
184
+ // The selected view brings its own filters and grouping. Stored filters
185
+ // hold the previous view's pills (its defaults included), which would
186
+ // otherwise be replayed onto this view when the page is visited again.
187
+ this.writeStorage(this.filtersStorageKey, null);
184
188
  this.clearPersistedGrouping();
185
189
  this.setSearchField("");
186
190
  this.updateSearchPlaceholder();
@@ -210,9 +214,7 @@ export default class FilterPillListComponentController extends ApplicationContro
210
214
  const order = this.parseOrderParams(url.searchParams);
211
215
  this.persistOrder(order);
212
216
 
213
- if (Object.keys(order).length > 0) {
214
- this._notifyUnsavedState();
215
- }
217
+ this._updateUnsavedState();
216
218
  }
217
219
 
218
220
  currentRequestState() {
@@ -286,21 +288,8 @@ export default class FilterPillListComponentController extends ApplicationContro
286
288
  const page = this.loadPage();
287
289
  const order = this.loadOrder();
288
290
  const columnOrder = this.loadColumnOrder();
289
- const hiddenColumns = this.loadHiddenColumns();
290
291
 
291
- // Show save/reset if any persistent state exists (order, column layout, filters, search)
292
- if (
293
- Object.keys(filters).length > 0 ||
294
- filtersProvided ||
295
- query.length > 0 ||
296
- Object.keys(order).length > 0 ||
297
- columnOrder.length > 0 ||
298
- hiddenColumns.length > 0 ||
299
- this.loadGroupBy() !== null ||
300
- this.loadAggregates() !== null
301
- ) {
302
- this._notifyUnsavedState();
303
- }
292
+ this._updateUnsavedState();
304
293
 
305
294
  const hasFilterOrSearch =
306
295
  Object.keys(filters).length > 0 ||
@@ -592,9 +581,7 @@ export default class FilterPillListComponentController extends ApplicationContro
592
581
  `[data-mensa-views-target="view"][data-view-id="${view}"]`,
593
582
  );
594
583
  if (viewEl) {
595
- const name =
596
- viewEl.dataset.viewName ||
597
- viewEl.querySelector("span")?.textContent?.trim();
584
+ const name = viewEl.dataset.viewName;
598
585
  if (name) triggerLabel.textContent = name;
599
586
  }
600
587
  }
@@ -611,6 +598,21 @@ export default class FilterPillListComponentController extends ApplicationContro
611
598
  );
612
599
  }
613
600
 
601
+ // Whether the table differs from the selected view, so there's something
602
+ // to save or reset. The server tells whether the rendered filters differ,
603
+ // as stored filters can equal the view's own.
604
+ hasUnsavedState() {
605
+ return (
606
+ this.filtersCustomizedValue ||
607
+ this.loadQuery().length > 0 ||
608
+ Object.keys(this.loadOrder()).length > 0 ||
609
+ this.loadColumnOrder().length > 0 ||
610
+ this.loadHiddenColumns().length > 0 ||
611
+ this.loadGroupBy() !== null ||
612
+ this.loadAggregates() !== null
613
+ );
614
+ }
615
+
614
616
  // Clears all dirty-state localStorage keys so the view is clean after save.
615
617
  clearPersistedDirtyState() {
616
618
  this.writeStorage(this.filtersStorageKey, null);
@@ -833,6 +835,15 @@ export default class FilterPillListComponentController extends ApplicationContro
833
835
  }
834
836
  }
835
837
 
838
+ _updateUnsavedState() {
839
+ if (
840
+ this.hasMensaTableOutlet &&
841
+ typeof this.mensaTableOutlet.updateUnsavedState === "function"
842
+ ) {
843
+ this.mensaTableOutlet.updateUnsavedState();
844
+ }
845
+ }
846
+
836
847
  _notifyUnsavedState() {
837
848
  if (
838
849
  this.hasMensaTableOutlet &&
@@ -185,6 +185,17 @@ export default class TableComponentController extends ApplicationController {
185
185
  this.showSaveReset();
186
186
  }
187
187
 
188
+ // Shows the save/reset buttons only while something differs from the view,
189
+ // so they disappear again once a change is undone.
190
+ updateUnsavedState() {
191
+ if (!this.hasMensaFilterPillListOutlet) return;
192
+ if (this.mensaFilterPillListOutlet.hasUnsavedState()) {
193
+ this.notifyUnsavedState();
194
+ } else {
195
+ this.hideSaveReset();
196
+ }
197
+ }
198
+
188
199
  // --- Cancel / Reset ---
189
200
 
190
201
  // Resets all filters, search, and column customisation to the active view's clean state.
@@ -400,6 +411,8 @@ export default class TableComponentController extends ApplicationController {
400
411
  // Called when the filter-pill-list outlet connects (including after turbo-stream re-renders).
401
412
  // Re-applied on every connect so late-arriving pills (from restoreState) get the right state.
402
413
  mensaFilterPillListOutletConnected() {
414
+ // The pill list is rendered again after every change
415
+ this.updateUnsavedState();
403
416
  const hasViewFilters = this._hasViewFilterPills();
404
417
  if (hasViewFilters) {
405
418
  const visible = this._loadViewFiltersVisible();
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  .mensa-table__views__option-check {
26
- @apply text-primary-600 text-xs w-4;
26
+ @apply inline-flex justify-center shrink-0 text-primary-600 text-xs w-4;
27
27
  }
28
28
 
29
29
  .mensa-table__views__option-menu {
@@ -22,7 +22,8 @@
22
22
  <% is_user_view = view.is_a?(Mensa::TableView) %>
23
23
  <li class="mensa-table__views__option" role="option" data-view-id="<%= view.id %>" data-view-name="<%= view.name %>" data-mensa-views-target="view">
24
24
  <button class="mensa-table__views__option-btn" type="button" data-action="mensa-views#select" data-view-id="<%= view.id %>">
25
- <i class="mensa-table__views__option-check <%= Mensa.config.icons[:views_option_selected] %><%= " invisible" unless is_selected %>"></i>
25
+ <%# The state is on the span: FontAwesome swaps the <i> for an <svg> asynchronously, copying classes that may be stale by then %>
26
+ <span class="mensa-table__views__option-check<%= " invisible" unless is_selected %>"><i class="<%= Mensa.config.icons[:views_option_selected] %>"></i></span>
26
27
  <span><%= view.name %></span>
27
28
  </button>
28
29
  <% if is_user_view %>
@@ -42,7 +42,7 @@ export default class ViewsComponentController extends ApplicationController {
42
42
  check?.classList.toggle("invisible", !isThis);
43
43
  });
44
44
 
45
- const viewName = event.currentTarget.querySelector("span")?.textContent?.trim() || "";
45
+ const viewName = selected.closest("[data-mensa-views-target='view']")?.dataset.viewName || "";
46
46
  if (this.hasTriggerLabelTarget) {
47
47
  this.triggerLabelTarget.textContent = viewName;
48
48
  }
@@ -106,17 +106,14 @@ module Mensa
106
106
  end
107
107
 
108
108
  def active_filters
109
- (config[:filters] || {}).filter_map do |column_name, filter_config|
110
- col = column(column_name)
111
- # Columns without SQL can only be filtered with a custom scope, skip
112
- # filters on them, e.g. from browser state saved earlier
113
- next if col.nil? || !(col.sql? || col.filter?)
109
+ filters_for(config[:filters])
110
+ end
114
111
 
115
- definition = config.dig(:columns, col.name, :filter)
116
- definition = {} unless definition.is_a?(Hash)
117
- requested = (filter_config || {}).to_h.symbolize_keys.slice(:value, :operator)
118
- Mensa::Filter.new(column: col, config: definition.merge(requested), table: self)
119
- end
112
+ # True when the filters differ from the selected view's, so there is
113
+ # something to save or reset
114
+ def filters_customized?
115
+ view_filters = (table_view&.config || {}).deep_symbolize_keys[:filters]
116
+ comparable_filters(active_filters) != comparable_filters(filters_for(view_filters))
120
117
  end
121
118
 
122
119
  def actions?
@@ -273,6 +270,28 @@ module Mensa
273
270
 
274
271
  private
275
272
 
273
+ def filters_for(filters)
274
+ (filters || {}).filter_map do |column_name, filter_config|
275
+ col = column(column_name)
276
+ # Columns without SQL can only be filtered with a custom scope, skip
277
+ # filters on them, e.g. from browser state saved earlier
278
+ next if col.nil? || !(col.sql? || col.filter?)
279
+
280
+ definition = config.dig(:columns, col.name, :filter)
281
+ definition = {} unless definition.is_a?(Hash)
282
+ requested = (filter_config || {}).to_h.symbolize_keys.slice(:value, :operator)
283
+ Mensa::Filter.new(column: col, config: definition.merge(requested), table: self)
284
+ end
285
+ end
286
+
287
+ # Operator and value per column, the value only when the operator takes one
288
+ def comparable_filters(filters)
289
+ filters.to_h do |filter|
290
+ value = Array(filter.value).map(&:to_s).reject(&:blank?).sort if filter.operator_with_value?
291
+ [filter.column.name, [filter.operator, value]]
292
+ end
293
+ end
294
+
276
295
  def ensure_internal_columns_for_joined_associations
277
296
  return if @internal_columns_ensured
278
297
 
data/lib/mensa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mensa
2
- VERSION = "0.9.3"
2
+ VERSION = "0.9.5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mensa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
@@ -167,6 +167,7 @@ files:
167
167
  - ".devcontainer/devcontainer-lock.json"
168
168
  - ".devcontainer/devcontainer.json"
169
169
  - ".devcontainer/docker-compose.yml"
170
+ - ".devcontainer/initialize.sh"
170
171
  - ".devcontainer/postStart.sh"
171
172
  - ".gem_release.yml"
172
173
  - ".github/workflows/gem-push.yml"