Skip to content

Commit 7c9d35d

Browse files
[6.x] Asset Browser: Show actions in context menu (#11791)
Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent 618250f commit 7c9d35d

9 files changed

Lines changed: 311 additions & 69 deletions

File tree

resources/js/components/assets/Browser/Grid.vue

Lines changed: 87 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,37 @@
44
<breadcrumbs v-if="!restrictFolderNavigation" :path="path" @navigated="selectFolder" />
55
<ui-slider size="sm" class="mr-2 w-24!" variant="subtle" v-model="thumbnailSize" :min="60" :max="300" :step="25" />
66
</ui-panel-header>
7-
<!-- Folders -->
7+
88
<ui-card class="space-y-8">
9+
<!-- Folders -->
910
<section class="folder-grid-listing" v-if="folders.length">
1011
<div
1112
class="group/folder relative"
1213
v-for="folder in folders"
1314
:key="folder.path"
1415
v-if="!restrictFolderNavigation"
1516
>
16-
<button @dblclick="selectFolder(folder.path)" class="w-[80px] h-[66px] group">
17-
<ui-icon name="asset-folder" class="size-full text-blue-400/90 hover:text-blue-400" />
18-
<div
19-
class="font-mono text-xs text-gray-500 text-center overflow-hidden text-ellipsis whitespace-nowrap"
20-
v-text="folder.basename"
21-
:title="folder.basename"
22-
/>
23-
</button>
24-
<dropdown-list
25-
v-if="folderActions(folder).length"
26-
class="absolute top-1 opacity-0 group-hover:opacity-100 end-2"
27-
:class="{ 'opacity-100': actionOpened === folder.path }"
28-
@opened="actionOpened = folder.path"
29-
@closed="actionOpened = null"
30-
>
31-
<data-list-inline-actions
32-
:item="folder.path"
33-
:url="folderActionUrl"
34-
:actions="folderActions(folder)"
35-
@started="actionStarted"
36-
@completed="actionCompleted"
37-
/>
38-
</dropdown-list>
17+
<Context>
18+
<template #trigger>
19+
<button @dblclick="selectFolder(folder.path)" class="group h-[66px] w-[80px]">
20+
<ui-icon name="asset-folder" class="size-full text-blue-400/90 hover:text-blue-400" />
21+
<div
22+
class="overflow-hidden text-center font-mono text-xs text-ellipsis whitespace-nowrap text-gray-500"
23+
v-text="folder.basename"
24+
:title="folder.basename"
25+
/>
26+
</button>
27+
</template>
28+
<ContextMenu v-if="folderActions(folder).length">
29+
<data-list-inline-actions
30+
:item="folder.path"
31+
:url="folderActionUrl"
32+
:actions="folderActions(folder)"
33+
@started="actionStarted"
34+
@completed="actionCompleted"
35+
/>
36+
</ContextMenu>
37+
</Context>
3938
</div>
4039
<div v-if="creatingFolder" class="group/folder relative">
4140
<div class="group h-[66px] w-[80px]">
@@ -59,62 +58,70 @@
5958
</section>
6059

6160
<!-- Assets -->
62-
<section class="asset-grid-listing"
63-
v-if="assets.length"
64-
:style="{ gridTemplateColumns: gridSize }"
65-
>
61+
<section class="asset-grid-listing" v-if="assets.length" :style="{ gridTemplateColumns: gridSize }">
6662
<div
6763
v-for="(asset, index) in assets"
6864
:key="asset.id"
6965
class="group relative"
70-
:class="{ 'selected': isSelected(asset.id) }"
66+
:class="{ selected: isSelected(asset.id) }"
7167
>
72-
<div class="asset-tile group relative" :class="{ 'bg-checkerboard': asset.can_be_transparent }">
73-
<button
74-
class="size-full"
75-
@click.stop="toggleSelection(asset.id, index, $event)"
76-
@dblclick.stop="$emit('edit-asset', asset)"
77-
>
78-
<div class="relative flex items-center justify-center aspect-square size-full">
79-
<div class="asset-thumb">
80-
<img
81-
v-if="asset.is_image"
82-
:src="asset.thumbnail"
83-
loading="lazy"
84-
:class="{
85-
'size-full p-4': asset.extension === 'svg',
86-
'p-1 rounded-lg': asset.orientation === 'square'
87-
}"
68+
<Context>
69+
<template #trigger>
70+
<div class="asset-tile group relative" :class="{ 'bg-checkerboard': asset.can_be_transparent }">
71+
<button
72+
class="size-full"
73+
@click.stop="toggleSelection(asset.id, index, $event)"
74+
@dblclick.stop="$emit('edit-asset', asset)"
75+
>
76+
<div class="relative flex aspect-square size-full items-center justify-center">
77+
<div class="asset-thumb">
78+
<img
79+
v-if="asset.is_image"
80+
:src="asset.thumbnail"
81+
loading="lazy"
82+
:class="{
83+
'size-full p-4': asset.extension === 'svg',
84+
'rounded-lg p-1': asset.orientation === 'square',
85+
}"
86+
/>
87+
<file-icon v-else :extension="asset.extension" class="size-1/2" />
88+
</div>
89+
</div>
90+
</button>
91+
<dropdown-list
92+
class="absolute top-1 opacity-0 group-hover:opacity-100 end-2"
93+
:class="{ 'opacity-100': actionOpened === asset.id }"
94+
@opened="actionOpened = asset.id"
95+
@closed="actionOpened = null"
96+
>
97+
<dropdown-item
98+
:text="__(canEdit ? 'Edit' : 'View')"
99+
@click="edit(asset.id)"
100+
/>
101+
<div class="divider" v-if="asset.actions.length" />
102+
<data-list-inline-actions
103+
:item="asset.id"
104+
:url="actionUrl"
105+
:actions="asset.actions"
106+
@started="actionStarted"
107+
@completed="actionCompleted"
88108
/>
89-
<file-icon v-else :extension="asset.extension" class="size-1/2" />
90-
</div>
109+
</dropdown-list>
91110
</div>
92-
</button>
93-
<dropdown-list
94-
class="absolute top-1 opacity-0 group-hover:opacity-100 end-2"
95-
:class="{ 'opacity-100': actionOpened === asset.id }"
96-
@opened="actionOpened = asset.id"
97-
@closed="actionOpened = null"
98-
>
99-
<dropdown-item
100-
:text="__(canEdit ? 'Edit' : 'View')"
101-
@click="edit(asset.id)"
102-
/>
103-
<div class="divider" v-if="asset.actions.length" />
111+
</template>
112+
<ContextMenu>
113+
<ContextItem icon="edit" :text="__(canEdit ? 'Edit' : 'View')" @click="edit(asset.id)" />
114+
<ContextSeparator />
104115
<data-list-inline-actions
105116
:item="asset.id"
106117
:url="actionUrl"
107118
:actions="asset.actions"
108119
@started="actionStarted"
109120
@completed="actionCompleted"
110121
/>
111-
</dropdown-list>
112-
</div>
113-
<div
114-
class="asset-filename"
115-
v-text="truncateFilename(asset.basename)"
116-
:title="asset.basename"
117-
/>
122+
</ContextMenu>
123+
</Context>
124+
<div class="asset-filename" v-text="truncateFilename(asset.basename)" :title="asset.basename" />
118125
</div>
119126
</section>
120127
</ui-card>
@@ -129,11 +136,23 @@ import AssetBrowserMixin from './AssetBrowserMixin';
129136
import Breadcrumbs from './Breadcrumbs.vue';
130137
import { debounce } from 'lodash-es';
131138
import { EditableArea, EditableInput, EditablePreview, EditableRoot } from 'reka-ui';
132-
import { Editable } from '@statamic/ui';
139+
import { Context, ContextMenu, ContextItem, ContextLabel, ContextSeparator, Editable } from '@statamic/ui';
133140
134141
export default {
135142
mixins: [AssetBrowserMixin],
136-
components: { Editable, EditableInput, EditablePreview, EditableArea, EditableRoot, Breadcrumbs },
143+
components: {
144+
ContextItem,
145+
ContextLabel,
146+
ContextMenu,
147+
ContextSeparator,
148+
Context,
149+
Editable,
150+
EditableInput,
151+
EditablePreview,
152+
EditableArea,
153+
EditableRoot,
154+
Breadcrumbs,
155+
},
137156
props: {
138157
assets: { type: Array },
139158
selectedAssets: { type: Array },
@@ -142,7 +161,6 @@ export default {
142161
143162
data() {
144163
return {
145-
actionOpened: null,
146164
thumbnailSize: 200,
147165
newFolderName: null,
148166
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script setup>
2+
import { useAttrs } from 'vue';
3+
import { cva } from 'cva';
4+
import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from 'reka-ui';
5+
import { Button } from '@statamic/ui';
6+
7+
defineOptions({
8+
inheritAttrs: false,
9+
});
10+
11+
const attrs = useAttrs();
12+
13+
const props = defineProps({
14+
align: { type: String, default: 'start' },
15+
offset: { type: Number, default: 5 },
16+
side: { type: String, default: 'bottom' },
17+
});
18+
19+
const contextContentClasses = cva({
20+
base: [
21+
'rounded-xl w-64 bg-gray-50 dark:bg-gray-800 outline-hidden overflow-hidden group z-50',
22+
'border border-gray-200 dark:border-black shadow-lg popoverAnimation',
23+
],
24+
})({});
25+
</script>
26+
27+
<template>
28+
<ContextMenuRoot>
29+
<ContextMenuTrigger data-ui-context-trigger>
30+
<slot name="trigger">
31+
<Button icon="ui/dots" variant="ghost" size="sm" v-bind="attrs" />
32+
</slot>
33+
</ContextMenuTrigger>
34+
<ContextMenuPortal>
35+
<ContextMenuContent
36+
data-ui-context-content
37+
:class="[contextContentClasses, $attrs.class]"
38+
:align
39+
:sideOffset="offset"
40+
:side
41+
>
42+
<slot />
43+
</ContextMenuContent>
44+
</ContextMenuPortal>
45+
</ContextMenuRoot>
46+
</template>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script setup>
2+
import { useSlots } from 'vue';
3+
import { cva } from 'cva';
4+
import { Icon } from '@statamic/ui';
5+
6+
defineProps({
7+
icon: { type: String, default: null },
8+
text: { type: String, default: null },
9+
});
10+
11+
const slots = useSlots();
12+
const usingSlot = !!slots.default;
13+
14+
const footerClasses = cva({
15+
base: 'text-gray-600 antialiased py-2 px-3 text-sm rounded-b-xl group/footer',
16+
variant: {
17+
noSlot: 'flex items-center gap-2',
18+
},
19+
});
20+
</script>
21+
22+
<template>
23+
<footer :class="footerClasses({ noSlot: !usingSlot })" data-ui-context-footer>
24+
<slot v-if="usingSlot" />
25+
<div v-else class="flex items-center gap-2">
26+
<div
27+
v-if="icon"
28+
class="flex size-6 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-700 dark:bg-gray-900 dark:text-gray-500"
29+
>
30+
<Icon :name="icon" />
31+
</div>
32+
<div
33+
class="grow truncate text-sm text-gray-600 antialiased group-hover/footer:text-gray-950 dark:text-gray-400 dark:group-hover/footer:text-gray-200"
34+
>
35+
{{ text }}
36+
</div>
37+
</div>
38+
</footer>
39+
</template>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script setup>
2+
import { useSlots } from 'vue';
3+
import { cva } from 'cva';
4+
import { Icon, Button } from '@statamic/ui';
5+
6+
defineProps({
7+
icon: { type: String, default: null },
8+
linkToConfig: { type: Boolean, default: false },
9+
appendIcon: { type: String, default: null },
10+
appendHref: { type: String, default: null },
11+
text: { type: String, default: null },
12+
});
13+
14+
const slots = useSlots();
15+
const usingSlot = !!slots.default;
16+
17+
const headerClasses = cva({
18+
base: 'col-span-2 px-3.5 py-3 bg-white dark:bg-gray-900 font-medium border-b border-gray-200 dark:border-black text-sm text-gray-800 dark:text-gray-300',
19+
variants: {
20+
usingSlot: {
21+
true: 'grid grid-cols-[auto_1fr_auto]',
22+
},
23+
},
24+
});
25+
</script>
26+
27+
<template>
28+
<header :class="headerClasses({ usingSlot: usingSlot })" data-ui-dropdown-header>
29+
<div
30+
v-if="icon"
31+
class="-ms-1 me-2 flex size-6 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-700 dark:bg-gray-800 dark:text-gray-400"
32+
>
33+
<Icon :name="icon" />
34+
</div>
35+
<div class="col-start-2 grow truncate">
36+
<slot v-if="usingSlot" />
37+
<template v-else>{{ text }}</template>
38+
</div>
39+
<Button
40+
v-if="appendIcon"
41+
:href="appendHref"
42+
:icon="appendIcon"
43+
class="[&_svg]:text-gray-700 dark:[&_svg]:text-gray-400"
44+
size="xs"
45+
/>
46+
</header>
47+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup>
2+
import { ContextMenuItem } from 'reka-ui';
3+
import { useSlots } from 'vue';
4+
import { Icon } from '@statamic/ui';
5+
6+
defineProps({
7+
href: { type: String, default: null },
8+
icon: { type: String, default: null },
9+
text: { type: String, default: null },
10+
});
11+
12+
const slots = useSlots();
13+
const hasDefaultSlot = !!slots.default;
14+
</script>
15+
16+
<template>
17+
<ContextMenuItem
18+
:class="[
19+
'col-span-2 grid grid-cols-subgrid items-center',
20+
'rounded-lg px-1 py-1.5 text-sm antialiased',
21+
'text-gray-700 dark:text-gray-300',
22+
'not-data-disabled:cursor-pointer data-disabled:opacity-50',
23+
'hover:not-data-disabled:bg-gray-50 dark:hover:not-data-disabled:bg-gray-950',
24+
'outline-hidden focus-visible:bg-gray-100 dark:focus-visible:bg-gray-950',
25+
]"
26+
data-ui-context-item
27+
:as="href ? 'a' : 'div'"
28+
:href="href"
29+
>
30+
<div v-if="icon" class="flex size-6 items-center justify-center p-1 text-gray-500">
31+
<Icon :name="icon" class="size-3.5!" />
32+
</div>
33+
<div class="col-start-2 ps-2">
34+
<slot v-if="hasDefaultSlot" />
35+
<template v-else>{{ text }}</template>
36+
</div>
37+
</ContextMenuItem>
38+
</template>

0 commit comments

Comments
 (0)