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 +4 -4
- data/.devcontainer/devcontainer-lock.json +4 -4
- data/.devcontainer/devcontainer.json +16 -7
- data/.devcontainer/docker-compose.yml +74 -13
- data/.devcontainer/env.conf +29 -0
- data/.devcontainer/postStart.sh +7 -2
- data/.gitignore +2 -1
- data/.shipkit.yml +1 -1
- data/.worktreeinclude +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +56 -4
- data/app/components/mensa/add_filter/component.css +9 -0
- data/app/components/mensa/add_filter/component_controller.js +55 -2
- data/app/components/mensa/filter_pill/component.rb +1 -7
- data/app/components/mensa/filter_pill_list/component_controller.js +1 -6
- data/app/tables/mensa/base.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +23 -0
- data/app/tables/mensa/config/view_dsl.rb +19 -0
- data/app/tables/mensa/filter.rb +51 -5
- data/app/views/mensa/tables/filters/show.turbo_stream.erb +15 -8
- data/bin/de +868 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/nl.yml +5 -0
- data/lib/mensa/verifier.rb +2 -0
- data/lib/mensa/version.rb +1 -1
- data/mise.toml +1 -3
- metadata +4 -2
- data/.devcontainer/initialize.sh +0 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd784c73c622f5a54cc41cd377bdd18021f82f0249be3e55b723f5aaf539e4a2
|
|
4
|
+
data.tar.gz: e202a9ee523e3c43a3469b88d9fc8947ec20c2bdff2a80348e5481486e8908a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:
|
|
6
|
-
"integrity": "sha256:
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/.devcontainer/postStart.sh
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
4
|
MISE="$HOME/.local/bin/mise"
|
|
5
|
-
|
|
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
|
-
|
|
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
data/.shipkit.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
---
|
|
2
|
-
previous_version: 0.9.
|
|
2
|
+
previous_version: 0.9.5
|
data/.worktreeinclude
ADDED
data/Gemfile.lock
CHANGED
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
|
-
`
|
|
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.
|
|
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.
|
|
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, """);
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|
data/app/tables/mensa/base.rb
CHANGED
|
@@ -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 =
|
|
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
|