@globalbrain/sefirot 4.53.0 → 4.55.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.
@@ -21,6 +21,7 @@ export type Rule =
21
21
  | AfterRule
22
22
  | AfterOrEqualRule
23
23
  | FileExtensionRule
24
+ | MimeTypesRule
24
25
  | MaxFileSizeRule
25
26
  | EachRule
26
27
 
@@ -122,6 +123,11 @@ export interface FileExtensionRule {
122
123
  extensions: string[]
123
124
  }
124
125
 
126
+ export interface MimeTypesRule {
127
+ type: 'mime_types'
128
+ mimeTypes: string[]
129
+ }
130
+
125
131
  export interface MaxFileSizeRule {
126
132
  type: 'max_file_size'
127
133
  size: string
@@ -39,9 +39,17 @@ const orderTextDict = {
39
39
  }
40
40
 
41
41
  function getFieldName(sort: LensQuerySort): string {
42
- return lang === 'ja'
43
- ? props.fields[sort[0]].labelJa
44
- : props.fields[sort[0]].labelEn
42
+ // A still-applied sort can reference a field that's not in the field set — a
43
+ // stale saved/URL sort, or a sort emitted under a companion key (e.g. an
44
+ // avatar sorting by its display-name field). Fall back to the raw key rather
45
+ // than crash, mirroring how the filter chip handles an unknown field.
46
+ const field = props.fields[sort[0]]
47
+
48
+ if (!field) {
49
+ return sort[0]
50
+ }
51
+
52
+ return lang === 'ja' ? field.labelJa : field.labelEn
45
53
  }
46
54
 
47
55
  function getOrderText(sort: LensQuerySort): string {
@@ -29,6 +29,13 @@ export class AvatarField extends Field<AvatarFieldData> {
29
29
  return {}
30
30
  }
31
31
 
32
+ // Sort by the display-name companion (resolved by the active language), since
33
+ // the field value itself is the image URL. With no companion configured there's
34
+ // nothing meaningful to sort on, so the field stays unsortable (no sort menu).
35
+ protected override sortKey(): string | null {
36
+ return this.nameKey()
37
+ }
38
+
32
39
  override dataListItemComponent(): any {
33
40
  return this.defineDataListItemComponent((value) => {
34
41
  // Render nothing for a missing image so SDataListItem shows its empty
@@ -81,12 +81,25 @@ export abstract class Field<T extends FieldData> {
81
81
  }
82
82
  }
83
83
 
84
+ /**
85
+ * The field key sorts are emitted under — the catalog sends it to the backend,
86
+ * which orders by that field's column. Defaults to the field's own key; a
87
+ * subtype whose own value isn't meaningful to sort on can redirect it (e.g. an
88
+ * avatar sorts by its display-name companion). A `null` makes the field
89
+ * unsortable, so no sort menu is shown.
90
+ */
91
+ protected sortKey(): string | null {
92
+ return this.data.key
93
+ }
94
+
84
95
  /**
85
96
  * Renders the table sort menu for the field. The sort maybe null when
86
- * the field `sortable` option is false.
97
+ * the field `sortable` option is false (or it has no sortable key).
87
98
  */
88
99
  tableSortMenu(onSortUpdated: (sort: LensQuerySort) => void): DropdownSection | null {
89
- if (!this.data.sortable) {
100
+ const sortKey = this.sortKey()
101
+
102
+ if (!this.data.sortable || sortKey === null) {
90
103
  return null
91
104
  }
92
105
 
@@ -100,8 +113,8 @@ export abstract class Field<T extends FieldData> {
100
113
  return {
101
114
  type: 'menu',
102
115
  options: [
103
- { label: t.sort_asc, onClick: () => onSortUpdated?.([this.data.key, 'asc']) },
104
- { label: t.sort_desc, onClick: () => onSortUpdated?.([this.data.key, 'desc']) }
116
+ { label: t.sort_asc, onClick: () => onSortUpdated?.([sortKey, 'asc']) },
117
+ { label: t.sort_desc, onClick: () => onSortUpdated?.([sortKey, 'desc']) }
105
118
  ]
106
119
  }
107
120
  }
@@ -13,6 +13,7 @@ import {
13
13
  maxFileSize,
14
14
  maxLength,
15
15
  maxValue,
16
+ mimeTypes,
16
17
  minLength,
17
18
  minValue,
18
19
  negativeInteger,
@@ -130,6 +131,8 @@ function mapRule(rule: Exclude<Rule, EachRule>): ValidationRuleWithParams | null
130
131
  return afterOrEqual(resolveDate(rule.date))
131
132
  case 'file_extension':
132
133
  return fileExtension(rule.extensions)
134
+ case 'mime_types':
135
+ return mimeTypes(rule.mimeTypes)
133
136
  case 'max_file_size':
134
137
  return maxFileSize(rule.size)
135
138
  default: {
@@ -12,6 +12,7 @@ export { maxFileSize } from './maxFileSize'
12
12
  export { maxLength } from './maxLength'
13
13
  export { maxTotalFileSize } from './maxTotalFileSize'
14
14
  export { maxValue } from './maxValue'
15
+ export { mimeTypes } from './mimeTypes'
15
16
  export { minLength } from './minLength'
16
17
  export { minValue } from './minValue'
17
18
  export { month } from './month'
@@ -0,0 +1,15 @@
1
+ import { createRule } from '../Rule'
2
+ import { mimeTypes as baseMimeTypes } from '../validators/mimeTypes'
3
+
4
+ export const message = {
5
+ en: 'The file type is invalid.',
6
+ ja: 'ファイル形式が正しくありません。'
7
+ }
8
+
9
+ export function mimeTypes(patterns: string[], msg?: string) {
10
+ return createRule({
11
+ message: ({ lang }) => msg ?? message[lang],
12
+ optional: true,
13
+ validation: (value) => baseMimeTypes(value, patterns)
14
+ })
15
+ }
@@ -12,6 +12,7 @@ export * from './maxFileSize'
12
12
  export * from './maxLength'
13
13
  export * from './maxTotalFileSize'
14
14
  export * from './maxValue'
15
+ export * from './mimeTypes'
15
16
  export * from './minLength'
16
17
  export * from './minValue'
17
18
  export * from './month'
@@ -0,0 +1,19 @@
1
+ export function mimeTypes(value: unknown, patterns: string[]): boolean {
2
+ if (!(value instanceof File)) { return false }
3
+
4
+ // The browser doesn't always report a type (e.g. some drag-and-drop or
5
+ // clipboard sources); defer to the server's content-based check in that case
6
+ // rather than rejecting a possibly-valid file outright.
7
+ if (value.type === '') { return true }
8
+
9
+ const type = value.type.toLowerCase()
10
+
11
+ return patterns.some((pattern) => {
12
+ const p = pattern.toLowerCase()
13
+
14
+ // A `type/*` wildcard (e.g. `image/*`) matches any subtype of that
15
+ // top-level type, mirroring the HTML accept attribute and Laravel's
16
+ // `mimetypes` rule.
17
+ return p.endsWith('/*') ? type.startsWith(p.slice(0, -1)) : type === p
18
+ })
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "4.53.0",
3
+ "version": "4.55.0",
4
4
  "description": "Vue Components for Global Brain Design System.",
5
5
  "keywords": [
6
6
  "components",