@globalbrain/sefirot 4.34.1 → 4.35.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 (55) hide show
  1. package/config/nuxt.js +44 -1
  2. package/config/vite.js +2 -3
  3. package/lib/blocks/lens/FieldContext.ts +5 -0
  4. package/lib/blocks/lens/FieldData.ts +140 -0
  5. package/lib/blocks/lens/FieldRegistry.ts +23 -0
  6. package/lib/blocks/lens/FileDownloader.ts +1 -0
  7. package/lib/blocks/lens/FilterOperator.ts +33 -0
  8. package/lib/blocks/lens/LensQuery.ts +10 -0
  9. package/lib/blocks/lens/LensResult.ts +20 -0
  10. package/lib/blocks/lens/ResourceFetcher.ts +3 -0
  11. package/lib/blocks/lens/Rule.ts +12 -0
  12. package/lib/blocks/lens/components/LensCatalog.vue +490 -0
  13. package/lib/blocks/lens/components/LensCatalogControl.vue +220 -0
  14. package/lib/blocks/lens/components/LensCatalogFooter.vue +46 -0
  15. package/lib/blocks/lens/components/LensCatalogStateFilter.vue +171 -0
  16. package/lib/blocks/lens/components/LensCatalogStateFilterCondition.vue +86 -0
  17. package/lib/blocks/lens/components/LensCatalogStateFilterGroup.vue +102 -0
  18. package/lib/blocks/lens/components/LensCatalogStateSort.vue +159 -0
  19. package/lib/blocks/lens/components/LensFormFilter.vue +169 -0
  20. package/lib/blocks/lens/components/LensFormFilterCondition.vue +205 -0
  21. package/lib/blocks/lens/components/LensFormFilterGroup.vue +175 -0
  22. package/lib/blocks/lens/components/LensFormOverride.vue +45 -0
  23. package/lib/blocks/lens/components/LensFormOverrideBase.vue +204 -0
  24. package/lib/blocks/lens/components/LensFormView.vue +347 -0
  25. package/lib/blocks/lens/components/LensTable.vue +154 -0
  26. package/lib/blocks/lens/composables/FieldFactory.ts +27 -0
  27. package/lib/blocks/lens/composables/FieldRegistry.ts +16 -0
  28. package/lib/blocks/lens/composables/FileDownloader.ts +10 -0
  29. package/lib/blocks/lens/composables/ResourceFetcher.ts +30 -0
  30. package/lib/blocks/lens/composables/SetupLens.ts +55 -0
  31. package/lib/blocks/lens/fields/ContentField.ts +34 -0
  32. package/lib/blocks/lens/fields/DateField.ts +66 -0
  33. package/lib/blocks/lens/fields/DatetimeField.ts +35 -0
  34. package/lib/blocks/lens/fields/Field.ts +244 -0
  35. package/lib/blocks/lens/fields/FileUploadField.ts +63 -0
  36. package/lib/blocks/lens/fields/IdField.ts +34 -0
  37. package/lib/blocks/lens/fields/LinkField.ts +53 -0
  38. package/lib/blocks/lens/fields/NumberField.ts +32 -0
  39. package/lib/blocks/lens/fields/RelatedManyField.ts +62 -0
  40. package/lib/blocks/lens/fields/SelectField.ts +198 -0
  41. package/lib/blocks/lens/fields/SlackMessageField.ts +34 -0
  42. package/lib/blocks/lens/fields/TextField.ts +46 -0
  43. package/lib/blocks/lens/fields/TextareaField.ts +49 -0
  44. package/lib/blocks/lens/filter-inputs/FilterInput.ts +72 -0
  45. package/lib/blocks/lens/filter-inputs/NumberFilterInput.ts +26 -0
  46. package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +76 -0
  47. package/lib/blocks/lens/filter-inputs/TextFilterInput.ts +26 -0
  48. package/lib/blocks/lens/validation/RuleMapper.ts +22 -0
  49. package/lib/components/SInputTextarea.vue +28 -10
  50. package/lib/components/STable.vue +230 -61
  51. package/lib/components/STableCell.vue +2 -2
  52. package/lib/composables/TableAnimation.ts +180 -0
  53. package/lib/support/Scroll.ts +263 -0
  54. package/lib/support/Utils.ts +1 -1
  55. package/package.json +7 -15
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { type LensResult } from '../LensResult'
3
+
4
+ defineProps<{
5
+ result: LensResult
6
+ loading: boolean
7
+ }>()
8
+
9
+ defineEmits<{
10
+ prev: []
11
+ next: []
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <div class="LensCatalogFooter">
17
+ <SControl>
18
+ <SControlRight>
19
+ <SControlPagination
20
+ :total="result.pagination.total"
21
+ :page="result.pagination.page"
22
+ :per-page="result.pagination.perPage"
23
+ :disabled="loading"
24
+ @prev="$emit('prev')"
25
+ @next="$emit('next')"
26
+ />
27
+ </SControlRight>
28
+ </SControl>
29
+ </div>
30
+ </template>
31
+
32
+ <style scoped lang="postcss">
33
+ .LensCatalogFooter {
34
+ position: relative;
35
+ z-index: 10;
36
+ display: flex;
37
+ flex-shrink: 0;
38
+ align-items: center;
39
+ margin-top: -1px;
40
+ border-top: 1px solid var(--c-gutter);
41
+ border-radius: 0 0 5px 5px;
42
+ padding: var(--lens-catalog-footer-padding, 0 12px);
43
+ height: 57px;
44
+ background-color: var(--c-bg-1);
45
+ }
46
+ </style>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ import IconArrowsInLineVertical from '~icons/ph/arrows-in-line-vertical'
3
+ import IconArrowsOutLineVertical from '~icons/ph/arrows-out-line-vertical'
4
+ import IconX from '~icons/ph/x'
5
+ import { computed, ref } from 'vue'
6
+ import { useTrans } from '../../../composables/Lang'
7
+ import { type FieldData } from '../FieldData'
8
+ import LensCatalogStateFilterGroup, { type FilterGroup } from './LensCatalogStateFilterGroup.vue'
9
+
10
+ export interface Props {
11
+ fields: Record<string, FieldData>
12
+ filters: any[]
13
+ }
14
+
15
+ interface Count {
16
+ value: number
17
+ }
18
+
19
+ const props = defineProps<Props>()
20
+
21
+ defineEmits<{
22
+ reset: []
23
+ }>()
24
+
25
+ const { t } = useTrans({
26
+ en: {
27
+ filter_count: (count: number) => `${count} filter${count > 1 ? 's' : ''} applied`,
28
+ reset_filters: 'Reset filters'
29
+ },
30
+ ja: {
31
+ filter_count: (count: number) => `${count}件のフィルターが適用中`,
32
+ reset_filters: 'フィルターをリセット'
33
+ }
34
+ })
35
+
36
+ const isOpen = ref(true)
37
+
38
+ const group = computed(() => {
39
+ const count: Count = { value: 0 }
40
+ const group = lensFiltersToGroup(props.filters, '$and', count)
41
+ return {
42
+ count: count.value,
43
+ group
44
+ }
45
+ })
46
+
47
+ function lensFiltersToGroup(filters: any[], connector: '$and' | '$or', count: Count) {
48
+ const group = newFilterGroup()
49
+
50
+ group.connector = connector
51
+
52
+ for (const filter of filters) {
53
+ const [fieldOrConnector, operatorOrFilters, value] = filter
54
+
55
+ if (!isConnector(fieldOrConnector)) {
56
+ group.conditions.push({
57
+ field: fieldOrConnector,
58
+ operator: operatorOrFilters,
59
+ value
60
+ })
61
+ count.value++
62
+ continue
63
+ }
64
+
65
+ if (!Array.isArray(operatorOrFilters)) {
66
+ throw new TypeError(`Invalid filter format: ${JSON.stringify(filter)}`)
67
+ }
68
+
69
+ group.groups.push(
70
+ lensFiltersToGroup(operatorOrFilters, fieldOrConnector, count)
71
+ )
72
+ }
73
+
74
+ return group
75
+ }
76
+
77
+ function newFilterGroup(): FilterGroup {
78
+ return {
79
+ connector: '$and',
80
+ conditions: [],
81
+ groups: []
82
+ }
83
+ }
84
+
85
+ function isConnector(value: any): value is '$and' | '$or' {
86
+ return value === '$and' || value === '$or'
87
+ }
88
+ </script>
89
+
90
+ <template>
91
+ <SCardBlock class="LensCatalogStateFilter">
92
+ <div v-if="!isOpen" class="closed">
93
+ <span class="filter-count">{{ t.filter_count(group.count) }}</span>
94
+ </div>
95
+ <div v-else class="filters">
96
+ <LensCatalogStateFilterGroup
97
+ :fields
98
+ :group="group.group"
99
+ is-root
100
+ />
101
+ </div>
102
+ <div class="actions">
103
+ <button class="action" @click="$emit('reset')">
104
+ <span class="action-icon"><IconX class="action-svg" /></span>
105
+ <span class="action-text">{{ t.reset_filters }}</span>
106
+ </button>
107
+ <button class="action icon" @click="isOpen = !isOpen">
108
+ <span class="action-icon">
109
+ <IconArrowsInLineVertical v-if="isOpen" class="action-svg" />
110
+ <IconArrowsOutLineVertical v-else class="action-svg" />
111
+ </span>
112
+ </button>
113
+ </div>
114
+ </SCardBlock>
115
+ </template>
116
+
117
+ <style scoped lang="postcss">
118
+ .LensCatalogStateFilter {
119
+ display: flex;
120
+ gap: 16px;
121
+ padding: var(--lens-catalog-filters-block-padding, 12px);
122
+ }
123
+
124
+ .closed {
125
+ flex-grow: 1;
126
+ padding-left: 4px;
127
+ line-height: 24px;
128
+ font-size: 12px;
129
+ color: var(--c-text-2);
130
+ }
131
+
132
+ .filters {
133
+ flex-grow: 1;
134
+ }
135
+
136
+ .actions {
137
+ display: flex;
138
+ gap: 8px;
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ .action {
143
+ display: flex;
144
+ justify-content: center;
145
+ align-items: center;
146
+ gap: 4px;
147
+ border: 1px dashed var(--c-divider);
148
+ border-radius: 6px;
149
+ padding: 0 6px;
150
+ height: 24px;
151
+ font-size: 12px;
152
+ color: var(--c-text-2);
153
+ background-color: var(--c-bg-1);
154
+ cursor: pointer;
155
+ transition: background-color 0.25s;
156
+
157
+ &.icon {
158
+ padding: 0;
159
+ width: 24px;
160
+ }
161
+
162
+ &:hover {
163
+ background-color: var(--c-bg-2);
164
+ }
165
+ }
166
+
167
+ .action-svg {
168
+ width: 14px;
169
+ height: 14px;
170
+ }
171
+ </style>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ import { computedAsync } from '@vueuse/core'
3
+ import { computed } from 'vue'
4
+ import { type FieldData } from '../FieldData'
5
+ import { type FilterOperator, FilterOperatorTextDict } from '../FilterOperator'
6
+ import { useFieldFactory } from '../composables/FieldFactory'
7
+
8
+ export interface Props {
9
+ fields: Record<string, FieldData>
10
+ condition: FilterCondition
11
+ }
12
+
13
+ export interface FilterCondition {
14
+ field: string
15
+ operator: FilterOperator
16
+ value: any
17
+ }
18
+
19
+ const props = defineProps<Props>()
20
+
21
+ const fieldFactory = useFieldFactory()
22
+
23
+ const field = computed(() => {
24
+ return fieldFactory.make(props.fields[props.condition.field])
25
+ })
26
+
27
+ const input = computed(() => {
28
+ return field.value.filterInputByOperator(props.condition.operator)
29
+ })
30
+
31
+ const fieldText = computed(() => {
32
+ return field.value.label()
33
+ })
34
+
35
+ const operatorText = computed(() => {
36
+ return FilterOperatorTextDict[props.condition.operator]
37
+ })
38
+
39
+ const valueText = computedAsync(async () => {
40
+ return input.value.valueToText(props.condition.value)
41
+ }, '...')
42
+ </script>
43
+
44
+ <template>
45
+ <div class="LensCatalogStateFilterCondition">
46
+ <div class="field">{{ fieldText }}</div>
47
+ <div class="operator">{{ operatorText }}</div>
48
+ <div v-if="input === null" class="value">
49
+ ...
50
+ </div>
51
+ <div v-else class="value">
52
+ {{ valueText }}
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <style scoped lang="postcss">
58
+ .LensCatalogStateFilterCondition {
59
+ display: flex;
60
+ gap: 1px;
61
+ border: 1px dashed var(--c-divider);
62
+ border-radius: 6px;
63
+ overflow: hidden;
64
+ background-color: var(--c-gutter);
65
+ }
66
+
67
+ .field,
68
+ .operator,
69
+ .value {
70
+ padding: 0 8px;
71
+ line-height: 22px;
72
+ font-size: 12px;
73
+ background-color: var(--c-bg-1);
74
+ }
75
+
76
+ .operator {
77
+ color: var(--c-text-2);
78
+ }
79
+
80
+ .value {
81
+ max-width: 192px;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
86
+ </style>
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ import { type FieldData } from '../FieldData'
3
+ import LensCatalogStateFilterCondition, { type FilterCondition } from './LensCatalogStateFilterCondition.vue'
4
+
5
+ export interface Props {
6
+ fields: Record<string, FieldData>
7
+ isRoot: boolean
8
+ group: FilterGroup
9
+ }
10
+
11
+ export interface FilterGroup {
12
+ connector: '$and' | '$or'
13
+ conditions: FilterCondition[]
14
+ groups: FilterGroup[]
15
+ }
16
+
17
+ defineProps<Props>()
18
+
19
+ const connectorTextDict = {
20
+ $and: 'AND',
21
+ $or: 'OR'
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="LensCatalogStateFilterGroup" :class="{ 'is-root': isRoot }">
27
+ <div v-if="!isRoot" class="connector">
28
+ <div class="connector-text">{{ connectorTextDict[group.connector] }}</div>
29
+ </div>
30
+ <div class="body">
31
+ <div v-if="group.conditions.length" class="conditions">
32
+ <LensCatalogStateFilterCondition
33
+ v-for="condition, index in group.conditions"
34
+ :key="index"
35
+ :fields
36
+ :condition
37
+ />
38
+ </div>
39
+ <div v-if="group.groups.length" class="groups">
40
+ <LensCatalogStateFilterGroup
41
+ v-for="g, i in group.groups"
42
+ :key="i"
43
+ :fields
44
+ :is-root="false"
45
+ :group="g"
46
+ />
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <style scoped lang="postcss">
53
+ .LensCatalogStateFilterGroup {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 8px;
57
+ border: 1px dashed var(--c-divider);
58
+ border-radius: 6px;
59
+ padding: 8px;
60
+
61
+ &.is-root {
62
+ border: 0;
63
+ padding: 0;
64
+ }
65
+ }
66
+
67
+ .connector {
68
+ display: flex;
69
+ }
70
+
71
+ .connector-text {
72
+ display: flex;
73
+ justify-content: center;
74
+ align-items: center;
75
+ flex-shrink: 0;
76
+ border: 1px dashed var(--c-divider);
77
+ border-radius: 6px;
78
+ padding: 0 8px;
79
+ height: 24px;
80
+ font-size: 12px;
81
+ color: var(--c-text-2);
82
+ }
83
+
84
+ .body {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 8px;
88
+ flex-grow: 1;
89
+ }
90
+
91
+ .conditions {
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ gap: 8px;
95
+ }
96
+
97
+ .groups {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 8px;
101
+ }
102
+ </style>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ import IconArrowsInLineVertical from '~icons/ph/arrows-in-line-vertical'
3
+ import IconArrowsOutLineVertical from '~icons/ph/arrows-out-line-vertical'
4
+ import IconX from '~icons/ph/x'
5
+ import { ref } from 'vue'
6
+ import { useLang, useTrans } from '../../../composables/Lang'
7
+ import { type FieldData } from '../FieldData'
8
+ import { type LensQuerySort } from '../LensQuery'
9
+
10
+ export interface Props {
11
+ fields: Record<string, FieldData>
12
+ sorts: LensQuerySort[]
13
+ }
14
+
15
+ const props = defineProps<Props>()
16
+
17
+ defineEmits<{
18
+ reset: []
19
+ }>()
20
+
21
+ const lang = useLang()
22
+
23
+ const { t } = useTrans({
24
+ en: {
25
+ sort_count: (count: number) => `${count} sort${count > 1 ? 's' : ''} applied`,
26
+ reset_sorts: 'Reset sorts'
27
+ },
28
+ ja: {
29
+ sort_count: (count: number) => `${count}件のソートが適用中`,
30
+ reset_sorts: 'ソートをリセット'
31
+ }
32
+ })
33
+
34
+ const isOpen = ref(true)
35
+
36
+ const orderTextDict = {
37
+ asc: 'ASC',
38
+ desc: 'DESC'
39
+ }
40
+
41
+ function getFieldName(sort: LensQuerySort): string {
42
+ return lang === 'ja'
43
+ ? props.fields[sort[0]].labelJa
44
+ : props.fields[sort[0]].labelEn
45
+ }
46
+
47
+ function getOrderText(sort: LensQuerySort): string {
48
+ return orderTextDict[sort[1]]
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <SCardBlock class="LensCatalogStateSort">
54
+ <div v-if="!isOpen" class="closed">
55
+ <span class="sort-count">{{ t.sort_count(sorts.length) }}</span>
56
+ </div>
57
+ <div v-else class="sorts">
58
+ <div v-for="sort in sorts" :key="sort[0]" class="sort">
59
+ <div class="field">{{ getFieldName(sort) }}</div>
60
+ <div class="order">{{ getOrderText(sort) }}</div>
61
+ </div>
62
+ </div>
63
+ <div class="actions">
64
+ <button class="action" @click="$emit('reset')">
65
+ <span class="action-icon"><IconX class="action-svg" /></span>
66
+ <span class="action-text">{{ t.reset_sorts }}</span>
67
+ </button>
68
+ <button class="action icon" @click="isOpen = !isOpen">
69
+ <span class="action-icon">
70
+ <IconArrowsInLineVertical v-if="isOpen" class="action-svg" />
71
+ <IconArrowsOutLineVertical v-else class="action-svg" />
72
+ </span>
73
+ </button>
74
+ </div>
75
+ </SCardBlock>
76
+ </template>
77
+
78
+ <style scoped lang="postcss">
79
+ .LensCatalogStateSort {
80
+ display: flex;
81
+ gap: 16px;
82
+ padding: var(--lens-catalog-sorts-block-padding, 12px);
83
+ }
84
+
85
+ .closed {
86
+ flex-grow: 1;
87
+ padding-left: 4px;
88
+ line-height: 24px;
89
+ font-size: 12px;
90
+ color: var(--c-text-2);
91
+ }
92
+
93
+ .sorts {
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ gap: 8px;
97
+ flex-grow: 1;
98
+ }
99
+
100
+ .sort {
101
+ display: flex;
102
+ gap: 1px;
103
+ border: 1px dashed var(--c-divider);
104
+ border-radius: 6px;
105
+ background-color: var(--c-gutter);
106
+ overflow: hidden;
107
+ }
108
+
109
+ .field,
110
+ .order {
111
+ display: flex;
112
+ justify-content: center;
113
+ align-items: center;
114
+ padding: 0 8px;
115
+ font-size: 12px;
116
+ height: 22px;
117
+ background-color: var(--c-bg-1);
118
+ }
119
+
120
+ .order {
121
+ color: var(--c-text-2);
122
+ }
123
+
124
+ .actions {
125
+ display: flex;
126
+ gap: 8px;
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .action {
131
+ display: flex;
132
+ justify-content: center;
133
+ align-items: center;
134
+ gap: 4px;
135
+ border: 1px dashed var(--c-divider);
136
+ border-radius: 6px;
137
+ padding: 0 6px;
138
+ height: 24px;
139
+ font-size: 12px;
140
+ color: var(--c-text-2);
141
+ background-color: var(--c-bg-1);
142
+ cursor: pointer;
143
+ transition: background-color 0.25s;
144
+
145
+ &.icon {
146
+ padding: 0;
147
+ width: 24px;
148
+ }
149
+
150
+ &:hover {
151
+ background-color: var(--c-bg-2);
152
+ }
153
+ }
154
+
155
+ .action-svg {
156
+ width: 14px;
157
+ height: 14px;
158
+ }
159
+ </style>