@awes-io/ui 2.142.0 → 2.143.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.
- package/CHANGELOG.md +58 -0
- package/assets/css/components/_index.css +7 -1
- package/assets/css/components/animation.css +38 -32
- package/assets/css/components/content-placeholder.css +103 -0
- package/assets/css/components/empty-container.css +69 -1
- package/assets/css/components/filter-chosen.css +6 -0
- package/assets/css/components/filter-date-range.css +17 -1
- package/assets/css/components/filter-month.css +23 -17
- package/assets/css/components/filter-select.css +11 -0
- package/assets/css/components/layout.css +1 -32
- package/assets/css/components/modal.css +1 -1
- package/assets/css/components/number.css +12 -0
- package/assets/css/components/page-aside.css +54 -0
- package/assets/js/css.js +1 -1
- package/assets/js/icons/mono.js +59 -91
- package/assets/js/icons/multicolor.js +1 -31
- package/components/1_atoms/AwContentPlaceholder.vue +60 -0
- package/components/1_atoms/AwFlow.vue +21 -48
- package/components/1_atoms/AwLabel.vue +1 -1
- package/components/2_molecules/AwButton.vue +1 -1
- package/components/2_molecules/AwEmptyContainer.vue +74 -72
- package/components/2_molecules/AwNumber.vue +180 -0
- package/components/2_molecules/AwSelect.vue +11 -4
- package/components/3_organisms/AwFilterChosen.vue +73 -0
- package/components/3_organisms/AwFilterDateRange.vue +177 -0
- package/components/3_organisms/AwFilterMonth.vue +37 -40
- package/components/3_organisms/AwFilterSelect.vue +368 -0
- package/components/3_organisms/AwImageUpload.vue +1 -1
- package/components/3_organisms/AwMarkdownEditor.vue +0 -0
- package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
- package/components/3_organisms/AwTable/AwTableBuilder.vue +12 -60
- package/components/4_pages/AwPageAside.vue +108 -0
- package/components/5_layouts/AwLayoutCenter.vue +3 -8
- package/components/5_layouts/_AwUserMenu.vue +1 -1
- package/dist/css/aw-icons.css +26 -0
- package/dist/fonts/aw-icons.svg +18 -0
- package/dist/fonts/aw-icons.ttf +0 -0
- package/dist/fonts/aw-icons.woff +0 -0
- package/dist/fonts/aw-icons.woff2 +0 -0
- package/docs/_template.md +80 -0
- package/docs/components/atoms/aw-accordion-fold.md +91 -0
- package/docs/components/atoms/aw-action-card-body.md +67 -0
- package/docs/components/atoms/aw-action-card.md +94 -0
- package/docs/components/atoms/aw-action-icon.md +88 -0
- package/docs/components/atoms/aw-avatar.md +106 -0
- package/docs/components/atoms/aw-card.md +112 -0
- package/docs/components/atoms/aw-checkbox.md +112 -0
- package/docs/components/atoms/aw-content-placeholder.md +116 -0
- package/docs/components/atoms/aw-description.md +83 -0
- package/docs/components/atoms/aw-dock.md +84 -0
- package/docs/components/atoms/aw-dropdown-button.md +94 -0
- package/docs/components/atoms/aw-dropdown.md +128 -0
- package/docs/components/atoms/aw-file.md +73 -0
- package/docs/components/atoms/aw-flow.md +92 -0
- package/docs/components/atoms/aw-grid.md +91 -0
- package/docs/components/atoms/aw-headline.md +71 -0
- package/docs/components/atoms/aw-icon-system-color.md +121 -0
- package/docs/components/atoms/aw-icon-system-mono.md +205 -0
- package/docs/components/atoms/aw-icon.md +235 -0
- package/docs/components/atoms/aw-info.md +85 -0
- package/docs/components/atoms/aw-input.md +120 -0
- package/docs/components/atoms/aw-label.md +83 -0
- package/docs/components/atoms/aw-link.md +99 -0
- package/docs/components/atoms/aw-list.md +88 -0
- package/docs/components/atoms/aw-progress.md +70 -0
- package/docs/components/atoms/aw-radio.md +109 -0
- package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
- package/docs/components/atoms/aw-select-native.md +106 -0
- package/docs/components/atoms/aw-slider.md +82 -0
- package/docs/components/atoms/aw-sub-headline.md +73 -0
- package/docs/components/atoms/aw-switcher.md +115 -0
- package/docs/components/atoms/aw-tag.md +80 -0
- package/docs/components/atoms/aw-title.md +70 -0
- package/docs/components/atoms/aw-toggler.md +69 -0
- package/docs/components/layouts/aw-layout-center.md +168 -0
- package/docs/components/layouts/aw-layout-error.md +153 -0
- package/docs/components/layouts/aw-layout-provider.md +238 -0
- package/docs/components/layouts/aw-layout.md +88 -0
- package/docs/components/molecules/aw-action-button.md +91 -0
- package/docs/components/molecules/aw-alert.md +96 -0
- package/docs/components/molecules/aw-badge.md +108 -0
- package/docs/components/molecules/aw-banner-text.md +90 -0
- package/docs/components/molecules/aw-button-nav.md +46 -0
- package/docs/components/molecules/aw-button.md +123 -0
- package/docs/components/molecules/aw-description-input.md +67 -0
- package/docs/components/molecules/aw-empty-container.md +86 -0
- package/docs/components/molecules/aw-island.md +234 -0
- package/docs/components/molecules/aw-number.md +138 -0
- package/docs/components/molecules/aw-select-object.md +401 -0
- package/docs/components/molecules/aw-select.md +215 -0
- package/docs/components/molecules/aw-tab-nav.md +108 -0
- package/docs/components/molecules/aw-tel.md +129 -0
- package/docs/components/molecules/aw-textarea.md +83 -0
- package/docs/components/molecules/aw-userpic.md +115 -0
- package/docs/components/organisms/aw-address-block.md +64 -0
- package/docs/components/organisms/aw-address.md +132 -0
- package/docs/components/organisms/aw-birthday-picker.md +73 -0
- package/docs/components/organisms/aw-bottom-bar.md +66 -0
- package/docs/components/organisms/aw-calendar-days.md +115 -0
- package/docs/components/organisms/aw-calendar-nav.md +98 -0
- package/docs/components/organisms/aw-calendar-view.md +98 -0
- package/docs/components/organisms/aw-calendar.md +166 -0
- package/docs/components/organisms/aw-chart.md +154 -0
- package/docs/components/organisms/aw-chip-select.md +164 -0
- package/docs/components/organisms/aw-chip.md +126 -0
- package/docs/components/organisms/aw-code-snippet.md +94 -0
- package/docs/components/organisms/aw-code.md +132 -0
- package/docs/components/organisms/aw-context-menu.md +117 -0
- package/docs/components/organisms/aw-cropper.md +151 -0
- package/docs/components/organisms/aw-date.md +161 -0
- package/docs/components/organisms/aw-display-date.md +33 -0
- package/docs/components/organisms/aw-download-link.md +46 -0
- package/docs/components/organisms/aw-fetch-data.md +161 -0
- package/docs/components/organisms/aw-filter-chosen.md +226 -0
- package/docs/components/organisms/aw-filter-date-range.md +205 -0
- package/docs/components/organisms/aw-filter-month.md +43 -0
- package/docs/components/organisms/aw-filter-select.md +225 -0
- package/docs/components/organisms/aw-form.md +174 -0
- package/docs/components/organisms/aw-gmap-marker.md +86 -0
- package/docs/components/organisms/aw-gmap.md +90 -0
- package/docs/components/organisms/aw-image-upload.md +56 -0
- package/docs/components/organisms/aw-island-avatar.md +87 -0
- package/docs/components/organisms/aw-markdown-editor.md +104 -0
- package/docs/components/organisms/aw-modal-buttons.md +57 -0
- package/docs/components/organisms/aw-modal.md +246 -0
- package/docs/components/organisms/aw-model-edit.md +74 -0
- package/docs/components/organisms/aw-money.md +53 -0
- package/docs/components/organisms/aw-multi-block-builder.md +165 -0
- package/docs/components/organisms/aw-pagination.md +121 -0
- package/docs/components/organisms/aw-password.md +103 -0
- package/docs/components/organisms/aw-preview-card.md +45 -0
- package/docs/components/organisms/aw-search.md +116 -0
- package/docs/components/organisms/aw-subnav.md +122 -0
- package/docs/components/organisms/aw-table-builder.md +165 -0
- package/docs/components/organisms/aw-table-col.md +123 -0
- package/docs/components/organisms/aw-table-head.md +92 -0
- package/docs/components/organisms/aw-table-row.md +91 -0
- package/docs/components/organisms/aw-table.md +172 -0
- package/docs/components/organisms/aw-tags.md +54 -0
- package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
- package/docs/components/organisms/aw-uploader-files.md +125 -0
- package/docs/components/organisms/aw-uploader.md +163 -0
- package/docs/components/organisms/aw-user-menu.md +87 -0
- package/docs/components/pages/aw-page-aside.md +296 -0
- package/docs/components/pages/aw-page-menu-buttons.md +172 -0
- package/docs/components/pages/aw-page-modal.md +198 -0
- package/docs/components/pages/aw-page-single.md +253 -0
- package/docs/components/pages/aw-page.md +194 -0
- package/docs/configuration.md +493 -0
- package/docs/cookbook/advanced-patterns.md +1388 -0
- package/docs/cookbook/common-patterns.md +965 -0
- package/docs/cookbook/index.md +786 -0
- package/docs/getting-started.md +596 -0
- package/docs/guides/best-practices.md +1106 -0
- package/docs/guides/data-fetching.md +852 -0
- package/docs/guides/error-handling.md +1172 -0
- package/docs/guides/forms-guide.md +1329 -0
- package/docs/guides/mobile-subnavigation.md +359 -0
- package/docs/guides/page-patterns/aside-pages.md +1418 -0
- package/docs/guides/page-patterns/dashboard-pages.md +990 -0
- package/docs/guides/page-patterns/detail-pages.md +1493 -0
- package/docs/guides/page-patterns/list-pages.md +1094 -0
- package/docs/index.md +263 -1
- package/docs/integrations.md +870 -0
- package/docs/reference/menu.md +462 -0
- package/docs/reference/plugins.md +970 -0
- package/docs/reference/troubleshooting.md +945 -0
- package/nuxt/awes.config.js +9 -8
- package/nuxt/icons.css +26 -0
- package/nuxt/index.js +2 -2
- package/nuxt/pages/more.vue +1 -1
- package/package.json +5 -3
- package/readme.md +171 -1
- package/docs/aw-accordion-fold.md +0 -46
- package/docs/aw-address.md +0 -44
- package/docs/aw-avatar.md +0 -51
- package/docs/aw-badge.md +0 -32
- package/docs/aw-button-nav.md +0 -44
- package/docs/aw-button.md +0 -50
- package/docs/aw-calendar-days.md +0 -46
- package/docs/aw-calendar-nav.md +0 -25
- package/docs/aw-calendar-view.md +0 -12
- package/docs/aw-calendar.md +0 -59
- package/docs/aw-card.md +0 -48
- package/docs/aw-chart.md +0 -51
- package/docs/aw-checkbox.md +0 -56
- package/docs/aw-chip-select.md +0 -46
- package/docs/aw-chip.md +0 -53
- package/docs/aw-code-snippet.md +0 -18
- package/docs/aw-code.md +0 -56
- package/docs/aw-content-wrapper.md +0 -40
- package/docs/aw-context-menu.md +0 -31
- package/docs/aw-cropper.md +0 -60
- package/docs/aw-dashboard-card.md +0 -37
- package/docs/aw-dashboard-donut.md +0 -30
- package/docs/aw-dashboard-line.md +0 -20
- package/docs/aw-dashboard-progress.md +0 -33
- package/docs/aw-dashboard-section.md +0 -32
- package/docs/aw-dashboard-speed.md +0 -30
- package/docs/aw-date.md +0 -52
- package/docs/aw-dropdown-button.md +0 -31
- package/docs/aw-dropdown.md +0 -69
- package/docs/aw-fetch-data.md +0 -45
- package/docs/aw-form.md +0 -52
- package/docs/aw-grid.md +0 -48
- package/docs/aw-icon.md +0 -50
- package/docs/aw-info.md +0 -53
- package/docs/aw-input.md +0 -55
- package/docs/aw-layout-default.md +0 -30
- package/docs/aw-layout-frame-center.md +0 -29
- package/docs/aw-layout-simple.md +0 -49
- package/docs/aw-link.md +0 -54
- package/docs/aw-markdown-editor.md +0 -51
- package/docs/aw-modal.md +0 -63
- package/docs/aw-multi-block-builder.md +0 -66
- package/docs/aw-page.md +0 -36
- package/docs/aw-pagination.md +0 -54
- package/docs/aw-password.md +0 -48
- package/docs/aw-radio.md +0 -54
- package/docs/aw-search.md +0 -49
- package/docs/aw-select.md +0 -93
- package/docs/aw-slider.md +0 -40
- package/docs/aw-svg-image.md +0 -19
- package/docs/aw-switcher.md +0 -51
- package/docs/aw-tab-nav.md +0 -55
- package/docs/aw-table-builder.md +0 -58
- package/docs/aw-table-col.md +0 -33
- package/docs/aw-table-head.md +0 -28
- package/docs/aw-table-row.md +0 -33
- package/docs/aw-table.md +0 -59
- package/docs/aw-tel.md +0 -47
- package/docs/aw-textarea.md +0 -47
- package/docs/aw-timeline-builder.md +0 -50
- package/docs/aw-toggler.md +0 -41
- package/docs/aw-uploader-files.md +0 -20
- package/docs/aw-uploader.md +0 -60
- package/docs/aw-user-menu.md +0 -34
- package/docs/aw-userpic.md +0 -34
- /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
# List Page Patterns
|
|
2
|
+
|
|
3
|
+
Complete guide to building list pages with tables, search, and actions using AwPage and AwTableBuilder.
|
|
4
|
+
|
|
5
|
+
## When to Use AwPage
|
|
6
|
+
|
|
7
|
+
Use `AwPage` for:
|
|
8
|
+
- **List pages** - Display collections of items in tables
|
|
9
|
+
- **Dashboards** - Overview pages with metrics and data
|
|
10
|
+
- **Index pages** - Entry points to sections with navigation
|
|
11
|
+
|
|
12
|
+
**Key characteristics:**
|
|
13
|
+
- Shows aside menu by default
|
|
14
|
+
- Simple title prop
|
|
15
|
+
- Automatic integration with menu system
|
|
16
|
+
- Best for browsing and navigation
|
|
17
|
+
|
|
18
|
+
## Basic List Page
|
|
19
|
+
|
|
20
|
+
### Minimal Example
|
|
21
|
+
|
|
22
|
+
```markup
|
|
23
|
+
<template>
|
|
24
|
+
<AwPage title="Customers">
|
|
25
|
+
<AwTableBuilder
|
|
26
|
+
:collection="customers"
|
|
27
|
+
@click:row="viewCustomer"
|
|
28
|
+
>
|
|
29
|
+
<AwTableCol field="name" title="Name" />
|
|
30
|
+
<AwTableCol field="email" title="Email" />
|
|
31
|
+
<AwTableCol field="phone" title="Phone" />
|
|
32
|
+
</AwTableBuilder>
|
|
33
|
+
</AwPage>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
import Customers from '~/collections/Customers'
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
middleware: 'auth',
|
|
41
|
+
|
|
42
|
+
data() {
|
|
43
|
+
return {
|
|
44
|
+
customers: new Customers([], {
|
|
45
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
methods: {
|
|
51
|
+
viewCustomer(customer) {
|
|
52
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/customers/${customer.id}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**What happens:**
|
|
60
|
+
1. ✅ AwTableBuilder automatically calls `customers.fetch()` on mount
|
|
61
|
+
2. ✅ No manual `fetch()` or `mounted()` needed
|
|
62
|
+
3. ✅ Collection handles pagination automatically
|
|
63
|
+
4. ✅ Loading states managed internally
|
|
64
|
+
5. ✅ Row click navigates to detail page
|
|
65
|
+
|
|
66
|
+
**⚠️ Important: Don't use v-if for loading**
|
|
67
|
+
|
|
68
|
+
```markup
|
|
69
|
+
<!-- ❌ BAD - Prevents AwTableBuilder from mounting and fetching -->
|
|
70
|
+
<template>
|
|
71
|
+
<AwPage title="Customers">
|
|
72
|
+
<div v-if="loading">Loading...</div>
|
|
73
|
+
<AwTableBuilder v-else :collection="customers">
|
|
74
|
+
<!-- Never mounts if loading=true initially -->
|
|
75
|
+
</AwTableBuilder>
|
|
76
|
+
</AwPage>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<!-- ✅ GOOD - Let AwTableBuilder handle its own loading -->
|
|
80
|
+
<template>
|
|
81
|
+
<AwPage title="Customers">
|
|
82
|
+
<AwTableBuilder :collection="customers">
|
|
83
|
+
<!-- Mounts immediately, shows internal loading state -->
|
|
84
|
+
</AwTableBuilder>
|
|
85
|
+
</AwPage>
|
|
86
|
+
</template>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Header Action Buttons
|
|
90
|
+
|
|
91
|
+
**⚠️ Important: Create buttons should not be in menu items**
|
|
92
|
+
|
|
93
|
+
Create actions should only appear on list pages using `AwPageMenuButtons` and in empty states. Do not add create pages to menu navigation items.
|
|
94
|
+
|
|
95
|
+
### Single CTA Button
|
|
96
|
+
|
|
97
|
+
For navigation to create pages, use `href` instead of `listeners`:
|
|
98
|
+
|
|
99
|
+
```markup
|
|
100
|
+
<template>
|
|
101
|
+
<AwPage title="Templates">
|
|
102
|
+
<template #buttons>
|
|
103
|
+
<AwPageMenuButtons
|
|
104
|
+
:items="[{
|
|
105
|
+
text: 'Create Template',
|
|
106
|
+
icon: 'awesio/plus',
|
|
107
|
+
cta: true,
|
|
108
|
+
href: '/templates/create'
|
|
109
|
+
}]"
|
|
110
|
+
/>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<AwTableBuilder :collection="templates">
|
|
114
|
+
<!-- Table columns -->
|
|
115
|
+
</AwTableBuilder>
|
|
116
|
+
</AwPage>
|
|
117
|
+
</template>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Multiple Action Buttons
|
|
121
|
+
|
|
122
|
+
```markup
|
|
123
|
+
<template #buttons>
|
|
124
|
+
<AwPageMenuButtons
|
|
125
|
+
:items="[
|
|
126
|
+
{
|
|
127
|
+
text: 'Export',
|
|
128
|
+
icon: 'awesio/download',
|
|
129
|
+
color: 'mono',
|
|
130
|
+
listeners: { click: exportData }
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
text: 'Import',
|
|
134
|
+
icon: 'awesio/upload',
|
|
135
|
+
color: 'mono',
|
|
136
|
+
listeners: { click: importData }
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
text: 'Create',
|
|
140
|
+
icon: 'awesio/plus',
|
|
141
|
+
cta: true,
|
|
142
|
+
listeners: { click: createItem }
|
|
143
|
+
}
|
|
144
|
+
]"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Button Configuration:**
|
|
150
|
+
- `text` - Button label (required)
|
|
151
|
+
- `icon` - Icon name (optional)
|
|
152
|
+
- `color` - `'mono'`, `'primary'`, `'accent'` (default: 'mono')
|
|
153
|
+
- `cta` - Makes button prominent with accent color (default: false)
|
|
154
|
+
- `href` - Navigation URL (use for navigation, preferred over listeners)
|
|
155
|
+
- `listeners` - Event handlers object `{ click: handler }` (use only for non-navigation actions)
|
|
156
|
+
|
|
157
|
+
**Button Ordering:**
|
|
158
|
+
- Secondary actions (Export, Import) first
|
|
159
|
+
- Primary CTA (Create, Add) last
|
|
160
|
+
- CTA buttons automatically styled with accent color
|
|
161
|
+
|
|
162
|
+
## Search Integration
|
|
163
|
+
|
|
164
|
+
### Basic Search
|
|
165
|
+
|
|
166
|
+
```markup
|
|
167
|
+
<template>
|
|
168
|
+
<AwPage title="Customers">
|
|
169
|
+
<!-- Search bar -->
|
|
170
|
+
<div class="flex justify-end mb-6">
|
|
171
|
+
<AwSearch class="w-full lg:w-auto" />
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Table watches for search param changes -->
|
|
175
|
+
<AwTableBuilder
|
|
176
|
+
:collection="customers"
|
|
177
|
+
:watch-params="['search']"
|
|
178
|
+
>
|
|
179
|
+
<AwTableCol field="name" title="Name" />
|
|
180
|
+
<AwTableCol field="email" title="Email" />
|
|
181
|
+
</AwTableBuilder>
|
|
182
|
+
</AwPage>
|
|
183
|
+
</template>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**How it works:**
|
|
187
|
+
1. ✅ AwSearch updates `$route.query.search`
|
|
188
|
+
2. ✅ AwTableBuilder watches `search` param via `:watch-params`
|
|
189
|
+
3. ✅ Table automatically refetches when search changes
|
|
190
|
+
4. ✅ Collection receives search param in API request
|
|
191
|
+
|
|
192
|
+
### Search with Filters
|
|
193
|
+
|
|
194
|
+
When using `AwFilterSelect` components that sync with URL parameters, use `AwFilterChosen` to display active filters:
|
|
195
|
+
|
|
196
|
+
```markup
|
|
197
|
+
<template>
|
|
198
|
+
<AwPage title="Users">
|
|
199
|
+
<div class="flex flex-wrap gap-4 mb-6">
|
|
200
|
+
<AwFilterSelect
|
|
201
|
+
param="is_active"
|
|
202
|
+
label="Status"
|
|
203
|
+
:options="statusOptions"
|
|
204
|
+
single
|
|
205
|
+
/>
|
|
206
|
+
<AwFilterSelect
|
|
207
|
+
param="permissions"
|
|
208
|
+
label="Permissions"
|
|
209
|
+
:options="permissionOptions"
|
|
210
|
+
/>
|
|
211
|
+
<AwSearch class="ml-auto" />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<AwFilterChosen :watch-params="['is_active', 'permissions']">
|
|
215
|
+
<template #is_active="{ value }">
|
|
216
|
+
{{ getStatusLabel(value) }}
|
|
217
|
+
</template>
|
|
218
|
+
<template #permissions="{ value }">
|
|
219
|
+
{{ getPermissionLabel(value) }}
|
|
220
|
+
</template>
|
|
221
|
+
</AwFilterChosen>
|
|
222
|
+
|
|
223
|
+
<AwTableBuilder
|
|
224
|
+
:collection="users"
|
|
225
|
+
:watch-params="['search', 'is_active', 'permissions']"
|
|
226
|
+
>
|
|
227
|
+
<AwTableCol field="first_name" title="First Name" />
|
|
228
|
+
<AwTableCol field="last_name" title="Last Name" />
|
|
229
|
+
</AwTableBuilder>
|
|
230
|
+
</AwPage>
|
|
231
|
+
</template>
|
|
232
|
+
|
|
233
|
+
<script>
|
|
234
|
+
export default {
|
|
235
|
+
computed: {
|
|
236
|
+
statusOptions() {
|
|
237
|
+
return [
|
|
238
|
+
{ id: true, title: 'Active' },
|
|
239
|
+
{ id: false, title: 'Inactive' }
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
permissionOptions() {
|
|
244
|
+
return [
|
|
245
|
+
{ id: 'manage-profile', title: 'Manage profile' },
|
|
246
|
+
{ id: 'manage-users', title: 'Manage users' }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
methods: {
|
|
252
|
+
getStatusLabel(value) {
|
|
253
|
+
if (value === true || value === 'true') return 'Active'
|
|
254
|
+
if (value === false || value === 'false') return 'Inactive'
|
|
255
|
+
return value
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
getPermissionLabel(value) {
|
|
259
|
+
if (!value) return value
|
|
260
|
+
|
|
261
|
+
const permissions = Array.isArray(value)
|
|
262
|
+
? value
|
|
263
|
+
: typeof value === 'string'
|
|
264
|
+
? value.split(',').map((p) => p.trim())
|
|
265
|
+
: [value]
|
|
266
|
+
|
|
267
|
+
return permissions
|
|
268
|
+
.map((perm) => {
|
|
269
|
+
const option = this.permissionOptions.find(
|
|
270
|
+
(opt) => opt.id === perm || String(opt.id) === String(perm)
|
|
271
|
+
)
|
|
272
|
+
return option ? option.title : perm
|
|
273
|
+
})
|
|
274
|
+
.join(', ')
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Best Practices:**
|
|
282
|
+
- ✅ **Always use custom slots** in `AwFilterChosen` to display user-friendly text instead of raw parameter values
|
|
283
|
+
- ✅ **Omit `'search'` from watch-params** - Search doesn't need to be displayed as a chip
|
|
284
|
+
- ✅ **Place search on the right** using `ml-auto` class
|
|
285
|
+
- ✅ **Handle different value types** - Values can be strings, booleans, arrays, or comma-separated strings
|
|
286
|
+
|
|
287
|
+
## Custom Column Rendering
|
|
288
|
+
|
|
289
|
+
### Understanding the `field` Prop
|
|
290
|
+
|
|
291
|
+
**⚠️ Critical Distinction:**
|
|
292
|
+
|
|
293
|
+
```markup
|
|
294
|
+
<!-- WITH field: cell = field value only -->
|
|
295
|
+
<AwTableCol title="Name" field="name">
|
|
296
|
+
<template #default="{ cell }">
|
|
297
|
+
{{ cell }} <!-- cell is just the string value of 'name' -->
|
|
298
|
+
</template>
|
|
299
|
+
</AwTableCol>
|
|
300
|
+
|
|
301
|
+
<!-- WITHOUT field: cell = entire row object -->
|
|
302
|
+
<AwTableCol title="Name">
|
|
303
|
+
<template #default="{ cell }">
|
|
304
|
+
{{ cell.name }} <!-- cell is the full row object -->
|
|
305
|
+
{{ cell.email }} <!-- can access other fields -->
|
|
306
|
+
</template>
|
|
307
|
+
</AwTableCol>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Simple Field Display
|
|
311
|
+
|
|
312
|
+
```markup
|
|
313
|
+
<!-- No custom template needed for simple fields -->
|
|
314
|
+
<AwTableCol field="name" title="Name" />
|
|
315
|
+
<AwTableCol field="email" title="Email" />
|
|
316
|
+
<AwTableCol field="phone" title="Phone" />
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Status Badge
|
|
320
|
+
|
|
321
|
+
```markup
|
|
322
|
+
<AwTableCol title="Status">
|
|
323
|
+
<template #default="{ cell }">
|
|
324
|
+
<AwLabel
|
|
325
|
+
:label="cell.is_active ? 'Active' : 'Inactive'"
|
|
326
|
+
:color="cell.is_active ? 'success' : 'mono'"
|
|
327
|
+
/>
|
|
328
|
+
</template>
|
|
329
|
+
</AwTableCol>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Date Formatting
|
|
333
|
+
|
|
334
|
+
```markup
|
|
335
|
+
<AwTableCol title="Created">
|
|
336
|
+
<template #default="{ cell }">
|
|
337
|
+
{{ $dayjs(cell.created_at).format('ll') }}
|
|
338
|
+
</template>
|
|
339
|
+
</AwTableCol>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Common date formats:**
|
|
343
|
+
- `ll` - Jan 15, 2024
|
|
344
|
+
- `lll` - Jan 15, 2024 10:30 AM
|
|
345
|
+
- `LL` - January 15, 2024
|
|
346
|
+
- `MMMM YYYY` - January 2024
|
|
347
|
+
|
|
348
|
+
### Currency Formatting
|
|
349
|
+
|
|
350
|
+
```markup
|
|
351
|
+
<AwTableCol title="Total">
|
|
352
|
+
<template #default="{ cell }">
|
|
353
|
+
${{ (cell.total / 100).toFixed(2) }}
|
|
354
|
+
</template>
|
|
355
|
+
</AwTableCol>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Combined Data
|
|
359
|
+
|
|
360
|
+
```markup
|
|
361
|
+
<AwTableCol title="Customer">
|
|
362
|
+
<template #default="{ cell }">
|
|
363
|
+
<div class="flex items-center gap-2">
|
|
364
|
+
<AwAvatar :src="cell.avatar" size="sm" />
|
|
365
|
+
<div>
|
|
366
|
+
<div class="font-medium">{{ cell.name }}</div>
|
|
367
|
+
<div class="text-sm text-secondary">{{ cell.email }}</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</template>
|
|
371
|
+
</AwTableCol>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Mobile vs Desktop Table Layouts
|
|
375
|
+
|
|
376
|
+
### Reordering Columns for Mobile
|
|
377
|
+
|
|
378
|
+
A common pattern is to display important information differently on mobile vs desktop. For example, an employee userpic might be in the 3rd or 4th column on desktop, but should appear in the mobile header for better visual hierarchy.
|
|
379
|
+
|
|
380
|
+
**Pattern: Userpic in Mobile Header**
|
|
381
|
+
|
|
382
|
+
```markup
|
|
383
|
+
<template>
|
|
384
|
+
<AwPage title="Jobs">
|
|
385
|
+
<AwTableBuilder :collection="jobs">
|
|
386
|
+
<!-- Job title - shown on both desktop and mobile -->
|
|
387
|
+
<AwTableCol field="title" title="Job Title" />
|
|
388
|
+
|
|
389
|
+
<!-- Client info -->
|
|
390
|
+
<AwTableCol field="client_name" title="Client" />
|
|
391
|
+
|
|
392
|
+
<!-- Assigned employee - 3rd column on desktop, hidden on mobile -->
|
|
393
|
+
<AwTableCol title="Assigned To" hide-mobile>
|
|
394
|
+
<template #default="{ cell }">
|
|
395
|
+
<AwUserpic
|
|
396
|
+
:src="cell.employee?.avatar"
|
|
397
|
+
:name="cell.employee?.name"
|
|
398
|
+
:description="cell.employee?.role"
|
|
399
|
+
/>
|
|
400
|
+
</template>
|
|
401
|
+
</AwTableCol>
|
|
402
|
+
|
|
403
|
+
<!-- Due date -->
|
|
404
|
+
<AwTableCol title="Due Date">
|
|
405
|
+
<template #default="{ cell }">
|
|
406
|
+
{{ $dayjs(cell.due_date).format('ll') }}
|
|
407
|
+
</template>
|
|
408
|
+
</AwTableCol>
|
|
409
|
+
|
|
410
|
+
<!-- Mobile header: Show userpic prominently -->
|
|
411
|
+
<template #mobile-header-content="{ cell }">
|
|
412
|
+
<AwUserpic
|
|
413
|
+
:src="cell.employee?.avatar"
|
|
414
|
+
:name="cell.employee?.name"
|
|
415
|
+
:description="cell.employee?.role"
|
|
416
|
+
/>
|
|
417
|
+
</template>
|
|
418
|
+
</AwTableBuilder>
|
|
419
|
+
</AwPage>
|
|
420
|
+
</template>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**What happens:**
|
|
424
|
+
- **Desktop:** Userpic appears in "Assigned To" column (3rd position)
|
|
425
|
+
- **Mobile:** Row collapses into card layout with userpic in header
|
|
426
|
+
- `hide-mobile` prop hides the desktop column on mobile to avoid duplication
|
|
427
|
+
- `mobile-header-content` slot positions userpic prominently in collapsed row header
|
|
428
|
+
|
|
429
|
+
**Pattern: Main Info in Mobile Header**
|
|
430
|
+
|
|
431
|
+
Sometimes the most important information should be highlighted in the mobile header:
|
|
432
|
+
|
|
433
|
+
```markup
|
|
434
|
+
<template>
|
|
435
|
+
<AwPage title="Services">
|
|
436
|
+
<AwTableBuilder :collection="services">
|
|
437
|
+
<!-- Service name - desktop only -->
|
|
438
|
+
<AwTableCol field="name" title="Service" hide-mobile />
|
|
439
|
+
|
|
440
|
+
<!-- Price -->
|
|
441
|
+
<AwTableCol title="Price">
|
|
442
|
+
<template #default="{ cell }">
|
|
443
|
+
${{ (cell.price / 100).toFixed(2) }}
|
|
444
|
+
</template>
|
|
445
|
+
</AwTableCol>
|
|
446
|
+
|
|
447
|
+
<!-- Duration -->
|
|
448
|
+
<AwTableCol field="duration" title="Duration" />
|
|
449
|
+
|
|
450
|
+
<!-- Category -->
|
|
451
|
+
<AwTableCol field="category" title="Category" />
|
|
452
|
+
|
|
453
|
+
<!-- Mobile header: Show service name as headline -->
|
|
454
|
+
<template #mobile-header-content="{ cell }">
|
|
455
|
+
<AwHeadline>{{ cell.name }}</AwHeadline>
|
|
456
|
+
</template>
|
|
457
|
+
</AwTableBuilder>
|
|
458
|
+
</AwPage>
|
|
459
|
+
</template>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**What happens:**
|
|
463
|
+
- **Desktop:** Service name in first column
|
|
464
|
+
- **Mobile:** Service name becomes prominent headline in collapsed row header
|
|
465
|
+
- Other details (price, duration, category) shown below in mobile card layout
|
|
466
|
+
- `hide-mobile` on desktop column prevents duplicate service name
|
|
467
|
+
|
|
468
|
+
**Pattern: Combined User + Main Info**
|
|
469
|
+
|
|
470
|
+
For complex tables, combine userpic with main information in mobile header:
|
|
471
|
+
|
|
472
|
+
```markup
|
|
473
|
+
<template>
|
|
474
|
+
<AwPage title="Bookings">
|
|
475
|
+
<AwTableBuilder :collection="bookings">
|
|
476
|
+
<!-- Service name - hidden on mobile -->
|
|
477
|
+
<AwTableCol field="service_name" title="Service" hide-mobile />
|
|
478
|
+
|
|
479
|
+
<!-- Date & Time -->
|
|
480
|
+
<AwTableCol title="Date & Time">
|
|
481
|
+
<template #default="{ cell }">
|
|
482
|
+
{{ $dayjs(cell.scheduled_at).format('lll') }}
|
|
483
|
+
</template>
|
|
484
|
+
</AwTableCol>
|
|
485
|
+
|
|
486
|
+
<!-- Client - shown on desktop, hidden on mobile -->
|
|
487
|
+
<AwTableCol title="Client" hide-mobile>
|
|
488
|
+
<template #default="{ cell }">
|
|
489
|
+
<AwUserpic
|
|
490
|
+
:src="cell.client?.avatar"
|
|
491
|
+
:name="cell.client?.name"
|
|
492
|
+
:description="cell.client?.email"
|
|
493
|
+
/>
|
|
494
|
+
</template>
|
|
495
|
+
</AwTableCol>
|
|
496
|
+
|
|
497
|
+
<!-- Status -->
|
|
498
|
+
<AwTableCol title="Status">
|
|
499
|
+
<template #default="{ cell }">
|
|
500
|
+
<AwLabel
|
|
501
|
+
:label="cell.status"
|
|
502
|
+
:color="getStatusColor(cell.status)"
|
|
503
|
+
/>
|
|
504
|
+
</template>
|
|
505
|
+
</AwTableCol>
|
|
506
|
+
|
|
507
|
+
<!-- Mobile header: Service + Client -->
|
|
508
|
+
<template #mobile-header-content="{ cell }">
|
|
509
|
+
<div class="space-y-2">
|
|
510
|
+
<AwHeadline>{{ cell.service_name }}</AwHeadline>
|
|
511
|
+
<AwUserpic
|
|
512
|
+
:src="cell.client?.avatar"
|
|
513
|
+
:name="cell.client?.name"
|
|
514
|
+
:description="cell.client?.email"
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
</template>
|
|
518
|
+
</AwTableBuilder>
|
|
519
|
+
</AwPage>
|
|
520
|
+
</template>
|
|
521
|
+
|
|
522
|
+
<script>
|
|
523
|
+
export default {
|
|
524
|
+
methods: {
|
|
525
|
+
getStatusColor(status) {
|
|
526
|
+
const colors = {
|
|
527
|
+
confirmed: 'success',
|
|
528
|
+
pending: 'warning',
|
|
529
|
+
cancelled: 'error',
|
|
530
|
+
completed: 'info'
|
|
531
|
+
}
|
|
532
|
+
return colors[status] || 'mono'
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
</script>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Best Practices for Mobile Layouts
|
|
540
|
+
|
|
541
|
+
**1. Use `hide-mobile` to Prevent Duplication**
|
|
542
|
+
|
|
543
|
+
```markup
|
|
544
|
+
<!-- ✅ GOOD - Hide desktop column to avoid showing info twice -->
|
|
545
|
+
<AwTableCol title="Employee" hide-mobile>
|
|
546
|
+
<template #default="{ cell }">
|
|
547
|
+
<AwUserpic :name="cell.employee.name" />
|
|
548
|
+
</template>
|
|
549
|
+
</AwTableCol>
|
|
550
|
+
|
|
551
|
+
<template #mobile-header-content="{ cell }">
|
|
552
|
+
<AwUserpic :name="cell.employee.name" />
|
|
553
|
+
</template>
|
|
554
|
+
|
|
555
|
+
<!-- ❌ BAD - Shows employee info twice on mobile -->
|
|
556
|
+
<AwTableCol title="Employee">
|
|
557
|
+
<template #default="{ cell }">
|
|
558
|
+
<AwUserpic :name="cell.employee.name" />
|
|
559
|
+
</template>
|
|
560
|
+
</AwTableCol>
|
|
561
|
+
|
|
562
|
+
<template #mobile-header-content="{ cell }">
|
|
563
|
+
<AwUserpic :name="cell.employee.name" />
|
|
564
|
+
</template>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**2. Prioritize Visual Hierarchy on Mobile**
|
|
568
|
+
|
|
569
|
+
```markup
|
|
570
|
+
<!-- ✅ GOOD - Most important info in mobile header -->
|
|
571
|
+
<template #mobile-header-content="{ cell }">
|
|
572
|
+
<AwHeadline>{{ cell.title }}</AwHeadline>
|
|
573
|
+
<AwUserpic :name="cell.owner.name" />
|
|
574
|
+
</template>
|
|
575
|
+
|
|
576
|
+
<!-- ❌ BAD - Generic info that doesn't help user scan -->
|
|
577
|
+
<template #mobile-header-content="{ cell }">
|
|
578
|
+
<span>{{ cell.id }}</span>
|
|
579
|
+
</template>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**3. Use Appropriate Components**
|
|
583
|
+
|
|
584
|
+
- **AwHeadline** - For main titles/names
|
|
585
|
+
- **AwUserpic** - For person/employee information with avatar
|
|
586
|
+
- **AwLabel** - For status badges
|
|
587
|
+
- Keep mobile headers concise and scannable
|
|
588
|
+
|
|
589
|
+
**4. Consider Touch Targets**
|
|
590
|
+
|
|
591
|
+
```markup
|
|
592
|
+
<!-- Mobile header content should support touch/tap -->
|
|
593
|
+
<AwTableBuilder
|
|
594
|
+
:collection="items"
|
|
595
|
+
@click:row="viewItem"
|
|
596
|
+
>
|
|
597
|
+
<template #mobile-header-content="{ cell }">
|
|
598
|
+
<AwUserpic :name="cell.name" />
|
|
599
|
+
</template>
|
|
600
|
+
</AwTableBuilder>
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Row Actions
|
|
604
|
+
|
|
605
|
+
### Click Row (Navigation)
|
|
606
|
+
|
|
607
|
+
```markup
|
|
608
|
+
<template>
|
|
609
|
+
<AwPage title="Customers">
|
|
610
|
+
<AwTableBuilder
|
|
611
|
+
:collection="customers"
|
|
612
|
+
@click:row="viewCustomer"
|
|
613
|
+
>
|
|
614
|
+
<AwTableCol field="name" title="Name" />
|
|
615
|
+
<AwTableCol field="email" title="Email" />
|
|
616
|
+
</AwTableBuilder>
|
|
617
|
+
</AwPage>
|
|
618
|
+
</template>
|
|
619
|
+
|
|
620
|
+
<script>
|
|
621
|
+
export default {
|
|
622
|
+
methods: {
|
|
623
|
+
viewCustomer(customer) {
|
|
624
|
+
this.$router.push(
|
|
625
|
+
`/${this.$route.params.shop_uuid}/customers/${customer.id}`
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
</script>
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Dropdown Actions
|
|
634
|
+
|
|
635
|
+
```markup
|
|
636
|
+
<template>
|
|
637
|
+
<AwPage title="Templates">
|
|
638
|
+
<AwTableBuilder :collection="templates">
|
|
639
|
+
<AwTableCol field="name" title="Name" />
|
|
640
|
+
<AwTableCol field="status" title="Status" />
|
|
641
|
+
|
|
642
|
+
<!-- Dropdown slot for row actions -->
|
|
643
|
+
<template #dropdown="{ cell }">
|
|
644
|
+
<AwDropdownButton
|
|
645
|
+
text="View"
|
|
646
|
+
@click="viewItem(cell)"
|
|
647
|
+
/>
|
|
648
|
+
<AwDropdownButton
|
|
649
|
+
text="Edit"
|
|
650
|
+
@click="editItem(cell)"
|
|
651
|
+
/>
|
|
652
|
+
<AwDropdownButton
|
|
653
|
+
text="Duplicate"
|
|
654
|
+
@click="duplicateItem(cell)"
|
|
655
|
+
/>
|
|
656
|
+
<AwDropdownButton
|
|
657
|
+
text="Delete"
|
|
658
|
+
color="error"
|
|
659
|
+
@click="deleteItem(cell)"
|
|
660
|
+
/>
|
|
661
|
+
</template>
|
|
662
|
+
</AwTableBuilder>
|
|
663
|
+
</AwPage>
|
|
664
|
+
</template>
|
|
665
|
+
|
|
666
|
+
<script>
|
|
667
|
+
export default {
|
|
668
|
+
methods: {
|
|
669
|
+
viewItem(item) {
|
|
670
|
+
this.$router.push(`/templates/${item.id}`)
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
editItem(item) {
|
|
674
|
+
this.$router.push(`/templates/${item.id}/edit`)
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
duplicateItem(item) {
|
|
678
|
+
// Duplicate logic
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
async deleteItem(item) {
|
|
682
|
+
const confirmed = await this.$confirm({
|
|
683
|
+
title: 'Delete Template',
|
|
684
|
+
message: `Are you sure you want to delete "${item.name}"?`
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
if (!confirmed) return
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
await this.$axios.delete(`/api/templates/${item.id}`)
|
|
691
|
+
this.$notify({
|
|
692
|
+
message: 'Template deleted successfully',
|
|
693
|
+
type: 'success'
|
|
694
|
+
})
|
|
695
|
+
this.templates.fetch() // Refetch list
|
|
696
|
+
} catch (error) {
|
|
697
|
+
this.$notify({
|
|
698
|
+
message: 'Failed to delete template',
|
|
699
|
+
type: 'error'
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
</script>
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**⚠️ Important: Can't use both @click:row and #dropdown**
|
|
709
|
+
|
|
710
|
+
```markup
|
|
711
|
+
<!-- ❌ BAD - Conflict! Row click and dropdown both trigger -->
|
|
712
|
+
<AwTableBuilder
|
|
713
|
+
:collection="items"
|
|
714
|
+
@click:row="viewItem"
|
|
715
|
+
>
|
|
716
|
+
<template #dropdown="{ cell }">
|
|
717
|
+
<AwDropdownButton text="Edit" />
|
|
718
|
+
</template>
|
|
719
|
+
</AwTableBuilder>
|
|
720
|
+
|
|
721
|
+
<!-- ✅ GOOD - Choose one approach -->
|
|
722
|
+
<!-- Option 1: Row click only -->
|
|
723
|
+
<AwTableBuilder
|
|
724
|
+
:collection="items"
|
|
725
|
+
@click:row="viewItem"
|
|
726
|
+
>
|
|
727
|
+
<!-- No dropdown -->
|
|
728
|
+
</AwTableBuilder>
|
|
729
|
+
|
|
730
|
+
<!-- Option 2: Dropdown only -->
|
|
731
|
+
<AwTableBuilder :collection="items">
|
|
732
|
+
<template #dropdown="{ cell }">
|
|
733
|
+
<AwDropdownButton text="View" @click="viewItem(cell)" />
|
|
734
|
+
<AwDropdownButton text="Edit" @click="editItem(cell)" />
|
|
735
|
+
</template>
|
|
736
|
+
</AwTableBuilder>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Dropdown Action Ordering:**
|
|
740
|
+
1. View (if separate from row click)
|
|
741
|
+
2. Edit
|
|
742
|
+
3. Other actions (Duplicate, Archive, etc.)
|
|
743
|
+
4. Delete (last, in error color)
|
|
744
|
+
|
|
745
|
+
## Mobile Breadcrumbs
|
|
746
|
+
|
|
747
|
+
For pages in subfolders, add breadcrumbs for mobile navigation:
|
|
748
|
+
|
|
749
|
+
```markup
|
|
750
|
+
<template>
|
|
751
|
+
<AwPage title="Email Templates">
|
|
752
|
+
<!-- Mobile breadcrumb -->
|
|
753
|
+
<template #mobile-breadcrumbs>
|
|
754
|
+
<AwButton
|
|
755
|
+
:href="`/${$route.params.shop_uuid}/notifications`"
|
|
756
|
+
theme="text"
|
|
757
|
+
icon="arrow-left"
|
|
758
|
+
text="Notifications"
|
|
759
|
+
/>
|
|
760
|
+
</template>
|
|
761
|
+
|
|
762
|
+
<!-- Page content -->
|
|
763
|
+
</AwPage>
|
|
764
|
+
</template>
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## Empty States
|
|
768
|
+
|
|
769
|
+
Use the `cta-button` prop on `AwEmptyContainer` to add a create button in the empty state. The button should use the same `href` as the header button:
|
|
770
|
+
|
|
771
|
+
```markup
|
|
772
|
+
<template>
|
|
773
|
+
<AwPage title="Customers">
|
|
774
|
+
<template #buttons>
|
|
775
|
+
<AwPageMenuButtons
|
|
776
|
+
:items="[{
|
|
777
|
+
text: 'Create Customer',
|
|
778
|
+
icon: 'awesio/plus',
|
|
779
|
+
cta: true,
|
|
780
|
+
href: '/customers/create'
|
|
781
|
+
}]"
|
|
782
|
+
/>
|
|
783
|
+
</template>
|
|
784
|
+
|
|
785
|
+
<AwTableBuilder :collection="customers">
|
|
786
|
+
<AwTableCol field="name" title="Name" />
|
|
787
|
+
|
|
788
|
+
<!-- Empty state with create button -->
|
|
789
|
+
<template #empty-container>
|
|
790
|
+
<AwEmptyContainer
|
|
791
|
+
icon="users"
|
|
792
|
+
title="No customers yet"
|
|
793
|
+
description="Create your first customer to get started"
|
|
794
|
+
:cta-button="{
|
|
795
|
+
text: 'Create Customer',
|
|
796
|
+
icon: 'awesio/plus',
|
|
797
|
+
href: '/customers/create'
|
|
798
|
+
}"
|
|
799
|
+
/>
|
|
800
|
+
</template>
|
|
801
|
+
</AwTableBuilder>
|
|
802
|
+
</AwPage>
|
|
803
|
+
</template>
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Best Practice:** Always use the same create button in both the header (`AwPageMenuButtons`) and empty state (`AwEmptyContainer` cta-button) with the same `href` for consistency.
|
|
807
|
+
|
|
808
|
+
## Complete Example
|
|
809
|
+
|
|
810
|
+
Full-featured list page with all patterns:
|
|
811
|
+
|
|
812
|
+
```markup
|
|
813
|
+
<template>
|
|
814
|
+
<AwPage title="Email Templates">
|
|
815
|
+
<!-- Mobile breadcrumbs -->
|
|
816
|
+
<template #mobile-breadcrumbs>
|
|
817
|
+
<AwButton
|
|
818
|
+
:href="`/${$route.params.shop_uuid}/notifications`"
|
|
819
|
+
theme="text"
|
|
820
|
+
icon="arrow-left"
|
|
821
|
+
text="Back to Notifications"
|
|
822
|
+
/>
|
|
823
|
+
</template>
|
|
824
|
+
|
|
825
|
+
<!-- Header action buttons -->
|
|
826
|
+
<template #buttons>
|
|
827
|
+
<AwPageMenuButtons
|
|
828
|
+
:items="[
|
|
829
|
+
{
|
|
830
|
+
text: 'Import',
|
|
831
|
+
icon: 'awesio/upload',
|
|
832
|
+
color: 'mono',
|
|
833
|
+
listeners: { click: importTemplates }
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
text: 'Create Template',
|
|
837
|
+
icon: 'awesio/plus',
|
|
838
|
+
cta: true,
|
|
839
|
+
href: `/${shopUuid}/templates/create`
|
|
840
|
+
}
|
|
841
|
+
]"
|
|
842
|
+
/>
|
|
843
|
+
</template>
|
|
844
|
+
|
|
845
|
+
<!-- Search and filters -->
|
|
846
|
+
<div class="flex flex-col lg:flex-row gap-4 mb-6">
|
|
847
|
+
<AwSearch class="flex-1" />
|
|
848
|
+
<AwSelect
|
|
849
|
+
v-model="channelFilter"
|
|
850
|
+
:options="['all', 'email', 'sms', 'push']"
|
|
851
|
+
label="Channel"
|
|
852
|
+
class="w-full lg:w-48"
|
|
853
|
+
/>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
<!-- Table with data -->
|
|
857
|
+
<AwTableBuilder
|
|
858
|
+
:collection="templates"
|
|
859
|
+
:watch-params="['search']"
|
|
860
|
+
:options="{ shop_uuid: shopUuid, channel: channelFilter }"
|
|
861
|
+
>
|
|
862
|
+
<!-- Simple field columns -->
|
|
863
|
+
<AwTableCol field="name" title="Template Name" />
|
|
864
|
+
|
|
865
|
+
<!-- Custom status column -->
|
|
866
|
+
<AwTableCol title="Status">
|
|
867
|
+
<template #default="{ cell }">
|
|
868
|
+
<AwLabel
|
|
869
|
+
:label="cell.is_active ? 'Active' : 'Inactive'"
|
|
870
|
+
:color="cell.is_active ? 'success' : 'mono'"
|
|
871
|
+
/>
|
|
872
|
+
</template>
|
|
873
|
+
</AwTableCol>
|
|
874
|
+
|
|
875
|
+
<!-- Channel badge -->
|
|
876
|
+
<AwTableCol title="Channel">
|
|
877
|
+
<template #default="{ cell }">
|
|
878
|
+
<AwBadge :text="cell.channel" />
|
|
879
|
+
</template>
|
|
880
|
+
</AwTableCol>
|
|
881
|
+
|
|
882
|
+
<!-- Date column -->
|
|
883
|
+
<AwTableCol title="Last Modified">
|
|
884
|
+
<template #default="{ cell }">
|
|
885
|
+
{{ $dayjs(cell.updated_at).format('ll') }}
|
|
886
|
+
</template>
|
|
887
|
+
</AwTableCol>
|
|
888
|
+
|
|
889
|
+
<!-- Row actions dropdown -->
|
|
890
|
+
<template #dropdown="{ cell }">
|
|
891
|
+
<AwDropdownButton
|
|
892
|
+
text="Edit"
|
|
893
|
+
@click="editTemplate(cell)"
|
|
894
|
+
/>
|
|
895
|
+
<AwDropdownButton
|
|
896
|
+
text="Duplicate"
|
|
897
|
+
@click="duplicateTemplate(cell)"
|
|
898
|
+
/>
|
|
899
|
+
<AwDropdownButton
|
|
900
|
+
:text="cell.is_active ? 'Deactivate' : 'Activate'"
|
|
901
|
+
@click="toggleActive(cell)"
|
|
902
|
+
/>
|
|
903
|
+
<AwDropdownButton
|
|
904
|
+
text="Delete"
|
|
905
|
+
color="error"
|
|
906
|
+
@click="deleteTemplate(cell)"
|
|
907
|
+
/>
|
|
908
|
+
</template>
|
|
909
|
+
|
|
910
|
+
<!-- Empty state -->
|
|
911
|
+
<template #empty-container>
|
|
912
|
+
<AwEmptyContainer
|
|
913
|
+
icon="awesio/mail"
|
|
914
|
+
title="No templates yet"
|
|
915
|
+
description="Create your first email template"
|
|
916
|
+
:cta-button="{
|
|
917
|
+
text: 'Create Template',
|
|
918
|
+
icon: 'awesio/plus',
|
|
919
|
+
href: `/${shopUuid}/templates/create`
|
|
920
|
+
}"
|
|
921
|
+
/>
|
|
922
|
+
</template>
|
|
923
|
+
</AwTableBuilder>
|
|
924
|
+
</AwPage>
|
|
925
|
+
</template>
|
|
926
|
+
|
|
927
|
+
<script>
|
|
928
|
+
import EmailTemplates from '~/collections/EmailTemplates'
|
|
929
|
+
|
|
930
|
+
export default {
|
|
931
|
+
middleware: 'auth',
|
|
932
|
+
|
|
933
|
+
data() {
|
|
934
|
+
return {
|
|
935
|
+
channelFilter: 'all',
|
|
936
|
+
templates: new EmailTemplates([], {
|
|
937
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
computed: {
|
|
943
|
+
shopUuid() {
|
|
944
|
+
return this.$route.params.shop_uuid
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
methods: {
|
|
949
|
+
editTemplate(template) {
|
|
950
|
+
this.$router.push({
|
|
951
|
+
name: 'templates-edit',
|
|
952
|
+
params: {
|
|
953
|
+
shop_uuid: this.shopUuid,
|
|
954
|
+
id: template.id
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
},
|
|
958
|
+
|
|
959
|
+
async duplicateTemplate(template) {
|
|
960
|
+
try {
|
|
961
|
+
await this.$axios.post(`/api/templates/${template.id}/duplicate`)
|
|
962
|
+
this.$notify({
|
|
963
|
+
message: 'Template duplicated successfully',
|
|
964
|
+
type: 'success'
|
|
965
|
+
})
|
|
966
|
+
this.templates.fetch()
|
|
967
|
+
} catch (error) {
|
|
968
|
+
this.$notify({
|
|
969
|
+
message: 'Failed to duplicate template',
|
|
970
|
+
type: 'error'
|
|
971
|
+
})
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
async toggleActive(template) {
|
|
976
|
+
try {
|
|
977
|
+
await this.$axios.patch(`/api/templates/${template.id}`, {
|
|
978
|
+
is_active: !template.is_active
|
|
979
|
+
})
|
|
980
|
+
this.$notify({
|
|
981
|
+
message: `Template ${template.is_active ? 'deactivated' : 'activated'}`,
|
|
982
|
+
type: 'success'
|
|
983
|
+
})
|
|
984
|
+
this.templates.fetch()
|
|
985
|
+
} catch (error) {
|
|
986
|
+
this.$notify({
|
|
987
|
+
message: 'Failed to update template',
|
|
988
|
+
type: 'error'
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
async deleteTemplate(template) {
|
|
994
|
+
const confirmed = await this.$confirm({
|
|
995
|
+
title: 'Delete Template',
|
|
996
|
+
message: `Are you sure you want to delete "${template.name}"?`
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
if (!confirmed) return
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
await this.$axios.delete(`/api/templates/${template.id}`)
|
|
1003
|
+
this.$notify({
|
|
1004
|
+
message: 'Template deleted successfully',
|
|
1005
|
+
type: 'success'
|
|
1006
|
+
})
|
|
1007
|
+
this.templates.fetch()
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
this.$notify({
|
|
1010
|
+
message: 'Failed to delete template',
|
|
1011
|
+
type: 'error'
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
importTemplates() {
|
|
1017
|
+
// Import logic
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
</script>
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
## Best Practices
|
|
1025
|
+
|
|
1026
|
+
### 1. Collection Initialization
|
|
1027
|
+
|
|
1028
|
+
✅ **Always include shop_uuid in options:**
|
|
1029
|
+
```javascript
|
|
1030
|
+
customers: new Customers([], {
|
|
1031
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
1032
|
+
})
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### 2. No Manual Fetching
|
|
1036
|
+
|
|
1037
|
+
✅ **Let AwTableBuilder handle fetching:**
|
|
1038
|
+
```markup
|
|
1039
|
+
<!-- ✅ GOOD -->
|
|
1040
|
+
<AwTableBuilder :collection="customers" />
|
|
1041
|
+
|
|
1042
|
+
<!-- ❌ BAD - Don't fetch manually -->
|
|
1043
|
+
<script>
|
|
1044
|
+
async mounted() {
|
|
1045
|
+
await this.customers.fetch() // Unnecessary!
|
|
1046
|
+
}
|
|
1047
|
+
</script>
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### 3. Loading States
|
|
1051
|
+
|
|
1052
|
+
✅ **Don't wrap in v-if:**
|
|
1053
|
+
```markup
|
|
1054
|
+
<!-- ✅ GOOD - Component handles loading -->
|
|
1055
|
+
<AwTableBuilder :collection="customers" />
|
|
1056
|
+
|
|
1057
|
+
<!-- ❌ BAD - Prevents mounting -->
|
|
1058
|
+
<AwTableBuilder v-if="!loading" :collection="customers" />
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### 4. Date Formatting
|
|
1062
|
+
|
|
1063
|
+
✅ **Always use $dayjs:**
|
|
1064
|
+
```markup
|
|
1065
|
+
<!-- ✅ GOOD -->
|
|
1066
|
+
{{ $dayjs(cell.created_at).format('ll') }}
|
|
1067
|
+
|
|
1068
|
+
<!-- ❌ BAD - Inconsistent formatting -->
|
|
1069
|
+
{{ new Date(cell.created_at).toLocaleDateString() }}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### 5. Confirmations for Destructive Actions
|
|
1073
|
+
|
|
1074
|
+
✅ **Always confirm deletes:**
|
|
1075
|
+
```javascript
|
|
1076
|
+
async deleteItem(item) {
|
|
1077
|
+
const confirmed = await this.$confirm({
|
|
1078
|
+
title: 'Delete Item',
|
|
1079
|
+
message: `Are you sure you want to delete "${item.name}"?`
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
if (!confirmed) return
|
|
1083
|
+
|
|
1084
|
+
// Proceed with deletion
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
## See Also
|
|
1089
|
+
|
|
1090
|
+
- [Detail Pages](./detail-pages.md) - Create and edit pages
|
|
1091
|
+
- [Dashboard Pages](./dashboard-pages.md) - Metrics and overview pages
|
|
1092
|
+
- [AwTableBuilder](../../components/organisms/aw-table-builder.md) - Table component reference
|
|
1093
|
+
- [AwPage](../../components/pages/aw-page.md) - Page component reference
|
|
1094
|
+
- [Data Fetching Guide](../data-fetching.md) - Working with collections
|