-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #25 from Dashibase/fix-router
Fix router, add sort and filter, multiple select and dark mode
- Loading branch information
Showing
40 changed files
with
1,681 additions
and
749 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,3 @@ | ||
<template> | ||
<router-view /> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import config from './dashibaseConfig' | ||
import { store } from './utils/store' | ||
import { Page } from './utils/config' | ||
import { isHostedByDashibase } from './utils/supabase' | ||
if (isHostedByDashibase) { | ||
store.appName = window.localStorage.getItem('dashibase.app_name') as string || 'Dashibase' | ||
store.pages = JSON.parse(window.localStorage.getItem('dashibase.pages') as string) as Page[] || [] | ||
} else { | ||
store.appName = config.name || 'Dashibase' | ||
store.pages = config.pages || [] | ||
} | ||
document.title = store.appName | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,40 @@ | ||
<template> | ||
<Dashboard /> | ||
<router-view /> | ||
<div class="z-50 absolute w-full top-0 bg-white transition-all duration-500" | ||
:class="store.dashboard.name ? 'opacity-0 pointer-events-none' : 'opacity-100'"> | ||
<Placeholder /> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import Dashboard from './dashboard/Dashboard.vue' | ||
import { useRoute } from 'vue-router' | ||
import { User as SupabaseUser } from '@supabase/supabase-js' | ||
import { supabase } from '@/utils/supabase' | ||
import { useStore } from '@/utils/store' | ||
import Placeholder from './dashboard/Placeholder.vue' | ||
const store = useStore() | ||
const route = useRoute() | ||
if (!store.user.id && !['/signin', '/signup'].includes(route.path)) window.location.href = '/signin' | ||
const intervalId = setInterval(() => { | ||
if (supabase) { | ||
clearInterval(intervalId) | ||
store.dashboard.supabaseAnonKey = window.localStorage.getItem('dashibase.supabase_anon_key') || '' | ||
store.dashboard.supabaseUrl = window.localStorage.getItem('dashibase.supabase_url') || '' | ||
store.dashboard.name = window.localStorage.getItem('dashibase.app_name') || '' | ||
store.dashboard.id = window.localStorage.getItem('dashibase.dashboard_id') || '' | ||
const user = supabase.auth.user() | ||
if (user) store.user = user | ||
supabase.auth.onAuthStateChange((_, session) => { | ||
store.user = session?.user as SupabaseUser | ||
}) | ||
if (!store.user.id) { | ||
if (!['/signin', '/signup'].includes(route.path)) window.location.href = '/signin' | ||
} | ||
} | ||
}, 100) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
<template> | ||
<h2 class="text-lg text-center font-bold text-gray-700 select-none">{{ store.appName }}</h2> | ||
<h2 class="text-lg text-center font-bold select-none transition" :class="store.darkMode ? 'text-neutral-300' : 'text-neutral-700'"> | ||
{{ store.dashboard.name }} | ||
</h2> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { store } from '../../utils/store' | ||
import { useStore } from '@/utils/store' | ||
const store = useStore() | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,11 @@ | ||
<template> | ||
<a class="text-gray-400 font-medium text-sm" href="https://dashibase.com"> | ||
<a class="font-medium text-sm" :class="store.darkMode ? 'text-neutral-500' : 'text-neutral-400'" href="https://dashibase.com"> | ||
Powered by Dashibase | ||
</a> | ||
</template> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { useStore } from '@/utils/store' | ||
const store = useStore() | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,92 @@ | ||
<template> | ||
<div class="w-full"> | ||
<div class="px-4 md:px-10 py-12 flex items-center"> | ||
<h1 class="text-2xl font-medium">{{ page.name }}</h1> | ||
<div class="px-4 md:px-10 py-12 flex flex-col sm:flex-row justify-between gap-4 sm:items-end"> | ||
<PageHeader>{{ page.name }}</PageHeader> | ||
<div class="flex gap-2"> | ||
<FilterMenu :attributes="page.attributes" @close="filterItems"/> | ||
<DeleteButton v-if="selected.length" @click="deleteCards"> | ||
Delete | ||
</DeleteButton> | ||
<PrimaryButton v-if="!selected.length" @click="createCard"> | ||
New | ||
</PrimaryButton> | ||
</div> | ||
</div> | ||
<!-- Warning --> | ||
<div v-if="warning" class="py-2 px-4 md:px-10 text-sm text-red-500"> | ||
{{ warning }} | ||
</div> | ||
<div class="px-4 md:px-10 grid grid-cols-1 md:grid-cols-2 gap-2"> | ||
<div class="px-4 md:px-10 grid grid-cols-1 md:grid-cols-2 gap-2" :class="store.darkMode ? 'text-neutral-200' : 'text-neutral-800'"> | ||
<!-- Cards --> | ||
<a v-for="(item, i) in items" :key="item.id" class="bg-white block relative hover:bg-gray-50 cursor-pointer border rounded px-2 py-1" :href="`/${page.page_id}/view/${item.id}`"> | ||
<div class="absolute top-0 left-0 w-full text-sm text-gray-300 text-right px-1"> | ||
#{{ i + 1 + ((paginationNum - 1) * maxItems) }} | ||
</div> | ||
<div class="flex flex-col gap-2 p-2"> | ||
<div v-for="attribute in page.attributes" :key="attribute.id" | ||
class="font-medium text-gray-900 flex flex-col gap-1"> | ||
<div class="text-gray-300 text-sm">{{ attribute.label }}</div> | ||
<div class="truncate">{{ item[attribute.id] }}</div> | ||
<button v-for="(item, i) in items" :key="item.id" class="text-left block relative cursor-pointer border rounded px-2 py-1" | ||
:class="store.darkMode ? 'border-neutral-700' : 'border-neutral-300'" | ||
@click.exact="router.push(`/${page.page_id}/view/${item.id}`)" | ||
@click.shift.left.exact="event => selectCard(i, event)"> | ||
<div class="flex flex-col gap-1 p-2"> | ||
<div class="font-medium text-lg flex items-center justify-between"> | ||
<div class="truncate">{{ item[page.attributes[0].id] }}</div> | ||
<input v-if="selected.length" type="checkbox" class="cursor-pointer focus:outline-none focus:ring-0 focus:ring-offset-0 focus:border-0 h-4 w-4 rounded text-neutral-700" | ||
:class="store.darkMode ? 'bg-neutral-900 border-neutral-600' : 'border-neutral-300'" :checked="selected.includes(i)" | ||
@click="event => selectCard(i, event)" /> | ||
</div> | ||
<div v-for="attribute in page.attributes.slice(1)" :key="attribute.id" | ||
class="flex flex-col"> | ||
<div class="text-xs" :class="store.darkMode ? 'text-neutral-700' : 'text-neutral-300'">{{ attribute.label }}</div> | ||
<div class="-mt-0.5 truncate text-sm">{{ item[attribute.id] }}</div> | ||
</div> | ||
</div> | ||
<div v-if="!page.readonly" class="absolute bottom-0 right-0 text-sm text-gray-300 hover:text-red-600 flex justify-end px-1 py-1" @click="event => deleteItem(item.id, event)"> | ||
<TrashIcon class="w-4 h-4 cursor-pointer" /> | ||
</div> | ||
</a> | ||
<!-- New Item --> | ||
<a v-if="!page.readonly" class="w-full border rounded px-4 py-3 text-gray-300 cursor-pointer hover:border-green-200 hover:bg-green-100 hover:text-green-400 flex justify-between" | ||
:href="`/${page.page_id}/new`"> | ||
<div class="font-medium">New item</div> | ||
<PlusIcon class="h-5" /> | ||
</a> | ||
</button> | ||
</div> | ||
<Pagination class="mt-10 px-10" :paginationList="paginationList" :maxPagination="maxPagination" v-model="paginationNum" /> | ||
<Pagination v-if="maxPagination > 1" class="mt-10 px-10" :paginationList="paginationList" :maxPagination="maxPagination" v-model="paginationNum" /> | ||
<DeleteModal ref="deleteModal" /> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { PropType } from 'vue' | ||
import { | ||
PlusIcon, | ||
TrashIcon, | ||
} from '@heroicons/vue/solid' | ||
import { Page } from '../../utils/config' | ||
import { initLoading, initCrud } from '../../utils/dashboard' | ||
import DropDown from './form-elements/DropDown.vue' | ||
import { ref, PropType } from 'vue' | ||
import router from '@/router' | ||
import { Page } from '@/utils/config' | ||
import { useStore } from '@/utils/store' | ||
import { initCrud } from '@/utils/dashboard' | ||
import Pagination from './Pagination.vue' | ||
import PageHeader from './elements/PageHeader.vue' | ||
import PrimaryButton from './elements/PrimaryButton.vue' | ||
import FilterMenu from './elements/FilterMenu.vue' | ||
import DeleteButton from './elements/DeleteButton.vue' | ||
import DeleteModal from './DeleteModal.vue' | ||
const store = useStore() | ||
const selected = ref([] as number[]) | ||
const props = defineProps({ | ||
loading: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
page: { | ||
type: Object as PropType<Page>, | ||
required: true, | ||
}, | ||
}) | ||
const deleteModal = ref<any|null>(null) | ||
const { items, page, warning, paginationNum, maxPagination, paginationList, deleteItems, filterItems } = initCrud(props.page) | ||
const maxItems = 10 | ||
function createCard () { | ||
router.push(`/${props.page.page_id}/new`) | ||
} | ||
const { loading } = initLoading(props.loading) | ||
const { page, warning, items, paginationNum, maxPagination, paginationList, getItems, deleteItem } = initCrud(loading, props.page, maxItems) | ||
function selectCard (idx:number, event:Event) { | ||
event.stopPropagation() | ||
if (!selected.value.includes(idx)) selected.value.push(idx) | ||
else selected.value.splice(selected.value.indexOf(idx), 1) | ||
} | ||
getItems() | ||
async function deleteCards () { | ||
if (!deleteModal.value) return | ||
deleteModal.value.title = 'Confirm deletion' | ||
deleteModal.value.message = `Are you sure you want to delete ${selected.value.length > 1 ? 'these rows' : 'this row'}?` | ||
const confirm = await deleteModal.value?.confirm() | ||
if (confirm) { | ||
deleteItems(selected.value.map((idx:number) => items.value[idx].id)) | ||
.then(() => selected.value = []) | ||
} | ||
} | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,89 @@ | ||
<template> | ||
<div class="space-y-6 sm:px-6 lg:px-0 w-full"> | ||
<div class="bg-white py-6 px-4 sm:p-6 mx-auto w-full"> | ||
<div class="px-10 py-12 flex items-center text-2xl font-medium gap-2"> | ||
<h1>{{ page.name }}</h1> | ||
<ChevronRightIcon class="inline text-gray-500 h-6 w-auto" /> | ||
<h1>New Item</h1> | ||
<div class="py-6 px-4 sm:p-6 mx-auto w-full"> | ||
<div class="px-4 md:px-10 py-12 flex justify-between items-end"> | ||
<PageHeader> | ||
<h1 class="cursor-pointer" @click="router.push(`/${page.page_id}`)">{{ page.name }}</h1> | ||
<ChevronRightIcon class="inline text-neutral-500 h-6 w-auto" /> | ||
<h1>New Item</h1> | ||
</PageHeader> | ||
</div> | ||
<div class="mt-6 flex flex-col gap-6" v-if="page.attributes"> | ||
<!-- Attribute Inputs --> | ||
<div v-for="attribute in page.attributes" :key="attribute.id" | ||
class="px-4 md:px-10"> | ||
<label :for="attribute.id" class="block text-sm font-medium text-gray-700">{{ attribute.label }} <span v-if="attribute.required" class="text-gray-400 font-normal pl-2">required</span></label> | ||
<input v-if="attribute.type === AttributeType.Date" type="date" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" /> | ||
<select v-else-if="attribute.type === AttributeType.Bool" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm"> | ||
<option :value="true">true</option> | ||
<option :value="false">false</option> | ||
</select> | ||
<select v-else-if="attribute.type === AttributeType.Enum" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm"> | ||
class="px-4 md:px-10 transition" :class="store.darkMode ? 'text-neutral-200' : 'text-neutral-800'"> | ||
<label :for="attribute.id" class="block text-sm font-medium transition" :class="store.darkMode ? 'text-neutral-400' : 'text-neutral-600'"> | ||
{{ attribute.label }} | ||
<span v-if="attribute.required" class="font-normal pl-2 transition" :class="store.darkMode ? 'text-neutral-600' : 'text-neutral-400'">required</span> | ||
</label> | ||
<input v-if="attribute.type === AttributeType.Date" type="date" :disabled="store.loading" :id="attribute.id" | ||
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition" | ||
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'" | ||
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" /> | ||
<Toggle v-else-if="attribute.type === AttributeType.Bool" class="mt-1" :modelValue="item[attribute.id] ? item[attribute.id] : false" | ||
@update:modelValue="(value:any) => update(attribute.id, value)" /> | ||
<select v-else-if="attribute.type === AttributeType.Enum" :disabled="store.loading" :id="attribute.id" | ||
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm cursor-pointer transition" | ||
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'" | ||
@input="update(attribute.id, ($event.target as HTMLInputElement).value)"> | ||
<option v-for="option in attribute.enumOptions" :key="option" :value="option">{{ option }}</option> | ||
</select> | ||
<textarea v-else-if="attribute.type === AttributeType.LongText" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" /> | ||
<input v-else type="text" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" /> | ||
<textarea v-else-if="attribute.type === AttributeType.LongText" :disabled="store.loading" :id="attribute.id" | ||
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition" | ||
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'" | ||
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" /> | ||
<input v-else type="text" :disabled="store.loading" :id="attribute.id" | ||
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition" | ||
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'" | ||
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" /> | ||
</div> | ||
<!-- Warning --> | ||
<div v-if="warning" class="px-4 md:px-10 text-sm text-red-500"> | ||
{{ warning }} | ||
</div> | ||
<!-- Buttons --> | ||
<div class="px-4 md:px-10 flex justify-end gap-4"> | ||
<button :disabled="loading" class="bg-white border border-transparent rounded-md py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500" | ||
@click="router.go(-1)"> | ||
<TertiaryButton :disabled="store.loading" @click="router.go(-1)"> | ||
Back | ||
</button> | ||
<button :disabled="loading" class="bg-green-500 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" | ||
@click="createItem"> | ||
{{ loading ? 'Loading...' : 'Save' }} | ||
</button> | ||
</TertiaryButton> | ||
<PrimaryButton :disabled="store.loading" @click="createItem(item)"> | ||
Save | ||
</PrimaryButton> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { PropType } from 'vue' | ||
import { ref, computed } from 'vue' | ||
import { ChevronRightIcon } from '@heroicons/vue/solid' | ||
import router from '../../router' | ||
import { initLoading, initCrud } from '../../utils/dashboard' | ||
import { Page, AttributeType } from '../../utils/config' | ||
import router from '@/router' | ||
import { useStore } from '@/utils/store' | ||
import { initCrud } from '@/utils/dashboard' | ||
import { Page, AttributeType } from '@/utils/config' | ||
import PageHeader from './elements/PageHeader.vue' | ||
import TertiaryButton from './elements/TertiaryButton.vue' | ||
import PrimaryButton from './elements/PrimaryButton.vue' | ||
const store = useStore() | ||
const props = defineProps({ | ||
loading: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
page: { | ||
type: Object as PropType<Page>, | ||
pageId: { | ||
type: String, | ||
required: true, | ||
}, | ||
}) | ||
const { loading } = initLoading(props.loading) | ||
const { page, warning, createItem } = initCrud(loading, props.page) | ||
const page = computed(():Page => { | ||
return store.dashboard.pages.find(page => page.page_id === props.pageId) || {} as Page | ||
}) | ||
const item = ref({} as {[k:string]:any}) | ||
function update (attributeId:string, newVal:any) { | ||
item.value[attributeId] = newVal | ||
} | ||
const { warning, createItem } = initCrud(page.value) | ||
</script> |
Oops, something went wrong.