mensa 0.9.5 → 0.9.6

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: 486efef62711b15401ec2a9704476bcd3768a7c856c2d21e050b478e23ada619
4
- data.tar.gz: 710ba8aa3365038772f309bd74fd13c7271c1a67068615c13c33fac6700ae804
3
+ metadata.gz: cd784c73c622f5a54cc41cd377bdd18021f82f0249be3e55b723f5aaf539e4a2
4
+ data.tar.gz: e202a9ee523e3c43a3469b88d9fc8947ec20c2bdff2a80348e5481486e8908a7
5
5
  SHA512:
6
- metadata.gz: cbb1d6a9a8a4bee3f1ef4ea7db1dddf44cee1d00432ab59bbed77be3ba506bcf5c586c3e33d799b54dce3cfc1275968b6f303346b62b5597867b94b888e3d042
7
- data.tar.gz: 4e6818cc69caed0ffc36163b69aa604d1d593eb144c684c4e71866d73a79879809acb5ff0cd1a627efbcb0c4c8636b2a103f06dd410f2ef0a13460f0597b6465
6
+ metadata.gz: 78db0d1687bb04a577831a70a98b870f57daca88f8d32ac12cc0ccaf05d5fae38d4d6a364a13326672b3c8d8fd30d230bc474c17368e763276d6b32e686b622e
7
+ data.tar.gz: d0a027ecf6d5cd5e4db5ad7bd8c8c4e9394a98244247fb492b9689ab861540832f08f1dd61e114bf930617c95229a153a13f45cda8a2c3de59456ff59d0ea16a
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "features": {
3
- "ghcr.io/devcontainers/features/common-utils:2": {
4
- "version": "2.5.9",
5
- "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
6
- "integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a"
3
+ "ghcr.io/devcontainers/features/common-utils:2.7.1": {
4
+ "version": "2.7.1",
5
+ "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08",
6
+ "integrity": "sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08"
7
7
  },
8
8
  "ghcr.io/devcontainers/features/docker-in-docker:2": {
9
9
  "version": "2.17.0",
@@ -6,7 +6,7 @@
6
6
  "service": "app",
7
7
  "features": {
8
8
  "ghcr.io/devcontainers/features/node:2": {},
9
- "ghcr.io/devcontainers/features/common-utils:2": {
9
+ "ghcr.io/devcontainers/features/common-utils:2.7.1": {
10
10
  "configureZshAsDefaultShell": true,
11
11
  "installOhMyZsh": false,
12
12
  "installOhMyZshConfig": false
@@ -18,22 +18,31 @@
18
18
  "ghcr.io/devcontainers/features/github-cli:1": {},
19
19
  "ghcr.io/devcontainers/features/docker-in-docker:2": {
20
20
  "moby": false
21
- }
21
+ },
22
+ // Your own tools from ~/.config/de/config.json, written by bin/de
23
+ // initialize, see https://github.com/tdegrunt/de#your-own-tools
24
+ "./de-host": {}
22
25
  },
23
26
  "mounts": [
24
27
  "source=/run/host-services/ssh-auth.sock,target=/ssh-agent,type=bind,consistency=cached",
25
28
  "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,consistency=cached"
26
29
  ],
27
- "initializeCommand": ".devcontainer/initialize.sh",
30
+ "initializeCommand": "bin/de initialize",
31
+ // Closing the editor leaves the environment running, bin/de down stops it
32
+ "shutdownAction": "none",
28
33
  "postStartCommand": ".devcontainer/postStart.sh",
29
34
  "containerEnv": {
30
35
  "VISUAL": "vi",
31
36
  "SSH_AUTH_SOCK": "/ssh-agent"
32
37
  },
33
- // Credentials for the GitHub Packages gem source in the Gemfile
34
38
  "remoteEnv": {
35
- "BUNDLE_RUBYGEMS__PKG__GITHUB__COM": "${localEnv:BUNDLE_RUBYGEMS__PKG__GITHUB__COM}"
39
+ // Credentials for the GitHub Packages gem source in the Gemfile
40
+ "BUNDLE_RUBYGEMS__PKG__GITHUB__COM": "${localEnv:BUNDLE_RUBYGEMS__PKG__GITHUB__COM}",
41
+ // The repository's bin/, so `rails test` works in terminals
42
+ "PATH": "${containerWorkspaceFolder}/bin:${containerEnv:PATH}"
36
43
  },
37
44
  "customizations": {},
38
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
39
- }
45
+ // Where docker-compose.yml mounts this checkout, see env_init in bin/de.
46
+ // /workspaces/mensa links here too, but only once the container runs.
47
+ "workspaceFolder": "/workspaces${localWorkspaceFolder}"
48
+ }
@@ -1,41 +1,102 @@
1
- name: ${COMPOSE_PROJECT_NAME:-mensa}
1
+ # One compose project per checkout. The variables come from .devcontainer/.env,
2
+ # written by bin/de up (or bin/de initialize when opened from an editor).
3
+ name: ${COMPOSE_PROJECT_NAME:?run bin/de up to write .devcontainer/.env}
4
+
5
+ x-env-labels: &env-labels
6
+ dev.env.name: ${ENV_NAME}
7
+ dev.env.kind: ${ENV_KIND}
8
+ dev.env.branch: ${ENV_BRANCH}
9
+ dev.env.path: ${ENV_PATH}
10
+ dev.env.gitdir: ${ENV_GIT_COMMON_DIR}
2
11
 
3
12
  services:
4
13
  app:
5
14
  image: ghcr.io/rails/devcontainer/images/ruby:4.0.5
15
+ labels: *env-labels
6
16
 
7
17
  volumes:
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}
18
+ # The checkout and the common .git directory sit at the same place
19
+ # relative to each other as on the host (under /workspaces), so a
20
+ # worktree's relative .git link resolves. /workspaces/<repo> links to
21
+ # the checkout.
22
+ - ${ENV_PATH}:${ENV_PATH_IN_CONTAINER}
23
+ - ${ENV_GIT_COMMON_DIR}:${ENV_GIT_COMMON_DIR_IN_CONTAINER}
24
+ # The global gitignore. Here, not in devcontainer.json, which drops
25
+ # readonly from its mounts.
26
+ - ${ENV_GIT_CONFIG_DIR}:/home/vscode/.config/git:ro
12
27
 
13
28
  environment:
14
- CAPYBARA_APP_HOST: http://app.${COMPOSE_PROJECT_NAME:-mensa}.orb.local
29
+ DEV_ENV_NAME: ${ENV_NAME}
30
+ DEV_ENV_KIND: ${ENV_KIND}
31
+ DB_HOST: postgres
32
+ REDIS_URL: redis://redis:6379/1
33
+ # See SELENIUM_URL in env.conf
34
+ SELENIUM_URL: ${SELENIUM_URL}
35
+ CAPYBARA_APP_HOST: ${CAPYBARA_APP_HOST}
36
+ # Other worktrees aren't mounted here, keep git gc from pruning them
37
+ GIT_CONFIG_COUNT: 1
38
+ GIT_CONFIG_KEY_0: gc.worktreePruneExpire
39
+ GIT_CONFIG_VALUE_0: never
40
+
41
+ expose:
42
+ - "3000"
15
43
 
16
- # Overrides default command so things don't shut down after the process ends.
17
- command: sleep infinity
44
+ # Keeps the container running after the process ends
45
+ command: ["sh", "-c", "sudo ln -sfn \"$$0\" \"$$1\" && exec sleep infinity", "${ENV_PATH_IN_CONTAINER}", "${ENV_WORKSPACE_LINK}"]
46
+ restart: ${ENV_RESTART:-no}
18
47
 
19
48
  depends_on:
20
- - postgres
21
- - redis
49
+ postgres:
50
+ condition: service_healthy
51
+ redis:
52
+ condition: service_healthy
53
+ selenium:
54
+ condition: service_healthy
22
55
 
23
56
  postgres:
24
57
  image: postgres:18
25
- restart: unless-stopped
58
+ labels: *env-labels
59
+ restart: ${ENV_RESTART:-no}
26
60
  volumes:
27
61
  - postgres-data:/var/lib/postgresql/18/docker
28
62
  environment:
29
63
  POSTGRES_USER: postgres
30
64
  POSTGRES_PASSWORD: postgres
65
+ healthcheck:
66
+ test: ["CMD", "pg_isready", "-U", "postgres"]
67
+ interval: 2s
68
+ timeout: 5s
69
+ retries: 30
31
70
 
32
71
  redis:
33
72
  image: valkey/valkey:8
34
- restart: unless-stopped
73
+ labels: *env-labels
74
+ restart: ${ENV_RESTART:-no}
75
+ healthcheck:
76
+ test: ["CMD", "valkey-cli", "ping"]
77
+ interval: 2s
78
+ timeout: 5s
79
+ retries: 30
35
80
 
36
81
  selenium:
37
82
  image: selenium/standalone-firefox
38
- restart: unless-stopped
83
+ labels: *env-labels
84
+ restart: ${ENV_RESTART:-no}
85
+ shm_size: 2gb
86
+ environment:
87
+ # The dark mode system test opens a second browser next to the default one
88
+ SE_NODE_MAX_SESSIONS: 2
89
+ SE_NODE_OVERRIDE_MAX_SESSIONS: "true"
90
+ healthcheck:
91
+ test: ["CMD", "/opt/bin/check-grid.sh", "--host", "localhost", "--port", "4444"]
92
+ interval: 2s
93
+ timeout: 5s
94
+ retries: 60
39
95
 
40
96
  volumes:
41
97
  postgres-data:
98
+ labels: *env-labels
99
+
100
+ networks:
101
+ default:
102
+ labels: *env-labels
@@ -0,0 +1,29 @@
1
+ # Per-repository settings for bin/de, which comes from the de repository and
2
+ # is the same everywhere.
3
+
4
+ # Prefix of every environment name, and the folder the checkout appears at
5
+ # inside the container (/workspaces/<ENV_REPO>, see devcontainer.json).
6
+ ENV_REPO=mensa
7
+
8
+ # Branches whose environments are persistent: they keep their volumes and are
9
+ # never removed by bin/de prune. Every other branch gets an ephemeral one.
10
+ PERSISTENT_BRANCHES=(staging develop)
11
+
12
+ # Branches whose environment is named just ENV_REPO, e.g. reachable as
13
+ # app.mensa.orb.local. Always persistent.
14
+ PRIMARY_BRANCHES=(main master)
15
+
16
+ # Selenium the system tests drive: the environment's own container. To watch
17
+ # a test in a browser on the Mac instead, override it for one command:
18
+ #
19
+ # SELENIUM_URL=http://host.docker.internal:4444/ HEADLESS=0 bin/de exec bin/rails test:system
20
+ SELENIUM_URL="http://selenium:4444/"
21
+
22
+ # Run in the container at the end of bin/de up, from the checkout root, e.g.
23
+ # to prepare the test database (postStart.sh prepares the development one).
24
+ # From the engine root, so the engine's own migrations are included.
25
+ UP_COMMAND=(env RAILS_ENV=test bin/rails app:db:prepare)
26
+
27
+ # Version of @devcontainers/cli to run through npx when `devcontainer` isn't
28
+ # installed.
29
+ DEVCONTAINER_CLI_VERSION=0.90.0
@@ -2,14 +2,19 @@
2
2
  set -e
3
3
 
4
4
  MISE="$HOME/.local/bin/mise"
5
- eval "$("$MISE" activate bash)"
5
+ # Trust first, activating reads mise.local.toml too
6
6
  "$MISE" trust
7
+ eval "$("$MISE" activate bash)"
7
8
  "$MISE" install
8
9
  sudo chmod 666 /ssh-agent
10
+ # So git over SSH, e.g. a git source in the Gemfile, doesn't stop to ask
11
+ mkdir -p ~/.ssh
12
+ ssh-keygen -F github.com >/dev/null 2>&1 || ssh-keyscan -t ed25519 github.com 2>/dev/null >> ~/.ssh/known_hosts
9
13
 
10
14
  "$MISE" exec -- npm install
11
15
  "$MISE" exec -- bundle install
12
- "$MISE" exec --cd test/dummy -- rake db:prepare
16
+ # From the engine root, so the engine's own migrations are included
17
+ "$MISE" exec -- bin/rails app:db:prepare
13
18
  "$MISE" exec --cd test/dummy -- rake tailwindcss:config
14
19
  # Propshaft only serves app/assets/builds when it exists at boot
15
20
  "$MISE" exec --cd test/dummy -- rake tailwindcss:build
data/.gitignore CHANGED
@@ -21,7 +21,8 @@ test/dummy/config/tailwind.config.js
21
21
 
22
22
  **/**/.DS_Store
23
23
  mise.toml
24
+ mise.local.toml
24
25
  todo.md
25
26
 
26
- # Written by .devcontainer/initialize.sh
27
27
  .devcontainer/.env
28
+ .devcontainer/de-host/
data/.shipkit.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  ---
2
- previous_version: 0.9.4
2
+ previous_version: 0.9.5
data/.worktreeinclude ADDED
@@ -0,0 +1,2 @@
1
+ .envrc
2
+ mise.local.toml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mensa (0.9.3)
4
+ mensa (0.9.5)
5
5
  csv
6
6
  importmap-rails
7
7
  pagy (>= 43)
data/README.md CHANGED
@@ -114,6 +114,49 @@ You can show your tables on the page using the following:
114
114
  <%= table :users %>
115
115
  ```
116
116
 
117
+ #### Composing views
118
+
119
+ Views often share filters. Define the shared part once with `partial` and `include` it in the views that need it.
120
+ An include acts as if the partial's contents were written there, so what follows it overrides it, e.g. a filter on the same column.
121
+ A partial can contain anything a view can (filters, `hidden_columns`, `column_order`, `group_by`, ...) and must be defined before the views that include it.
122
+
123
+ ```ruby
124
+ partial :open do
125
+ filter :state do
126
+ operator :isnt
127
+ value ["completed", "cancelled"]
128
+ end
129
+ end
130
+
131
+ view :default do
132
+ include :open
133
+ end
134
+
135
+ view :sales_orders do
136
+ name -> { Orders::SalesOrder.model_name.human(count: 2) }
137
+ include :open
138
+ filter :type do
139
+ operator :is
140
+ value "Orders::SalesOrder"
141
+ end
142
+ end
143
+ ```
144
+
145
+ `include` also takes a block, so a plain Ruby constant works too:
146
+
147
+ ```ruby
148
+ OPEN = proc do
149
+ filter :state do
150
+ operator :isnt
151
+ value ["completed", "cancelled"]
152
+ end
153
+ end
154
+
155
+ view :default do
156
+ include OPEN
157
+ end
158
+ ```
159
+
117
160
  #### Custom views
118
161
 
119
162
  Custom views are views not defined by the developer (SystemViews) but by the end-user by adding/removing filters.
@@ -198,14 +241,14 @@ end
198
241
  ```
199
242
 
200
243
  The operators are `is`, `isnt`, `matches`, `does_not_match`, `gt`, `gteq`, `lt`,
201
- `lteq`, `is_duplicate`, `is_current`, `is_empty` and `isnt_empty`. Without an
244
+ `lteq`, `between`, `is_duplicate`, `is_current`, `is_empty` and `isnt_empty`. Without an
202
245
  `operators` list, a column gets the ones that work for its type in SQL:
203
246
 
204
247
  | Column type | Operators besides `is_duplicate`, `is_empty` and `isnt_empty` |
205
248
  |---|---|
206
249
  | string, text, citext, or an `attribute` without `type` | `is`, `isnt`, `matches`, `does_not_match` |
207
- | integer, float, decimal, time | `is`, `isnt`, `gt`, `gteq`, `lt`, `lteq` |
208
- | date, datetime | `gt`, `gteq`, `lt`, `lteq` |
250
+ | integer, float, decimal, time | `is`, `isnt`, `gt`, `gteq`, `lt`, `lteq`, `between` |
251
+ | date, datetime | `gt`, `gteq`, `lt`, `lteq`, `between` |
209
252
  | boolean, uuid, enums and other types | `is`, `isnt` |
210
253
  | jsonb, json | none (json also has no `is_duplicate`) |
211
254
 
@@ -214,7 +257,12 @@ The operators are `is`, `isnt`, `matches`, `does_not_match`, `gt`, `gteq`, `lt`,
214
257
  column is empty (NULL). The value is taken literally: `%`, `_` and `\` are matched as
215
258
  characters, not as wildcards. A blank value matches only empty strings.
216
259
 
217
- `matches` and `does_not_match` are left out when the filter has a `collection`, and
260
+ `between` takes the range as `[from, to]` and includes both ends. A blank end leaves
261
+ that side open, so `["2026-01-01", ""]` finds everything from 1 January 2026 on. In
262
+ the filter popover it shows a second input for the end of the range, and the pill reads
263
+ e.g. "Created at between 2026-01-01 and 2026-03-31".
264
+
265
+ `matches`, `does_not_match` and `between` are left out when the filter has a `collection`, and
218
266
  `is_current` is only offered when `Current` has a method named after the column.
219
267
 
220
268
  Mensa reads the type from the database column. Give an `attribute` that isn't text a
@@ -318,6 +366,10 @@ end
318
366
  - Run `direnv allow`
319
367
  - Run `overmind s`
320
368
 
369
+ ### Development environments
370
+
371
+ See [de](https://github.com/tdegrunt/de#readme).
372
+
321
373
  ### Docs
322
374
 
323
375
  Using the following in your view will render Mensa::Table::Component
@@ -36,6 +36,15 @@ input.mensa-table__add_filter__popover_container__input {
36
36
  @apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 focus:outline-none;
37
37
  }
38
38
 
39
+ /* The second input of a range (between) shows only while that operator is selected */
40
+ .mensa-table__add_filter__popover_container__range {
41
+ @apply flex flex-col gap-2;
42
+ }
43
+
44
+ .mensa-table__add_filter__popover_container__range:not([data-range="true"]) > input:nth-of-type(2) {
45
+ @apply hidden;
46
+ }
47
+
39
48
  .mensa-table__add_filter__popover_container__separator {
40
49
  @apply border-t border-gray-200 dark:border-gray-600 my-3 -mx-4;
41
50
  }
@@ -13,6 +13,7 @@ export default class AddFilterComponentController extends ApplicationController
13
13
  "description", // contains the filter description in the "tab"
14
14
  "valuePopover", // contains the filter-value
15
15
  "value",
16
+ "valueTo", // the end of a range, for the between operator
16
17
  "valueOption", // individual value list items in the popover
17
18
  "operatorOption", // individual operator list items
18
19
  ];
@@ -245,7 +246,7 @@ export default class AddFilterComponentController extends ApplicationController
245
246
  );
246
247
  }
247
248
  if (!item) {
248
- if (this.hasValueTarget && this.valueTarget.value) {
249
+ if (this._hasManualValue) {
249
250
  this._applyManualValue();
250
251
  }
251
252
  return;
@@ -310,6 +311,22 @@ export default class AddFilterComponentController extends ApplicationController
310
311
  return this.hasValueTarget ? this.valueTarget.value : "";
311
312
  }
312
313
 
314
+ // The value to filter on: the selected values, or what was typed, as
315
+ // [from, to] for a range
316
+ get filterValue() {
317
+ if (this.isRange)
318
+ return [this.valueTarget.value, this.valueToTarget.value];
319
+ return this.selectedValues;
320
+ }
321
+
322
+ get isRange() {
323
+ return (
324
+ this.operator === "between" &&
325
+ this.hasValueTarget &&
326
+ this.hasValueToTarget
327
+ );
328
+ }
329
+
313
330
  get hasHighlightedValue() {
314
331
  return this._valuePopoverItems.some((i) =>
315
332
  i.classList.contains("highlighted"),
@@ -425,6 +442,8 @@ export default class AddFilterComponentController extends ApplicationController
425
442
  }
426
443
  });
427
444
 
445
+ this._updateRangeInputs();
446
+
428
447
  const requiresValue = this.operatorRequiresValue(
429
448
  selected.dataset.operator,
430
449
  );
@@ -445,8 +464,14 @@ export default class AddFilterComponentController extends ApplicationController
445
464
  this._updateDescription();
446
465
 
447
466
  // Apply to the table immediately when a value is already selected.
467
+ // A range waits for both ends, Enter applies it
448
468
  if (!requiresValue) {
449
469
  this.mensaFilterPillListOutlet.refreshFilters();
470
+ } else if (this.isRange) {
471
+ const input = [this.valueTarget, this.valueToTarget].find(
472
+ (i) => !i.value,
473
+ );
474
+ input?.focus({ preventScroll: true });
450
475
  } else if (this.isMultipleMode) {
451
476
  if (this.selectedValues.length > 0) {
452
477
  _pendingMultiReopen = {
@@ -517,6 +542,10 @@ export default class AddFilterComponentController extends ApplicationController
517
542
  .map((opt) => opt.dataset.label || opt.dataset.value)
518
543
  : [];
519
544
  valueLabel = labels.length > 0 ? labels.join(", ") : null;
545
+ } else if (this.isRange) {
546
+ const [from, to] = this.filterValue;
547
+ if (from && to) valueLabel = `${from} – ${to}`;
548
+ else valueLabel = from || to || null;
520
549
  } else {
521
550
  const value = this.hasValueTarget ? this.valueTarget.value : "";
522
551
  if (value) {
@@ -582,7 +611,7 @@ export default class AddFilterComponentController extends ApplicationController
582
611
  }
583
612
 
584
613
  _applyManualValue() {
585
- if (!this.hasValueTarget || !this.valueTarget.value) return;
614
+ if (!this._hasManualValue) return;
586
615
  this._updateDescription();
587
616
  this._removePendingPill();
588
617
  this.mensaFilterPillListOutlet.refreshFilters();
@@ -669,6 +698,30 @@ export default class AddFilterComponentController extends ApplicationController
669
698
  .replace(/"/g, "&quot;");
670
699
  }
671
700
 
701
+ get _hasManualValue() {
702
+ if (this.isRange)
703
+ return this.filterValue.some((value) => value);
704
+ return this.hasValueTarget && !!this.valueTarget.value;
705
+ }
706
+
707
+ // Shows the end of the range only for between, the placeholders tell
708
+ // the two inputs apart
709
+ _updateRangeInputs() {
710
+ if (!this.hasValueToTarget) return;
711
+
712
+ const range = this.valueTarget.closest(
713
+ ".mensa-table__add_filter__popover_container__range",
714
+ );
715
+ if (this.isRange) {
716
+ range.dataset.range = "true";
717
+ this.valueTarget.placeholder =
718
+ this.valueTarget.dataset.rangePlaceholder;
719
+ } else {
720
+ delete range.dataset.range;
721
+ this.valueTarget.placeholder = "";
722
+ }
723
+ }
724
+
672
725
  get _manualValueInput() {
673
726
  if (!this.hasValueTarget) return null;
674
727
  const input = this.valueTarget;
@@ -25,13 +25,7 @@ module Mensa
25
25
  view_filter = view_filter.to_h.with_indifferent_access
26
26
  return false if view_filter[:operator].present? && view_filter[:operator].to_sym != filter.operator
27
27
 
28
- comparable(view_filter[:value]) == comparable(filter.value)
29
- end
30
-
31
- private
32
-
33
- def comparable(value)
34
- Array(value).map(&:to_s).reject(&:blank?).sort
28
+ Mensa::Filter.comparable_value(filter.operator, view_filter[:value]) == filter.comparable_value
35
29
  end
36
30
  end
37
31
  end
@@ -471,16 +471,11 @@ export default class FilterPillListComponentController extends ApplicationContro
471
471
  this.hasMensaAddFilterOutlet &&
472
472
  this.mensaAddFilterOutlet.selectedFilterColumn
473
473
  ) {
474
- const isMultiple = this.mensaAddFilterOutlet.isMultipleMode;
475
474
  const filter = {
476
475
  operator: this.mensaAddFilterOutlet.operator,
477
476
  };
478
477
  if (this.mensaAddFilterOutlet.operatorRequiresValue()) {
479
- filter.value = isMultiple
480
- ? this.mensaAddFilterOutlet.selectedValues
481
- : this.mensaAddFilterOutlet.hasValueTarget
482
- ? this.mensaAddFilterOutlet.valueTarget.value
483
- : "";
478
+ filter.value = this.mensaAddFilterOutlet.filterValue;
484
479
  }
485
480
  filters[this.mensaAddFilterOutlet.selectedFilterColumn] = filter;
486
481
  }
@@ -287,7 +287,7 @@ module Mensa
287
287
  # Operator and value per column, the value only when the operator takes one
288
288
  def comparable_filters(filters)
289
289
  filters.to_h do |filter|
290
- value = Array(filter.value).map(&:to_s).reject(&:blank?).sort if filter.operator_with_value?
290
+ value = filter.comparable_value if filter.operator_with_value?
291
291
  [filter.column.name, [filter.operator, value]]
292
292
  end
293
293
  end
@@ -86,6 +86,23 @@ module Mensa::Config
86
86
 
87
87
  option :view, dsl_hash: Mensa::Config::ViewDsl
88
88
 
89
+ # Reusable part of a view, which views apply with `include :name`:
90
+ #
91
+ # partial :open do
92
+ # filter :state do
93
+ # operator :isnt
94
+ # value ["completed", "cancelled"]
95
+ # end
96
+ # end
97
+ def partial(name, &block)
98
+ partials[name.to_sym] = block
99
+ end
100
+
101
+ def view(name = nil, &block)
102
+ config[:views] ||= {}
103
+ config[:views][name] = Mensa::Config::ViewDsl.new(nil, partials: partials, &block).config
104
+ end
105
+
89
106
  # Syntactic sugar for `column :carrier_id do internal true end`.
90
107
  def internal(name, &block)
91
108
  column(name) do
@@ -94,5 +111,11 @@ module Mensa::Config
94
111
  instance_exec(&block) if block
95
112
  end
96
113
  end
114
+
115
+ private
116
+
117
+ def partials
118
+ @partials ||= {}
119
+ end
97
120
  end
98
121
  end
@@ -9,5 +9,24 @@ module Mensa::Config
9
9
  option :hidden_columns
10
10
  option :group_by
11
11
  option :aggregates
12
+
13
+ def initialize(name, partials: {}, &)
14
+ @partials = partials
15
+ super(name, &)
16
+ end
17
+
18
+ # Applies a partial, defined in the table with `partial :name do ... end`,
19
+ # or a block, as if its contents were written here. What follows the
20
+ # include overrides it, e.g. a filter on the same column.
21
+ #
22
+ # view :sales_orders do
23
+ # include :open
24
+ # end
25
+ def include(partial)
26
+ block = partial.is_a?(Proc) ? partial : @partials[partial.to_sym]
27
+ raise ArgumentError, "Unknown view partial #{partial.inspect}, define it with `partial #{partial.inspect} do ... end` before the views that include it" unless block
28
+
29
+ instance_exec(&block)
30
+ end
12
31
  end
13
32
  end