@xy-planning-network/trees 0.4.0-rc-8 → 0.4.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/README.md +234 -41
- package/dist/trees.es.js +224 -217
- package/dist/trees.umd.js +4 -4
- package/package.json +4 -2
- package/src/lib-components/forms/BaseInput.vue +83 -0
- package/src/lib-components/forms/Checkbox.vue +46 -0
- package/src/lib-components/forms/DateRangePicker.vue +65 -0
- package/src/lib-components/forms/InputHelp.vue +24 -0
- package/src/lib-components/forms/InputLabel.vue +23 -0
- package/src/lib-components/forms/MultiCheckboxes.vue +55 -0
- package/src/lib-components/forms/Radio.vue +58 -0
- package/src/lib-components/forms/Select.vue +65 -0
- package/src/lib-components/forms/TextArea.vue +50 -0
- package/src/lib-components/forms/Toggle.vue +25 -0
- package/src/lib-components/forms/YesOrNoRadio.vue +70 -0
- package/src/lib-components/layout/DateFilter.vue +54 -0
- package/src/lib-components/layout/SidebarLayout.vue +239 -0
- package/src/lib-components/layout/StackedLayout.vue +172 -0
- package/src/lib-components/lists/Cards.vue +33 -0
- package/src/lib-components/lists/DetailList.vue +114 -0
- package/src/lib-components/lists/DownloadCell.vue +12 -0
- package/src/lib-components/lists/StaticTable.vue +83 -0
- package/src/lib-components/lists/Table.vue +291 -0
- package/src/lib-components/navigation/ActionsDropdown.vue +78 -0
- package/src/lib-components/navigation/Paginator.vue +115 -0
- package/src/lib-components/navigation/Steps.vue +83 -0
- package/src/lib-components/navigation/Tabs.vue +92 -0
- package/src/lib-components/overlays/ContentModal.vue +95 -0
- package/src/lib-components/overlays/Flash.vue +131 -0
- package/src/lib-components/overlays/Modal.vue +133 -0
- package/src/lib-components/overlays/Slideover.vue +87 -0
- package/src/lib-components/overlays/Spinner.vue +149 -0
- package/types/composables/nav.d.ts +2 -2
- package/types/composables/table.d.ts +2 -2
- package/types/composables/user.d.ts +1 -4
- package/types/lib-components/forms/Select.vue.d.ts +2 -2
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Flash from "../overlays/Flash.vue"
|
|
3
|
+
import Spinner from "../overlays/Spinner.vue"
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogOverlay,
|
|
7
|
+
Menu,
|
|
8
|
+
MenuButton,
|
|
9
|
+
MenuItem,
|
|
10
|
+
MenuItems,
|
|
11
|
+
TransitionChild,
|
|
12
|
+
TransitionRoot,
|
|
13
|
+
} from "@headlessui/vue"
|
|
14
|
+
import { MenuAlt2Icon, XIcon } from "@heroicons/vue/outline"
|
|
15
|
+
import { CogIcon } from "@heroicons/vue/solid"
|
|
16
|
+
import * as NavTypes from "@/composables/nav"
|
|
17
|
+
import { ref } from "vue"
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(
|
|
20
|
+
defineProps<{
|
|
21
|
+
activeURL?: string
|
|
22
|
+
iconURL: string
|
|
23
|
+
navigation: NavTypes.Item[]
|
|
24
|
+
userNavigation: NavTypes.Item[]
|
|
25
|
+
}>(),
|
|
26
|
+
{
|
|
27
|
+
activeURL: "",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const sidebarOpen = ref<boolean>(false)
|
|
32
|
+
|
|
33
|
+
const isActive = (url: string): boolean => {
|
|
34
|
+
return props.activeURL === url
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
<template>
|
|
38
|
+
<div class="h-screen flex overflow-hidden bg-gray-100">
|
|
39
|
+
<TransitionRoot as="template" :show="sidebarOpen">
|
|
40
|
+
<Dialog
|
|
41
|
+
as="div"
|
|
42
|
+
static
|
|
43
|
+
class="fixed inset-0 flex z-40 md:hidden"
|
|
44
|
+
@close="sidebarOpen = false"
|
|
45
|
+
:open="sidebarOpen"
|
|
46
|
+
>
|
|
47
|
+
<TransitionChild
|
|
48
|
+
as="template"
|
|
49
|
+
enter="transition-opacity ease-linear duration-300"
|
|
50
|
+
enter-from="opacity-0"
|
|
51
|
+
enter-to="opacity-100"
|
|
52
|
+
leave="transition-opacity ease-linear duration-300"
|
|
53
|
+
leave-from="opacity-100"
|
|
54
|
+
leave-to="opacity-0"
|
|
55
|
+
>
|
|
56
|
+
<DialogOverlay class="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
|
57
|
+
</TransitionChild>
|
|
58
|
+
<TransitionChild
|
|
59
|
+
as="template"
|
|
60
|
+
enter="transition ease-in-out duration-300 transform"
|
|
61
|
+
enter-from="-translate-x-full"
|
|
62
|
+
enter-to="translate-x-0"
|
|
63
|
+
leave="transition ease-in-out duration-300 transform"
|
|
64
|
+
leave-from="translate-x-0"
|
|
65
|
+
leave-to="-translate-x-full"
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
class="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-white"
|
|
69
|
+
>
|
|
70
|
+
<TransitionChild
|
|
71
|
+
as="template"
|
|
72
|
+
enter="ease-in-out duration-300"
|
|
73
|
+
enter-from="opacity-0"
|
|
74
|
+
enter-to="opacity-100"
|
|
75
|
+
leave="ease-in-out duration-300"
|
|
76
|
+
leave-from="opacity-100"
|
|
77
|
+
leave-to="opacity-0"
|
|
78
|
+
>
|
|
79
|
+
<div class="absolute top-0 right-0 -mr-12 pt-2">
|
|
80
|
+
<button
|
|
81
|
+
class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
|
82
|
+
@click="sidebarOpen = false"
|
|
83
|
+
>
|
|
84
|
+
<span class="sr-only">Close sidebar</span>
|
|
85
|
+
<XIcon class="h-6 w-6 text-white" aria-hidden="true" />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</TransitionChild>
|
|
89
|
+
<div class="flex-shrink-0 flex justify-center px-4">
|
|
90
|
+
<img class="w-auto h-12" :src="iconURL" alt="Logo" />
|
|
91
|
+
</div>
|
|
92
|
+
<div class="mt-5 flex-1 h-0 overflow-y-auto">
|
|
93
|
+
<nav class="px-2 space-y-1">
|
|
94
|
+
<a
|
|
95
|
+
v-for="item in navigation"
|
|
96
|
+
:key="item.name"
|
|
97
|
+
:href="item.url"
|
|
98
|
+
:class="[
|
|
99
|
+
isActive(item.url)
|
|
100
|
+
? 'bg-gray-100 text-gray-900'
|
|
101
|
+
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900',
|
|
102
|
+
'group flex items-center px-2 py-2 text-base font-medium rounded-md',
|
|
103
|
+
]"
|
|
104
|
+
:target="item.openInTab ? '_blank' : '_self'"
|
|
105
|
+
>
|
|
106
|
+
<component
|
|
107
|
+
:is="item.icon"
|
|
108
|
+
:class="[
|
|
109
|
+
isActive(item.url)
|
|
110
|
+
? 'text-gray-600'
|
|
111
|
+
: 'text-gray-500 group-hover:text-gray-600',
|
|
112
|
+
'mr-4 h-6 w-6',
|
|
113
|
+
]"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
/>
|
|
116
|
+
{{ item.name }}
|
|
117
|
+
</a>
|
|
118
|
+
</nav>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</TransitionChild>
|
|
122
|
+
<div class="flex-shrink-0 w-14" aria-hidden="true">
|
|
123
|
+
<!-- Dummy element to force sidebar to shrink to fit close icon -->
|
|
124
|
+
</div>
|
|
125
|
+
</Dialog>
|
|
126
|
+
</TransitionRoot>
|
|
127
|
+
|
|
128
|
+
<!-- Static sidebar for desktop -->
|
|
129
|
+
<div class="hidden md:flex md:flex-shrink-0">
|
|
130
|
+
<div class="flex flex-col w-64">
|
|
131
|
+
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
|
132
|
+
<div
|
|
133
|
+
class="flex flex-col flex-grow border-r border-gray-200 pt-5 pb-4 bg-white overflow-y-auto"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex items-center flex-shrink-0 px-4">
|
|
136
|
+
<img class="w-auto h-12" :src="iconURL" alt="Logo" />
|
|
137
|
+
</div>
|
|
138
|
+
<div class="mt-5 flex-grow flex flex-col">
|
|
139
|
+
<nav class="flex-1 px-2 bg-white space-y-1">
|
|
140
|
+
<a
|
|
141
|
+
v-for="item in navigation"
|
|
142
|
+
:key="item.name"
|
|
143
|
+
:href="item.url"
|
|
144
|
+
:class="[
|
|
145
|
+
isActive(item.url)
|
|
146
|
+
? 'bg-gray-100 text-gray-900'
|
|
147
|
+
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900',
|
|
148
|
+
'group flex items-center px-2 py-2 text-sm font-medium rounded-md',
|
|
149
|
+
]"
|
|
150
|
+
:target="item.openInTab ? '_blank' : '_self'"
|
|
151
|
+
>
|
|
152
|
+
<component
|
|
153
|
+
:is="item.icon"
|
|
154
|
+
:class="[
|
|
155
|
+
isActive(item.url)
|
|
156
|
+
? 'text-gray-600'
|
|
157
|
+
: 'text-gray-500 group-hover:text-gray-600',
|
|
158
|
+
'mr-3 h-6 w-6',
|
|
159
|
+
]"
|
|
160
|
+
aria-hidden="true"
|
|
161
|
+
/>
|
|
162
|
+
{{ item.name }}
|
|
163
|
+
</a>
|
|
164
|
+
</nav>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="flex flex-col w-0 flex-1 overflow-hidden">
|
|
170
|
+
<div class="relative z-10 flex-shrink-0 flex h-16 bg-xy-blue shadow">
|
|
171
|
+
<button
|
|
172
|
+
class="px-4 border-r border-gray-200 text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 md:hidden"
|
|
173
|
+
@click="sidebarOpen = true"
|
|
174
|
+
>
|
|
175
|
+
<span class="sr-only">Open sidebar</span>
|
|
176
|
+
<MenuAlt2Icon class="h-6 w-6" aria-hidden="true" />
|
|
177
|
+
</button>
|
|
178
|
+
<div class="flex-1 px-4 flex justify-between">
|
|
179
|
+
<div class="flex-1 flex">
|
|
180
|
+
<h1 class="flex items-center text-2xl text-white">
|
|
181
|
+
<slot name="header"></slot>
|
|
182
|
+
</h1>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="ml-4 flex items-center md:ml-6">
|
|
185
|
+
<!-- Profile dropdown -->
|
|
186
|
+
<Menu as="div" class="ml-3 relative">
|
|
187
|
+
<div>
|
|
188
|
+
<MenuButton
|
|
189
|
+
class="max-w-xs flex items-center text-sm text-white rounded-full hover:bg-blue-500 hover:text-gray-200 focus:outline-none focus:ring focus:text-white"
|
|
190
|
+
>
|
|
191
|
+
<span class="sr-only">Open user menu</span>
|
|
192
|
+
<CogIcon class="h-8 w-8" fill="currentColor" />
|
|
193
|
+
</MenuButton>
|
|
194
|
+
</div>
|
|
195
|
+
<transition
|
|
196
|
+
enter-active-class="transition ease-out duration-100"
|
|
197
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
198
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
199
|
+
leave-active-class="transition ease-in duration-75"
|
|
200
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
201
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
202
|
+
>
|
|
203
|
+
<MenuItems
|
|
204
|
+
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
|
205
|
+
>
|
|
206
|
+
<MenuItem
|
|
207
|
+
v-for="item in userNavigation"
|
|
208
|
+
:key="item.name"
|
|
209
|
+
v-slot="{ active }"
|
|
210
|
+
>
|
|
211
|
+
<a
|
|
212
|
+
:href="item.url"
|
|
213
|
+
:class="[
|
|
214
|
+
active ? 'bg-gray-100' : '',
|
|
215
|
+
'block px-4 py-2 text-sm text-gray-700 font-semibold',
|
|
216
|
+
]"
|
|
217
|
+
>{{ item.name }}</a
|
|
218
|
+
>
|
|
219
|
+
</MenuItem>
|
|
220
|
+
</MenuItems>
|
|
221
|
+
</transition>
|
|
222
|
+
</Menu>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<main class="flex-1 relative overflow-y-auto focus:outline-none">
|
|
228
|
+
<div class="mx-auto">
|
|
229
|
+
<!-- Replace with your content -->
|
|
230
|
+
<slot></slot>
|
|
231
|
+
<!-- /End replace -->
|
|
232
|
+
</div>
|
|
233
|
+
</main>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<Flash />
|
|
238
|
+
<Spinner />
|
|
239
|
+
</template>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Flash from "../overlays/Flash.vue"
|
|
3
|
+
import Spinner from "../overlays/Spinner.vue"
|
|
4
|
+
import {
|
|
5
|
+
Disclosure,
|
|
6
|
+
DisclosureButton,
|
|
7
|
+
DisclosurePanel,
|
|
8
|
+
Menu,
|
|
9
|
+
MenuButton,
|
|
10
|
+
MenuItem,
|
|
11
|
+
MenuItems,
|
|
12
|
+
} from "@headlessui/vue"
|
|
13
|
+
import { MenuIcon, UserCircleIcon, XIcon } from "@heroicons/vue/outline"
|
|
14
|
+
import * as NavTypes from "@/composables/nav"
|
|
15
|
+
import User from "@/composables/user"
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
activeURL?: string
|
|
20
|
+
currentUser: User
|
|
21
|
+
iconURL: string
|
|
22
|
+
navigation: NavTypes.Item[]
|
|
23
|
+
userNavigation: NavTypes.Item[]
|
|
24
|
+
}>(),
|
|
25
|
+
{
|
|
26
|
+
activeURL: "",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
const isActive = (url: string): boolean => {
|
|
30
|
+
return props.activeURL === url
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
<template>
|
|
34
|
+
<div class="min-h-screen bg-gray-100">
|
|
35
|
+
<Disclosure as="nav" class="bg-white shadow-sm" v-slot="{ open }">
|
|
36
|
+
<div class="mx-auto px-4 sm:px-6 lg:px-8">
|
|
37
|
+
<div class="flex justify-between h-16">
|
|
38
|
+
<div class="flex">
|
|
39
|
+
<div class="flex-shrink-0 flex items-center">
|
|
40
|
+
<img class="block h-8 w-auto" :src="iconURL" alt="XY Trees" />
|
|
41
|
+
</div>
|
|
42
|
+
<div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
|
|
43
|
+
<a
|
|
44
|
+
v-for="item in navigation"
|
|
45
|
+
:key="item.name"
|
|
46
|
+
:href="item.url"
|
|
47
|
+
:class="[
|
|
48
|
+
isActive(item.url)
|
|
49
|
+
? 'border-blue-500 text-gray-900'
|
|
50
|
+
: 'border-transparent text-gray-700 hover:text-gray-900 hover:border-blue-500',
|
|
51
|
+
'inline-flex items-center px-1 pt-1 border-b-2 text-sm font-semibold',
|
|
52
|
+
]"
|
|
53
|
+
:aria-current="isActive(item.url) ? 'page' : undefined"
|
|
54
|
+
>{{ item.name }}</a
|
|
55
|
+
>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="hidden sm:ml-6 sm:flex sm:items-center">
|
|
59
|
+
<!-- Profile dropdown -->
|
|
60
|
+
<Menu as="div" class="ml-3 relative">
|
|
61
|
+
<div>
|
|
62
|
+
<MenuButton
|
|
63
|
+
class="bg-white flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
64
|
+
>
|
|
65
|
+
<span class="sr-only">Open user menu</span>
|
|
66
|
+
<UserCircleIcon class="text-gray-500 h-8 w-8 rounded-full" />
|
|
67
|
+
</MenuButton>
|
|
68
|
+
</div>
|
|
69
|
+
<transition
|
|
70
|
+
enter-active-class="transition ease-out duration-200"
|
|
71
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
72
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
73
|
+
leave-active-class="transition ease-in duration-75"
|
|
74
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
75
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
76
|
+
>
|
|
77
|
+
<MenuItems
|
|
78
|
+
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
|
79
|
+
>
|
|
80
|
+
<MenuItem
|
|
81
|
+
v-for="item in userNavigation"
|
|
82
|
+
:key="item.name"
|
|
83
|
+
v-slot="{ active }"
|
|
84
|
+
>
|
|
85
|
+
<a
|
|
86
|
+
:href="item.url"
|
|
87
|
+
:class="[
|
|
88
|
+
active ? 'bg-gray-100' : '',
|
|
89
|
+
'block px-4 py-2 text-sm text-gray-700 font-semibold',
|
|
90
|
+
]"
|
|
91
|
+
>{{ item.name }}</a
|
|
92
|
+
>
|
|
93
|
+
</MenuItem>
|
|
94
|
+
</MenuItems>
|
|
95
|
+
</transition>
|
|
96
|
+
</Menu>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="-mr-2 flex items-center sm:hidden">
|
|
99
|
+
<!-- Mobile menu button -->
|
|
100
|
+
<DisclosureButton
|
|
101
|
+
class="bg-white inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
102
|
+
>
|
|
103
|
+
<span class="sr-only">Open main menu</span>
|
|
104
|
+
<MenuIcon v-if="!open" class="block h-6 w-6" aria-hidden="true" />
|
|
105
|
+
<XIcon v-else class="block h-6 w-6" aria-hidden="true" />
|
|
106
|
+
</DisclosureButton>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<DisclosurePanel class="sm:hidden">
|
|
112
|
+
<div class="pt-2 pb-3 space-y-1">
|
|
113
|
+
<a
|
|
114
|
+
v-for="item in navigation"
|
|
115
|
+
:key="item.name"
|
|
116
|
+
:href="item.url"
|
|
117
|
+
:class="[
|
|
118
|
+
isActive(item.url)
|
|
119
|
+
? 'bg-blue-50 border-blue-500 text-blue-700'
|
|
120
|
+
: 'border-transparent text-gray-700 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-900',
|
|
121
|
+
'block pl-3 pr-4 py-2 border-l-4 text-base font-semibold',
|
|
122
|
+
]"
|
|
123
|
+
:aria-current="isActive(item.url) ? 'page' : undefined"
|
|
124
|
+
>{{ item.name }}</a
|
|
125
|
+
>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="pt-4 pb-3 border-t border-gray-200">
|
|
128
|
+
<div class="flex items-center px-4">
|
|
129
|
+
<div class="flex-shrink-0">
|
|
130
|
+
<UserCircleIcon class="text-gray-500 h-10 w-10 rounded-full" />
|
|
131
|
+
</div>
|
|
132
|
+
<div class="ml-3">
|
|
133
|
+
<div
|
|
134
|
+
class="text-base font-medium text-gray-800"
|
|
135
|
+
v-if="currentUser.name"
|
|
136
|
+
v-text="currentUser.name"
|
|
137
|
+
></div>
|
|
138
|
+
<div
|
|
139
|
+
class="text-sm font-medium text-gray-500"
|
|
140
|
+
v-text="currentUser.email"
|
|
141
|
+
></div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="mt-3 space-y-1">
|
|
145
|
+
<a
|
|
146
|
+
v-for="item in userNavigation"
|
|
147
|
+
:key="item.name"
|
|
148
|
+
:href="item.url"
|
|
149
|
+
class="block px-4 py-2 text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
|
150
|
+
>{{ item.name }}</a
|
|
151
|
+
>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</DisclosurePanel>
|
|
155
|
+
</Disclosure>
|
|
156
|
+
|
|
157
|
+
<slot name="header"></slot>
|
|
158
|
+
|
|
159
|
+
<main>
|
|
160
|
+
<div class="mx-auto sm:px-6 lg:px-8">
|
|
161
|
+
<!-- Replace with your content -->
|
|
162
|
+
<div class="px-4 py-8 sm:px-0">
|
|
163
|
+
<slot></slot>
|
|
164
|
+
</div>
|
|
165
|
+
<!-- /End replace -->
|
|
166
|
+
</div>
|
|
167
|
+
</main>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<Flash />
|
|
171
|
+
<Spinner />
|
|
172
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
cards: {
|
|
4
|
+
primary: string
|
|
5
|
+
secondary: string
|
|
6
|
+
}[]
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
<template>
|
|
10
|
+
<div
|
|
11
|
+
class="mt-5 grid grid-cols-1 gap-5"
|
|
12
|
+
:class="'lg:grid-cols-' + cards.length"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
class="bg-white overflow-hidden shadow rounded-lg"
|
|
16
|
+
v-for="(card, idx) in cards"
|
|
17
|
+
:key="idx"
|
|
18
|
+
>
|
|
19
|
+
<div class="px-4 py-5 sm:p-6 text-center">
|
|
20
|
+
<dl>
|
|
21
|
+
<dd
|
|
22
|
+
class="mt-1 text-3xl leading-9 font-semibold text-xy-blue"
|
|
23
|
+
v-text="card.primary"
|
|
24
|
+
></dd>
|
|
25
|
+
<dt
|
|
26
|
+
class="text-sm leading-5 font-medium text-gray-700 truncate"
|
|
27
|
+
v-text="card.secondary"
|
|
28
|
+
></dt>
|
|
29
|
+
</dl>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from "vue"
|
|
3
|
+
import BaseAPI from "../../api/base"
|
|
4
|
+
import DateFilter from "../layout/DateFilter.vue"
|
|
5
|
+
import Paginator from "../navigation/Paginator.vue"
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
refreshTrigger?: number
|
|
10
|
+
reloadTrigger?: number
|
|
11
|
+
title: string
|
|
12
|
+
url: string
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
refreshTrigger: 0,
|
|
16
|
+
reloadTrigger: 0,
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const dateRange = ref<{ minDate: number; maxDate: number }>({
|
|
21
|
+
minDate: 0,
|
|
22
|
+
maxDate: 0,
|
|
23
|
+
})
|
|
24
|
+
const hasContent = ref(true)
|
|
25
|
+
const items = ref<any[]>([])
|
|
26
|
+
const pagination = ref({
|
|
27
|
+
page: 1,
|
|
28
|
+
perPage: 10,
|
|
29
|
+
totalItems: 0,
|
|
30
|
+
totalPages: 0,
|
|
31
|
+
})
|
|
32
|
+
const sortDir = ref("DESC")
|
|
33
|
+
|
|
34
|
+
const loadAndRender = (checkForContent: boolean): void => {
|
|
35
|
+
const params = {
|
|
36
|
+
page: pagination.value.page,
|
|
37
|
+
perPage: pagination.value.perPage,
|
|
38
|
+
sortDir: sortDir.value,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BaseAPI.get(props.url, {}, Object.assign(params, dateRange.value)).then(
|
|
42
|
+
(success: any) => {
|
|
43
|
+
pagination.value = {
|
|
44
|
+
page: success.data.page,
|
|
45
|
+
perPage: success.data.perPage,
|
|
46
|
+
totalItems: success.data.totalItems,
|
|
47
|
+
totalPages: success.data.totalPages,
|
|
48
|
+
}
|
|
49
|
+
items.value = success.data.items
|
|
50
|
+
if (checkForContent) hasContent.value = items.value.length != 0
|
|
51
|
+
},
|
|
52
|
+
() => {
|
|
53
|
+
// TODO: let's make this really generic or configurable
|
|
54
|
+
window.VueBus.emit(
|
|
55
|
+
"Flash-show-generic-error",
|
|
56
|
+
"archive@xyplanningnetwork.com"
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
watch([sortDir, dateRange], () => {
|
|
63
|
+
loadAndRender(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
watch(
|
|
67
|
+
() => props.refreshTrigger,
|
|
68
|
+
() => {
|
|
69
|
+
loadAndRender(false)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
watch(
|
|
74
|
+
() => props.reloadTrigger,
|
|
75
|
+
() => {
|
|
76
|
+
// This lets parent components trigger a refresh of the current page depending on external actions.
|
|
77
|
+
pagination.value.page = 1
|
|
78
|
+
loadAndRender(true)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
loadAndRender(true)
|
|
83
|
+
</script>
|
|
84
|
+
<template>
|
|
85
|
+
<div>
|
|
86
|
+
<DateFilter
|
|
87
|
+
:date-range="dateRange"
|
|
88
|
+
:sort-dir="sortDir"
|
|
89
|
+
:title="title"
|
|
90
|
+
@sort-dir-changed="sortDir = $event"
|
|
91
|
+
@date-range-changed="dateRange = $event"
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<div class="shadow overflow-hidden sm:rounded-md border" v-if="hasContent">
|
|
95
|
+
<ul>
|
|
96
|
+
<li
|
|
97
|
+
v-for="(item, idx) in items"
|
|
98
|
+
:key="idx"
|
|
99
|
+
:class="{ 'border-t border-gray-200': idx > 0 }"
|
|
100
|
+
>
|
|
101
|
+
<slot :item="item"></slot>
|
|
102
|
+
</li>
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<slot name="empty" v-else></slot>
|
|
107
|
+
|
|
108
|
+
<Paginator
|
|
109
|
+
v-model="pagination"
|
|
110
|
+
@update:modelValue="loadAndRender(false)"
|
|
111
|
+
v-if="hasContent"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { DownloadIcon } from "@heroicons/vue/solid"
|
|
3
|
+
defineProps<{
|
|
4
|
+
propsData: Record<string, unknown>
|
|
5
|
+
attribute: string
|
|
6
|
+
}>()
|
|
7
|
+
</script>
|
|
8
|
+
<template>
|
|
9
|
+
<a :href="propsData[attribute] as string">
|
|
10
|
+
<DownloadIcon class="h-6 w-6 group-hover:text-gray-500 transition" />
|
|
11
|
+
</a>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ComponentPublicInstance, getCurrentInstance } from "vue"
|
|
3
|
+
import * as TableTypes from "@/composables/table"
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
tableData: TableTypes.Static
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const cellValue = (
|
|
10
|
+
item: Record<string, any>,
|
|
11
|
+
col: TableTypes.Column
|
|
12
|
+
): string => {
|
|
13
|
+
if (col.key) {
|
|
14
|
+
return item[col.key]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (col.presenter) {
|
|
18
|
+
// TODO: discuss this pattern. Current usage can be replaced with modules.
|
|
19
|
+
// https://v3.vuejs.org/api/composition-api.html#getcurrentinstance
|
|
20
|
+
const internalInstance = getCurrentInstance()
|
|
21
|
+
return col.presenter(
|
|
22
|
+
item,
|
|
23
|
+
internalInstance?.proxy as ComponentPublicInstance
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return ""
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
<template>
|
|
31
|
+
<div class="flex flex-col">
|
|
32
|
+
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
33
|
+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
|
34
|
+
<div
|
|
35
|
+
class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg"
|
|
36
|
+
>
|
|
37
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
38
|
+
<thead>
|
|
39
|
+
<tr>
|
|
40
|
+
<th
|
|
41
|
+
class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-900 uppercase bg-gray-50 leading-4"
|
|
42
|
+
v-for="(col, idx) in tableData.columns"
|
|
43
|
+
:key="idx"
|
|
44
|
+
v-text="col.display"
|
|
45
|
+
></th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
49
|
+
<tr
|
|
50
|
+
v-for="(item, rowIdx) in tableData.items"
|
|
51
|
+
:key="item.id ? (item.id as string) : rowIdx"
|
|
52
|
+
>
|
|
53
|
+
<td
|
|
54
|
+
class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap leading-5"
|
|
55
|
+
v-for="(col, colIdx) in tableData.columns"
|
|
56
|
+
:key="rowIdx + '-' + colIdx"
|
|
57
|
+
>
|
|
58
|
+
<component
|
|
59
|
+
:is="col.component"
|
|
60
|
+
v-if="col.component"
|
|
61
|
+
:props-data="item"
|
|
62
|
+
:current-user="tableData.currentUser"
|
|
63
|
+
:attribute="col.key"
|
|
64
|
+
></component>
|
|
65
|
+
<span v-else v-text="cellValue(item, col)"></span>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
|
|
69
|
+
<tr v-if="tableData.items.length == 0">
|
|
70
|
+
<td
|
|
71
|
+
:colspan="tableData.columns.length"
|
|
72
|
+
class="px-6 py-4 text-sm text-gray-700 whitespace-nowrap leading-5"
|
|
73
|
+
>
|
|
74
|
+
No items were found!
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|