@globalbrain/sefirot 4.48.1 → 4.50.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.
|
@@ -146,7 +146,7 @@ export interface DecimalFieldData extends FieldDataBase {
|
|
|
146
146
|
|
|
147
147
|
export interface SelectFieldData extends FieldDataBase {
|
|
148
148
|
type: 'select'
|
|
149
|
-
displayAs: 'text' | 'state'
|
|
149
|
+
displayAs: 'text' | 'state' | 'pills'
|
|
150
150
|
inputAs: 'dropdown' | 'radio'
|
|
151
151
|
placeholderEn: string | null
|
|
152
152
|
placeholderJa: string | null
|
package/lib/blocks/lens/Rule.ts
CHANGED
|
@@ -1,19 +1,83 @@
|
|
|
1
1
|
export type Rule =
|
|
2
2
|
| MaxLengthRule
|
|
3
|
+
| MinLengthRule
|
|
4
|
+
| MinValueRule
|
|
5
|
+
| MaxValueRule
|
|
3
6
|
| RequiredRule
|
|
4
7
|
| UniqueRule
|
|
8
|
+
| EmailRule
|
|
9
|
+
| UrlRule
|
|
10
|
+
| DecimalRule
|
|
11
|
+
| DecimalOrHyphenRule
|
|
12
|
+
| PositiveIntegerRule
|
|
13
|
+
| NegativeIntegerRule
|
|
14
|
+
| ZeroOrPositiveIntegerRule
|
|
15
|
+
| ZeroOrNegativeIntegerRule
|
|
16
|
+
| CheckedRule
|
|
5
17
|
| SlackChannelLinkRule
|
|
6
18
|
| SlackChannelNameRule
|
|
7
19
|
| BeforeRule
|
|
8
20
|
| BeforeOrEqualRule
|
|
9
21
|
| AfterRule
|
|
10
22
|
| AfterOrEqualRule
|
|
23
|
+
| EachRule
|
|
11
24
|
|
|
12
25
|
export interface MaxLengthRule {
|
|
13
26
|
type: 'max_length'
|
|
14
27
|
length: number
|
|
15
28
|
}
|
|
16
29
|
|
|
30
|
+
export interface MinLengthRule {
|
|
31
|
+
type: 'min_length'
|
|
32
|
+
length: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MinValueRule {
|
|
36
|
+
type: 'min_value'
|
|
37
|
+
value: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MaxValueRule {
|
|
41
|
+
type: 'max_value'
|
|
42
|
+
value: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EmailRule {
|
|
46
|
+
type: 'email'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UrlRule {
|
|
50
|
+
type: 'url'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DecimalRule {
|
|
54
|
+
type: 'decimal'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DecimalOrHyphenRule {
|
|
58
|
+
type: 'decimal_or_hyphen'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PositiveIntegerRule {
|
|
62
|
+
type: 'positive_integer'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NegativeIntegerRule {
|
|
66
|
+
type: 'negative_integer'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ZeroOrPositiveIntegerRule {
|
|
70
|
+
type: 'zero_or_positive_integer'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ZeroOrNegativeIntegerRule {
|
|
74
|
+
type: 'zero_or_negative_integer'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CheckedRule {
|
|
78
|
+
type: 'checked'
|
|
79
|
+
}
|
|
80
|
+
|
|
17
81
|
export interface RequiredRule {
|
|
18
82
|
type: 'required'
|
|
19
83
|
}
|
|
@@ -50,3 +114,12 @@ export interface AfterOrEqualRule {
|
|
|
50
114
|
type: 'after_or_equal'
|
|
51
115
|
date: string
|
|
52
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Applies the nested `rules` to every element of an array value, mirroring
|
|
120
|
+
* Laravel's `field.*` wildcard on the backend.
|
|
121
|
+
*/
|
|
122
|
+
export interface EachRule {
|
|
123
|
+
type: 'each'
|
|
124
|
+
rules: Rule[]
|
|
125
|
+
}
|
|
@@ -3,6 +3,8 @@ import IconPencilSimple from '~icons/ph/pencil-simple'
|
|
|
3
3
|
import { useElementBounding } from '@vueuse/core'
|
|
4
4
|
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from 'vue'
|
|
5
5
|
import SButton from '../../../components/SButton.vue'
|
|
6
|
+
import SPill, { type Mode as PillMode } from '../../../components/SPill.vue'
|
|
7
|
+
import SState, { type Mode as StateMode } from '../../../components/SState.vue'
|
|
6
8
|
import { useManualDropdownPosition } from '../../../composables/Dropdown'
|
|
7
9
|
import { useTrans } from '../../../composables/Lang'
|
|
8
10
|
import { useValidation } from '../../../composables/Validation'
|
|
@@ -54,21 +56,37 @@ onUnmounted(() => {
|
|
|
54
56
|
|
|
55
57
|
// Reuse the field's own table-cell rendering for the display value so the
|
|
56
58
|
// label mapping (e.g. select option labels) matches the read-only columns.
|
|
57
|
-
|
|
58
|
-
const displayValue = computed(() => {
|
|
59
|
+
const resolvedCell = computed<any>(() => {
|
|
59
60
|
try {
|
|
60
61
|
const cell = props.field.tableColumn().cell
|
|
61
|
-
|
|
62
|
-
if (resolved && (resolved.type === 'text' || resolved.type === 'number')) {
|
|
63
|
-
return resolved.value ?? ''
|
|
64
|
-
}
|
|
65
|
-
// `state` cells (e.g. a select with displayAs: 'state') carry the localized
|
|
66
|
-
// label, not the raw payload — show that rather than falling through.
|
|
67
|
-
if (resolved && resolved.type === 'state') {
|
|
68
|
-
return resolved.label ?? ''
|
|
69
|
-
}
|
|
62
|
+
return typeof cell === 'function' ? cell(props.value, props.record) : cell
|
|
70
63
|
} catch {
|
|
71
|
-
// Field types without a
|
|
64
|
+
// Field types without a usable cell fall through to the generic display.
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// A `pills` cell (e.g. a multi-select with displayAs: 'pills') renders as pills
|
|
70
|
+
// rather than text, mirroring the read-only `STableCellPills` column.
|
|
71
|
+
const displayPills = computed<{ label: string; color?: PillMode }[] | null>(() => {
|
|
72
|
+
const cell = resolvedCell.value
|
|
73
|
+
return cell && cell.type === 'pills' ? cell.pills : null
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// A `state` cell (e.g. a select with displayAs: 'state') renders as a status
|
|
77
|
+
// badge rather than its bare label, mirroring the read-only `STableCellState`
|
|
78
|
+
// column.
|
|
79
|
+
const displayState = computed<{ label: string; mode?: StateMode } | null>(() => {
|
|
80
|
+
const cell = resolvedCell.value
|
|
81
|
+
return cell && cell.type === 'state' ? { label: cell.label, mode: cell.mode } : null
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Falls back to a plain representation for non-text displays (pills and state
|
|
85
|
+
// cells are rendered as their own components in the template above).
|
|
86
|
+
const displayValue = computed(() => {
|
|
87
|
+
const resolved: any = resolvedCell.value
|
|
88
|
+
if (resolved && (resolved.type === 'text' || resolved.type === 'number')) {
|
|
89
|
+
return resolved.value ?? ''
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
const v = props.value
|
|
@@ -184,7 +202,22 @@ function isTextLikeInput(target: EventTarget | null): boolean {
|
|
|
184
202
|
|
|
185
203
|
<template>
|
|
186
204
|
<div ref="anchor" class="LensTableEditableCell" :class="{ editing }">
|
|
187
|
-
<
|
|
205
|
+
<div v-if="displayPills" class="pills">
|
|
206
|
+
<SPill
|
|
207
|
+
v-for="(pill, i) in displayPills"
|
|
208
|
+
:key="i"
|
|
209
|
+
size="mini"
|
|
210
|
+
:mode="pill.color"
|
|
211
|
+
:label="pill.label"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<SState
|
|
215
|
+
v-else-if="displayState"
|
|
216
|
+
size="mini"
|
|
217
|
+
:mode="displayState.mode"
|
|
218
|
+
:label="displayState.label"
|
|
219
|
+
/>
|
|
220
|
+
<span v-else class="value">{{ displayValue }}</span>
|
|
188
221
|
<button
|
|
189
222
|
class="edit"
|
|
190
223
|
type="button"
|
|
@@ -233,6 +266,15 @@ function isTextLikeInput(target: EventTarget | null): boolean {
|
|
|
233
266
|
text-overflow: ellipsis;
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
.pills {
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-wrap: nowrap;
|
|
272
|
+
align-items: center;
|
|
273
|
+
gap: 4px;
|
|
274
|
+
min-width: 0;
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
}
|
|
277
|
+
|
|
236
278
|
.edit {
|
|
237
279
|
position: absolute;
|
|
238
280
|
top: 50%;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { xor } from 'lodash-es'
|
|
2
2
|
import { h } from 'vue'
|
|
3
|
+
import SDescPill from '../../../components/SDescPill.vue'
|
|
3
4
|
import SInputCheckboxes from '../../../components/SInputCheckboxes.vue'
|
|
4
5
|
import SInputDropdown from '../../../components/SInputDropdown.vue'
|
|
5
6
|
import SInputRadios from '../../../components/SInputRadios.vue'
|
|
@@ -38,6 +39,8 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
38
39
|
switch (this.data.displayAs) {
|
|
39
40
|
case 'state':
|
|
40
41
|
return this.tableCellState(v, _r)
|
|
42
|
+
case 'pills':
|
|
43
|
+
return this.tableCellPills(v, _r)
|
|
41
44
|
case 'text':
|
|
42
45
|
return this.tableCellText(v, _r)
|
|
43
46
|
default:
|
|
@@ -45,6 +48,18 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
protected tableCellPills(v: any, _r: any): TableCell {
|
|
52
|
+
v = Array.isArray(v) ? v : [v]
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
type: 'pills',
|
|
56
|
+
pills: this.optionsForValues(v).map((o) => ({
|
|
57
|
+
label: this.labelForOption(o),
|
|
58
|
+
color: o.mode
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
protected tableCellText(v: any, _r: any): TableCell {
|
|
49
64
|
v = Array.isArray(v) ? v : [v]
|
|
50
65
|
|
|
@@ -95,6 +110,8 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
95
110
|
switch (this.data.displayAs) {
|
|
96
111
|
case 'state':
|
|
97
112
|
return this.renderDataListItemValueForState(value)
|
|
113
|
+
case 'pills':
|
|
114
|
+
return this.renderDataListItemValueForPills(value)
|
|
98
115
|
case 'text':
|
|
99
116
|
return this.renderDataListItemValueForText(value)
|
|
100
117
|
default:
|
|
@@ -103,6 +120,17 @@ export class SelectField extends Field<SelectFieldData> {
|
|
|
103
120
|
})
|
|
104
121
|
}
|
|
105
122
|
|
|
123
|
+
protected renderDataListItemValueForPills(value: any): any {
|
|
124
|
+
value = Array.isArray(value) ? value : [value]
|
|
125
|
+
|
|
126
|
+
return h(SDescPill, {
|
|
127
|
+
pill: this.optionsForValues(value).map((o) => ({
|
|
128
|
+
label: this.labelForOption(o),
|
|
129
|
+
mode: o.mode
|
|
130
|
+
}))
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
protected renderDataListItemValueForState(value: any): any {
|
|
107
135
|
if (this.data.multiple) {
|
|
108
136
|
throw new Error('Displaying select field as state with multiple option is not supported.')
|
|
@@ -5,30 +5,90 @@ import {
|
|
|
5
5
|
afterOrEqual,
|
|
6
6
|
before,
|
|
7
7
|
beforeOrEqual,
|
|
8
|
+
checked,
|
|
9
|
+
decimal,
|
|
10
|
+
decimalOrHyphen,
|
|
11
|
+
email,
|
|
8
12
|
maxLength,
|
|
13
|
+
maxValue,
|
|
14
|
+
minLength,
|
|
15
|
+
minValue,
|
|
16
|
+
negativeInteger,
|
|
17
|
+
positiveInteger,
|
|
9
18
|
required,
|
|
19
|
+
rule,
|
|
10
20
|
slackChannelLink,
|
|
11
|
-
slackChannelName
|
|
21
|
+
slackChannelName,
|
|
22
|
+
url,
|
|
23
|
+
zeroOrNegativeInteger,
|
|
24
|
+
zeroOrPositiveInteger
|
|
12
25
|
} from '../../../validation/rules'
|
|
13
|
-
import { type Rule } from '../Rule'
|
|
26
|
+
import { type EachRule, type Rule } from '../Rule'
|
|
14
27
|
|
|
15
28
|
/**
|
|
16
29
|
* Maps field rules to vuelidate validation rules.
|
|
17
30
|
*/
|
|
18
31
|
export function map(rules: Rule[]): ValidationArgs {
|
|
19
|
-
|
|
32
|
+
const carry = rules.reduce((carry: ValidationArgs<any>, rule: Rule) => {
|
|
33
|
+
// `each` rules are handled separately below so that multiple of them on the
|
|
34
|
+
// same field compose rather than overwrite.
|
|
35
|
+
if (rule.type === 'each') {
|
|
36
|
+
return carry
|
|
37
|
+
}
|
|
20
38
|
const mapped = mapRule(rule)
|
|
21
39
|
if (mapped) {
|
|
22
40
|
carry[rule.type] = mapped
|
|
23
41
|
}
|
|
24
42
|
return carry
|
|
25
43
|
}, {})
|
|
44
|
+
|
|
45
|
+
// `each` validates every element of an array value rather than the value
|
|
46
|
+
// itself, so it maps to a single per-element validator (see `mapEach`). If a
|
|
47
|
+
// field carries more than one `each` rule, merge their child rules so every
|
|
48
|
+
// wildcard rule runs — the backend accumulates them the same way. A single
|
|
49
|
+
// `each` is the common case.
|
|
50
|
+
const eachRules = rules.filter((rule): rule is EachRule => rule.type === 'each')
|
|
51
|
+
if (eachRules.length) {
|
|
52
|
+
carry.each = mapEach({ type: 'each', rules: eachRules.flatMap((rule) => rule.rules) })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return carry
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds a validator that applies an `each` rule's nested rules to every
|
|
60
|
+
* element of an array value. vuelidate's `forEach` only handles arrays of
|
|
61
|
+
* objects, so for the scalar arrays Lens fields hold (multi-select values,
|
|
62
|
+
* related ids) we run each element through the mapped child validators
|
|
63
|
+
* directly. Mirrors the backend, which expands the same rules onto Laravel's
|
|
64
|
+
* `key.*` wildcard. A non-array value fails; an empty/absent value is left to
|
|
65
|
+
* `required` (the rule is optional on its own).
|
|
66
|
+
*/
|
|
67
|
+
function mapEach(eachRule: EachRule): ValidationRuleWithParams {
|
|
68
|
+
// Map each child rule straight to a validator rather than through the
|
|
69
|
+
// type-keyed `map()` object, so two child rules of the same family are both
|
|
70
|
+
// kept (the backend's `key.*` expansion is additive and de-dupes nothing).
|
|
71
|
+
// Server-only children (e.g. `unique`) map to null and are dropped.
|
|
72
|
+
const childValidators = eachRule.rules
|
|
73
|
+
.map((r) => (r.type === 'each' ? mapEach(r) : mapRule(r)))
|
|
74
|
+
.filter((v): v is ValidationRuleWithParams => v !== null)
|
|
75
|
+
|
|
76
|
+
return rule((value) =>
|
|
77
|
+
Array.isArray(value)
|
|
78
|
+
&& value.every((item) => childValidators.every((v) => v.$validator(item, {}, {}) === true))
|
|
79
|
+
)
|
|
26
80
|
}
|
|
27
81
|
|
|
28
|
-
function mapRule(rule: Rule): ValidationRuleWithParams | null {
|
|
82
|
+
function mapRule(rule: Exclude<Rule, EachRule>): ValidationRuleWithParams | null {
|
|
29
83
|
switch (rule.type) {
|
|
30
84
|
case 'max_length':
|
|
31
85
|
return maxLength(rule.length)
|
|
86
|
+
case 'min_length':
|
|
87
|
+
return minLength(rule.length)
|
|
88
|
+
case 'min_value':
|
|
89
|
+
return minValue(rule.value)
|
|
90
|
+
case 'max_value':
|
|
91
|
+
return maxValue(rule.value)
|
|
32
92
|
case 'required':
|
|
33
93
|
return required()
|
|
34
94
|
case 'unique':
|
|
@@ -36,6 +96,24 @@ function mapRule(rule: Rule): ValidationRuleWithParams | null {
|
|
|
36
96
|
// database). The backend enforces it and returns a 422 with field
|
|
37
97
|
// errors, so there is no client-side equivalent to apply here.
|
|
38
98
|
return null
|
|
99
|
+
case 'email':
|
|
100
|
+
return email()
|
|
101
|
+
case 'url':
|
|
102
|
+
return url()
|
|
103
|
+
case 'decimal':
|
|
104
|
+
return decimal()
|
|
105
|
+
case 'decimal_or_hyphen':
|
|
106
|
+
return decimalOrHyphen()
|
|
107
|
+
case 'positive_integer':
|
|
108
|
+
return positiveInteger()
|
|
109
|
+
case 'negative_integer':
|
|
110
|
+
return negativeInteger()
|
|
111
|
+
case 'zero_or_positive_integer':
|
|
112
|
+
return zeroOrPositiveInteger()
|
|
113
|
+
case 'zero_or_negative_integer':
|
|
114
|
+
return zeroOrNegativeInteger()
|
|
115
|
+
case 'checked':
|
|
116
|
+
return checked()
|
|
39
117
|
case 'slack_channel_link':
|
|
40
118
|
return slackChannelLink()
|
|
41
119
|
case 'slack_channel_name':
|