@globalbrain/sefirot 4.46.0 → 4.48.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.
Files changed (31) hide show
  1. package/lib/blocks/lens/FieldData.ts +27 -0
  2. package/lib/blocks/lens/ResourceFetcher.ts +5 -1
  3. package/lib/blocks/lens/Rule.ts +10 -0
  4. package/lib/blocks/lens/components/LensCatalog.vue +826 -39
  5. package/lib/blocks/lens/components/LensSheet.vue +465 -0
  6. package/lib/blocks/lens/components/LensSheetField.vue +210 -0
  7. package/lib/blocks/lens/components/LensTable.vue +79 -5
  8. package/lib/blocks/lens/components/LensTableEditableCell.vue +285 -0
  9. package/lib/blocks/lens/composables/CatalogUrlQuerySync.ts +146 -0
  10. package/lib/blocks/lens/composables/LensEdit.ts +59 -0
  11. package/lib/blocks/lens/composables/LensInlineEdit.ts +36 -0
  12. package/lib/blocks/lens/composables/ResourceFetcher.ts +20 -7
  13. package/lib/blocks/lens/composables/SetupLens.ts +2 -0
  14. package/lib/blocks/lens/fields/AvatarField.ts +43 -0
  15. package/lib/blocks/lens/fields/ContentField.ts +21 -10
  16. package/lib/blocks/lens/fields/Field.ts +26 -1
  17. package/lib/blocks/lens/fields/FileUploadField.ts +8 -0
  18. package/lib/blocks/lens/fields/RelatedManyField.ts +44 -7
  19. package/lib/blocks/lens/fields/RelatedOneField.ts +42 -7
  20. package/lib/blocks/lens/validation/RuleMapper.ts +22 -3
  21. package/lib/blocks/lens/validation/ServerErrors.ts +25 -0
  22. package/lib/components/SInputSwitches.vue +2 -2
  23. package/lib/components/SSheet.vue +104 -0
  24. package/lib/composables/Api.ts +18 -10
  25. package/lib/composables/Url.ts +19 -2
  26. package/lib/styles/variables.css +1 -0
  27. package/lib/validation/rules/index.ts +1 -0
  28. package/lib/validation/rules/slackChannelLink.ts +15 -0
  29. package/lib/validation/validators/index.ts +1 -0
  30. package/lib/validation/validators/slackChannelLink.ts +52 -0
  31. package/package.json +17 -17
@@ -4,6 +4,7 @@ import { type LocationQuery, useRoute, useRouter } from 'vue-router'
4
4
 
5
5
  export interface UseUrlQuerySyncOptions {
6
6
  casts?: Record<string, (value: any) => any>
7
+ serializers?: Record<string, (value: any) => string>
7
8
  exclude?: string[]
8
9
  }
9
10
 
@@ -16,7 +17,7 @@ export interface UseUrlQuerySyncOptions {
16
17
  */
17
18
  export function useUrlQuerySync(
18
19
  state: MaybeRef<Record<string, any>>,
19
- { casts = {}, exclude = [] }: UseUrlQuerySyncOptions = {}
20
+ { casts = {}, serializers = {}, exclude = [] }: UseUrlQuerySyncOptions = {}
20
21
  ): void {
21
22
  const route = useRoute()
22
23
  const router = useRouter()
@@ -78,11 +79,27 @@ export function useUrlQuerySync(
78
79
 
79
80
  const currentQuery = normalizeQuery(route.query)
80
81
 
82
+ // Both sides of this comparison hold deserialized values: `newQuery`
83
+ // carries raw state values and `normalizeQuery` runs `casts` on the
84
+ // URL params. Serialization must therefore only happen on the final
85
+ // write below — serializing before the comparison would make a
86
+ // round-tripped value always look different from the state and cause
87
+ // an endless replace loop.
81
88
  if (!isEqual(newQuery, currentQuery)) {
82
- await router.replace({ query: unflattenObject(newQuery) })
89
+ await router.replace({ query: unflattenObject(serializeQuery(newQuery)) })
83
90
  }
84
91
  }
85
92
 
93
+ function serializeQuery(query: Record<string, any>): Record<string, any> {
94
+ const result: Record<string, any> = {}
95
+
96
+ for (const key in query) {
97
+ result[key] = serializers[key] ? serializers[key](query[key]) : query[key]
98
+ }
99
+
100
+ return result
101
+ }
102
+
86
103
  function normalizeQuery(query: LocationQuery): Record<string, any> {
87
104
  const flattenedQuery = flattenObject(query)
88
105
  const result: Record<string, any> = {}
@@ -376,6 +376,7 @@
376
376
  --z-index-tooltip: 10;
377
377
  --z-index-dropdown: 1000;
378
378
  --z-index-backdrop: 2000;
379
+ --z-index-sheet: 2000;
379
380
  }
380
381
 
381
382
  /**
@@ -24,6 +24,7 @@ export { requiredIf } from './requiredIf'
24
24
  export { requiredYmd } from './requiredYmd'
25
25
  export { requiredYmdIf } from './requiredYmdIf'
26
26
  export { rule } from './rule'
27
+ export { slackChannelLink } from './slackChannelLink'
27
28
  export { slackChannelName } from './slackChannelName'
28
29
  export { url } from './url'
29
30
  export { ymd } from './ymd'
@@ -0,0 +1,15 @@
1
+ import { createRule } from '../Rule'
2
+ import { slackChannelLink as baseSlackChannelLink } from '../validators'
3
+
4
+ export const message = {
5
+ en: 'The slack channel link is invalid.',
6
+ ja: 'Slackチャンネルリンクの形式が正しくありません。'
7
+ }
8
+
9
+ export function slackChannelLink(msg?: string) {
10
+ return createRule({
11
+ message: ({ lang }) => msg ?? message[lang],
12
+ optional: true,
13
+ validation: baseSlackChannelLink
14
+ })
15
+ }
@@ -21,6 +21,7 @@ export * from './required'
21
21
  export * from './requiredHmsIf'
22
22
  export * from './requiredIf'
23
23
  export * from './requiredYmdIf'
24
+ export * from './slackChannelLink'
24
25
  export * from './slackChannelName'
25
26
  export * from './url'
26
27
  export * from './ymd'
@@ -0,0 +1,52 @@
1
+ const channelIdRE = /^[CG][A-Z0-9]{6,}$/
2
+
3
+ /**
4
+ * Checks that the value identifies a Slack channel. Accepts a channel link as
5
+ * copied from Slack (`https://<workspace>.slack.com/archives/<id>`, with or
6
+ * without a trailing message path), a browser URL
7
+ * (`https://app.slack.com/client/<team-id>/<id>`), or a raw channel ID.
8
+ * Mirrors the backend `slack_channel_link` rule definition.
9
+ */
10
+ export function slackChannelLink(value: unknown): boolean {
11
+ return typeof value === 'string' && extractChannelId(value) !== null
12
+ }
13
+
14
+ function extractChannelId(value: string): string | null {
15
+ const trimmed = value.trim()
16
+
17
+ if (channelIdRE.test(trimmed)) {
18
+ return trimmed
19
+ }
20
+
21
+ return extractChannelIdFromUrl(trimmed)
22
+ }
23
+
24
+ function extractChannelIdFromUrl(value: string): string | null {
25
+ let url: URL
26
+
27
+ try {
28
+ url = new URL(value)
29
+ } catch {
30
+ return null
31
+ }
32
+
33
+ if (url.protocol !== 'https:') {
34
+ return null
35
+ }
36
+
37
+ if (url.hostname !== 'slack.com' && !url.hostname.endsWith('.slack.com')) {
38
+ return null
39
+ }
40
+
41
+ const segments = url.pathname.split('/').filter((segment) => segment !== '')
42
+
43
+ let candidate: string | undefined
44
+
45
+ if (segments[0] === 'archives') {
46
+ candidate = segments[1]
47
+ } else if (url.hostname === 'app.slack.com' && segments[0] === 'client') {
48
+ candidate = segments[2]
49
+ }
50
+
51
+ return candidate !== undefined && channelIdRE.test(candidate) ? candidate : null
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "4.46.0",
3
+ "version": "4.48.0",
4
4
  "description": "Vue Components for Global Brain Design System.",
5
5
  "keywords": [
6
6
  "components",
@@ -53,14 +53,14 @@
53
53
  "test:coverage": "vitest run --coverage",
54
54
  "check": "pnpm run --aggregate-output '/^(type|lint|test:fail)$/'",
55
55
  "check:fail": "pnpm run --aggregate-output '/^(type|lint:fail|test:fail)$/'",
56
- "release": "pnpm whoami >/dev/null 2>&1 || pnpm login && release-it"
56
+ "release": "npm whoami >/dev/null 2>&1 || npm login && release-it"
57
57
  },
58
58
  "dependencies": {
59
59
  "@iconify-json/ph": "^1.2.2",
60
60
  "@iconify-json/ri": "^1.2.10",
61
61
  "@popperjs/core": "^2.11.8",
62
- "@sentry/browser": "^10.55.0",
63
- "@sentry/vue": "^10.55.0",
62
+ "@sentry/browser": "^10.58.0",
63
+ "@sentry/vue": "^10.58.0",
64
64
  "@tanstack/vue-virtual": "3.0.0-beta.62",
65
65
  "@tinyhttp/content-disposition": "^2.2.4",
66
66
  "@tinyhttp/cookie": "^2.1.1",
@@ -71,15 +71,15 @@
71
71
  "@types/markdown-it": "^14.1.2",
72
72
  "@types/qs": "^6.15.1",
73
73
  "@vitejs/plugin-vue": "^6.0.7",
74
- "@vue/reactivity": "^3.5.35",
74
+ "@vue/reactivity": "^3.5.38",
75
75
  "@vuelidate/core": "^2.0.3",
76
76
  "@vuelidate/validators": "^2.0.4",
77
77
  "@vueuse/core": "^14.3.0",
78
78
  "body-scroll-lock": "4.0.0-beta.0",
79
79
  "d3": "^7.9.0",
80
80
  "dayjs": "^1.11.21",
81
- "dompurify": "^3.4.7",
82
- "fuse.js": "^7.3.0",
81
+ "dompurify": "^3.4.11",
82
+ "fuse.js": "^7.4.2",
83
83
  "html2canvas": "^1.4.1",
84
84
  "jsdom": "^29.1.1",
85
85
  "lodash-es": "^4.18.1",
@@ -94,8 +94,8 @@
94
94
  "qs": "^6.15.2",
95
95
  "unplugin-icons": "^23.0.1",
96
96
  "v-calendar": "3.0.1",
97
- "vite": "^7.3.3",
98
- "vue": "^3.5.35",
97
+ "vite": "^7.3.5",
98
+ "vue": "^3.5.38",
99
99
  "vue-draggable-plus": "^0.6.1",
100
100
  "vue-router": "^5.1.0"
101
101
  },
@@ -104,18 +104,18 @@
104
104
  "@histoire/plugin-vue": "1.0.0-beta.1",
105
105
  "@release-it/conventional-changelog": "^11.0.1",
106
106
  "@types/jsdom": "^28.0.3",
107
- "@types/node": "^25.9.1",
108
- "@typescript-eslint/rule-tester": "^8.60.0",
109
- "@vitest/coverage-v8": "^4.1.7",
110
- "@vue/test-utils": "^2.4.10",
107
+ "@types/node": "^26.0.0",
108
+ "@typescript-eslint/rule-tester": "^8.61.1",
109
+ "@vitest/coverage-v8": "^4.1.9",
110
+ "@vue/test-utils": "^2.4.11",
111
111
  "eslint": "^9.39.4",
112
- "happy-dom": "^20.9.0",
112
+ "happy-dom": "^20.10.5",
113
113
  "histoire": "1.0.0-beta.1",
114
114
  "release-it": "^20.2.0",
115
115
  "typescript": "~6.0.3",
116
116
  "vitepress": "^2.0.0-alpha.17",
117
- "vitest": "^4.1.7",
118
- "vue-tsc": "^3.3.3"
117
+ "vitest": "^4.1.9",
118
+ "vue-tsc": "^3.3.5"
119
119
  },
120
- "packageManager": "pnpm@11.5.0"
120
+ "packageManager": "pnpm@11.7.0"
121
121
  }