Skip to content

Commit b0f14cc

Browse files
authored
feat(OASchemaUI): add expand all / collapse all buttons for object/array properties (#220)
* chore(shadcn/components): update aliases and add iconLibrary configuration * chore(ui): add Tooltip components * feat(OASchemaUI): add expand all / collapse all buttons object/array properties * chore: format * chore(OASchemaUI): remove deep watch from childrenExpandState * chore(OASchemaUI): add aria-labels for expand/collapse buttons * chore(e2e): update ss
1 parent 464801e commit b0f14cc

File tree

14 files changed

+180
-36
lines changed

14 files changed

+180
-36
lines changed

components.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
"$schema": "https://shadcn-vue.com/schema.json",
33
"style": "default",
44
"typescript": true,
5-
"tsConfigPath": "./tsconfig.json",
65
"tailwind": {
76
"config": "tailwind.config.js",
87
"css": "src/style.css",
98
"baseColor": "slate",
109
"cssVariables": true
1110
},
12-
"framework": "vite",
1311
"aliases": {
1412
"components": "src/components",
15-
"utils": "lib/utils"
16-
}
17-
}
13+
"composables": "src/composables",
14+
"utils": "lib/utils",
15+
"ui": "src/components/ui",
16+
"lib": "src/lib"
17+
},
18+
"iconLibrary": "lucide"
19+
}
116 KB
Loading
187 KB
Loading

src/components/Schema/OASchemaUI.vue

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup>
2-
import { ref } from 'vue'
2+
import { ChevronDown, ChevronRight, Maximize2, Minimize2 } from 'lucide-vue-next'
3+
import { computed, ref, watch } from 'vue'
34
import OAMarkdown from '../Common/OAMarkdown.vue'
45
import { Badge } from '../ui/badge'
6+
import { Button } from '../ui/button/index'
57
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'
8+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip/index'
69
import OASchemaPropertyAttributes from './OASchemaPropertyAttributes.vue'
710
811
const props = defineProps({
@@ -26,13 +29,38 @@ const props = defineProps({
2629
type: Boolean,
2730
default: undefined,
2831
},
32+
expandAll: {
33+
type: Boolean,
34+
default: undefined,
35+
},
2936
})
3037
3138
const isOpen = ref(props.open !== undefined ? props.open : props.deep > 0 && props.level <= 10)
3239
40+
const childrenExpandState = ref(undefined)
41+
42+
watch(() => props.expandAll, (newValue) => {
43+
if (newValue !== undefined) {
44+
isOpen.value = newValue
45+
childrenExpandState.value = newValue
46+
}
47+
}, { immediate: true })
48+
49+
const toggleAllChildren = (expand) => {
50+
childrenExpandState.value = expand
51+
isOpen.value = expand
52+
}
53+
3354
const isObject = props.property.types?.includes('object')
3455
const isArray = props.property.types?.includes('array')
3556
const isObjectOrArray = isObject || isArray || props.property.type === 'object' || props.property.type === 'array'
57+
58+
const hasExpandableProperties = computed(() => {
59+
return isObjectOrArray
60+
&& props.property.properties
61+
&& props.property.properties.length > 0
62+
&& props.property.properties.some(p => (p.types?.includes('object') || p.types?.includes('array') || p.type === 'object' || p.type === 'array') && p.properties)
63+
})
3664
</script>
3765
3866
<template>
@@ -51,30 +79,26 @@ const isObjectOrArray = isObject || isArray || props.property.type === 'object'
5179
{{ props.property.name }}
5280
</span>
5381
54-
<div
55-
v-if="isObjectOrArray && props.property.properties"
56-
class="flex-shrink-0 w-4 h-4 cursor-pointer"
57-
>
58-
<svg
59-
v-if="!isOpen"
60-
xmlns="http://www.w3.org/2000/svg"
61-
width="100%"
62-
height="100%"
63-
viewBox="0 0 24 24"
64-
><path
65-
fill="currentColor"
66-
d="M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6z"
67-
/></svg>
68-
<svg
69-
v-if="isOpen"
70-
xmlns="http://www.w3.org/2000/svg"
71-
width="100%"
72-
height="100%"
73-
viewBox="0 0 24 24"
74-
><path
75-
fill="currentColor"
76-
d="M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z"
77-
/></svg>
82+
<div class="flex items-center">
83+
<TooltipProvider>
84+
<Tooltip delay-duration="200">
85+
<TooltipTrigger as-child>
86+
<Button
87+
v-if="isObjectOrArray && props.property.properties"
88+
size="icon"
89+
variant="icon"
90+
:aria-label="isOpen ? $t('Collapse') : $t('Expand')"
91+
class="flex-shrink-0 w-4 h-4 cursor-pointer"
92+
>
93+
<ChevronDown v-if="isOpen" />
94+
<ChevronRight v-else />
95+
</Button>
96+
</TooltipTrigger>
97+
<TooltipContent>
98+
<p>{{ isOpen ? $t('Collapse') : $t('Expand') }}</p>
99+
</TooltipContent>
100+
</Tooltip>
101+
</TooltipProvider>
78102
</div>
79103
80104
<div class="flex flex-row items-center gap-1 text-muted-foreground">
@@ -102,6 +126,30 @@ const isObjectOrArray = isObject || isArray || props.property.type === 'object'
102126
</template>
103127
</div>
104128
129+
<div
130+
v-if="hasExpandableProperties"
131+
class="flex items-center"
132+
>
133+
<TooltipProvider>
134+
<Tooltip delay-duration="200">
135+
<TooltipTrigger as-child>
136+
<Button
137+
size="icon"
138+
variant="icon"
139+
:aria-label="isOpen ? $t('Collapse all') : $t('Expand all')"
140+
@click.stop.prevent="toggleAllChildren(!isOpen)"
141+
>
142+
<Minimize2 v-if="isOpen" />
143+
<Maximize2 v-else />
144+
</Button>
145+
</TooltipTrigger>
146+
<TooltipContent>
147+
<p>{{ isOpen ? $t('Collapse all') : $t('Expand all') }}</p>
148+
</TooltipContent>
149+
</Tooltip>
150+
</TooltipProvider>
151+
</div>
152+
105153
<div class="flex-grow mx-2">
106154
<div
107155
v-if="props.property.required === true"
@@ -144,7 +192,8 @@ const isObjectOrArray = isObject || isArray || props.property.type === 'object'
144192
:schema="props.schema"
145193
:deep="props.deep - 1"
146194
:level="props.level + 1"
147-
:open="subProperty?.meta?.isOneOf === true"
195+
:open="childrenExpandState !== undefined ? childrenExpandState : subProperty?.meta?.isOneOf === true"
196+
:expand-all="childrenExpandState"
148197
/>
149198
</div>
150199
</CollapsibleContent>

src/components/ui/button/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ export const buttonVariants = cva(
1818
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
1919
ghost: 'hover:bg-accent hover:text-accent-foreground',
2020
link: 'text-primary underline-offset-4 hover:underline',
21+
icon: 'rounded bg-transparent hover:bg-muted',
2122
},
2223
size: {
2324
default: 'h-10 px-4 py-2',
2425
xs: 'h-7 rounded px-2',
2526
sm: 'h-9 rounded-md px-3',
2627
lg: 'h-11 rounded-md px-8',
27-
icon: 'h-10 w-10',
28+
icon: 'size-4 p-[2px]',
2829
},
2930
},
3031
defaultVariants: {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
3+
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
4+
5+
const props = defineProps<TooltipRootProps>()
6+
const emits = defineEmits<TooltipRootEmits>()
7+
8+
const forwarded = useForwardPropsEmits(props, emits)
9+
</script>
10+
11+
<template>
12+
<TooltipRoot v-bind="forwarded">
13+
<slot />
14+
</TooltipRoot>
15+
</template>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script setup lang="ts">
2+
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
3+
import type { HTMLAttributes } from 'vue'
4+
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
5+
import { computed } from 'vue'
6+
import { cn } from '../../../lib/utils'
7+
8+
defineOptions({
9+
inheritAttrs: false,
10+
})
11+
12+
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
13+
sideOffset: 4,
14+
})
15+
16+
const emits = defineEmits<TooltipContentEmits>()
17+
18+
const delegatedProps = computed(() => {
19+
const { class: _, ...delegated } = props
20+
21+
return delegated
22+
})
23+
24+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
25+
</script>
26+
27+
<template>
28+
<TooltipPortal>
29+
<TooltipContent v-bind="{ ...forwarded, ...$attrs }" :class="cn('z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)">
30+
<slot />
31+
</TooltipContent>
32+
</TooltipPortal>
33+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import type { TooltipProviderProps } from 'reka-ui'
3+
import { TooltipProvider } from 'reka-ui'
4+
5+
const props = defineProps<TooltipProviderProps>()
6+
</script>
7+
8+
<template>
9+
<TooltipProvider v-bind="props">
10+
<slot />
11+
</TooltipProvider>
12+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import type { TooltipTriggerProps } from 'reka-ui'
3+
import { TooltipTrigger } from 'reka-ui'
4+
5+
const props = defineProps<TooltipTriggerProps>()
6+
</script>
7+
8+
<template>
9+
<TooltipTrigger v-bind="props">
10+
<slot />
11+
</TooltipTrigger>
12+
</template>

src/components/ui/tooltip/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { default as Tooltip } from './Tooltip.vue'
2+
export { default as TooltipContent } from './TooltipContent.vue'
3+
export { default as TooltipProvider } from './TooltipProvider.vue'
4+
export { default as TooltipTrigger } from './TooltipTrigger.vue'

0 commit comments

Comments
 (0)