Skip to content

Commit

Permalink
refactor: list views with espresso designs
Browse files Browse the repository at this point in the history
  • Loading branch information
nextchamp-saqib committed Aug 31, 2023
1 parent fca8210 commit 28701ab
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 320 deletions.
6 changes: 6 additions & 0 deletions frontend/src/components/Icons/IndicatorIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect width="16" height="16" rx="4.5" class="currentColor fill-current" />
<circle cx="8" cy="8" r="3" fill="white" />
</svg>
</template>
170 changes: 33 additions & 137 deletions frontend/src/components/ListView.vue
Original file line number Diff line number Diff line change
@@ -1,158 +1,54 @@
<script setup>
import { List } from 'frappe-ui'
import { computed, ref } from 'vue'
import { ellipsis } from '@/utils'
import PageTitle from './PageTitle.vue'
const emit = defineEmits(['row-select'])
const props = defineProps({
title: { type: String },
actions: { type: Array },
filters: { type: Array },
columns: { type: Array },
data: { type: Array },
rowClick: { type: Function },
rows: { type: Array },
})
const searchTerm = ref('')
const filteredData = computed(() => {
if (searchTerm.value === '') {
return props.data.slice(0, 100)
}
return props.data
const searchQuery = ref('')
const filteredList = computed(() => {
if (!props.rows) return []
if (!searchQuery.value) return props.rows.slice(0, 100)
return props.rows
.filter((row) => {
return Object.values(row).some((value) => {
return String(value).toLowerCase().includes(searchTerm.value.toLowerCase())
return String(value).toLowerCase().includes(searchQuery.value.toLowerCase())
})
})
.slice(0, 100)
})
const selectedIndex = ref([])
function selectAll() {
if (selectedIndex.value.length === props.data.length) {
selectedIndex.value.splice(0, selectedIndex.value.length)
} else {
selectedIndex.value.splice(
0,
selectedIndex.value.length,
...props.data.map((_, index) => index)
)
}
}
function isSelected(row) {
return selectedIndex.value.includes(props.data.indexOf(row))
}
function toggleSelected(row) {
const index = props.data.indexOf(row)
if (isSelected(row)) {
selectedIndex.value.splice(selectedIndex.value.indexOf(index), 1)
} else {
selectedIndex.value.push(index)
}
}
</script>

<template>
<div class="flex h-full w-full flex-col overflow-hidden">
<!-- Title -->
<PageTitle v-if="title" :title="title" :actions="actions">
<slot name="title-items"></slot>
</PageTitle>

<div class="flex flex-1 flex-col overflow-hidden">
<!-- Filter Bar -->
<div class="my-2 flex justify-between">
<div class="flex space-x-4">
<Input
ref="searchInput"
v-model="searchTerm"
iconLeft="search"
placeholder="Search..."
/>
</div>
<!-- Enable after grid feature -->
<!-- <div class="flex items-center space-x-1 rounded bg-gray-100 p-1">
<div class="cursor-pointer rounded px-2 py-1 transition-all">
<FeatherIcon name="grid" class="h-4 w-4" />
</div>
<div class="cursor-pointer rounded bg-white px-2 py-1 shadow transition-all">
<FeatherIcon name="list" class="h-4 w-4" />
</div>
</div> -->
<div class="flex px-6 py-0.5">
<div class="flex space-x-2">
<Input
placeholder="Search"
icon-left="search"
:value="searchQuery"
:debounce="500"
@input="searchQuery = $event"
/>
<slot name="actions" />
</div>
</div>
<div class="flex flex-1 overflow-hidden bg-white px-4 py-2">
<List :columns="props.columns" :rows="filteredList">
<template #list-row="{ row }">
<slot name="list-row" :row="row" />
</template>
</List>

<!-- Data -->
<ul
v-if="props.data.length > 0"
class="relative flex flex-1 flex-col overflow-y-scroll text-lg"
<div
v-if="props.rows?.length == 0"
class="flex h-full w-full flex-col items-center justify-center"
>
<li
class="sticky top-0 z-10 flex items-center gap-4 border-b bg-white text-gray-500"
>
<div>
<Input
type="checkbox"
class="rounded border-gray-300"
@click.prevent.stop="selectAll"
/>
</div>
<div
v-for="(column, idx) in columns"
:key="column.label"
class="py-4 text-left font-normal"
:class="[column.class, idx === 0 ? 'w-[30%]' : 'flex-1']"
scope="col"
>
<component :is="column.headerComponent || 'span'">
{{ column.label }}
</component>
</div>
</li>
<li
v-for="row in filteredData"
:key="row[columns[0].key]"
class="flex items-center gap-4 border-b text-gray-600"
:class="props.rowClick ? 'cursor-pointer hover:bg-gray-50' : ''"
@click="props.rowClick && props.rowClick(row)"
>
<div>
<Input
type="checkbox"
class="rounded border-gray-300"
:checked="isSelected(row)"
@click.stop="toggleSelected(row)"
/>
</div>
<div
v-for="(column, idx) in columns"
:key="column.label"
class="overflow-hidden text-ellipsis whitespace-nowrap py-4"
:class="[idx === 0 ? 'w-[30%]' : 'flex-1']"
>
<component
:is="column.cellComponent || 'span'"
:row="row"
:class="[idx === 0 ? 'font-medium text-gray-700' : '']"
>
{{ ellipsis(row[column.key], 80) }}
</component>
</div>
</li>

<li
class="sticky bottom-0 right-0 flex w-full flex-1 flex-shrink-0 items-end bg-white"
>
<div class="mb-3 flex w-full justify-end border-t py-3 text-lg text-gray-600">
<p>Showing {{ filteredData.length }} of {{ props.data.length }} results</p>
</div>
</li>
</ul>

<div v-else class="flex-1 overflow-hidden">
<slot name="empty-state">
<div
class="flex h-full w-full flex-col items-center justify-center text-lg text-gray-500/50"
>
<FeatherIcon name="folder" class="h-12 w-12" />
<p class="mt-4">No data to display</p>
</div>
<slot name="emptyState">
<div class="text-xl font-medium">No data.</div>
<div class="mt-1 text-base text-gray-600">No data to display.</div>
</slot>
</div>
</div>
Expand Down
103 changes: 54 additions & 49 deletions frontend/src/datasource/DataSource.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,55 @@
]"
/>
</header>
<div class="flex flex-1 overflow-hidden bg-white px-6 py-2">
<ListView
v-if="dataSource.doc && dataSource.tableList.length"
:columns="columns"
:data="dataSource.tableList.filter((t) => !t.is_query_based)"
:rowClick="
({ name }) =>
router.push({
name: 'DataSourceTable',
params: { name: dataSource.doc.name, table: name },
})
"
>
<template #title-items>
<Badge theme="green">Active</Badge>
<Dropdown
placement="left"
:button="{ icon: 'more-horizontal', variant: 'ghost' }"
:options="dropdownActions"
/>
</template>
<template #empty-state>
<div
v-if="dataSource.tableList.length !== 0"
class="mt-2 flex h-full w-full flex-col items-center justify-center rounded text-base font-light text-gray-600"
>
<div class="text-base font-light text-gray-600">Tables are not synced yet.</div>
<div
class="cursor-pointer text-sm font-light text-blue-500 hover:underline"
@click="syncTables"
>
Sync Tables?
</div>
</div>
</template>
</ListView>
</div>

<ListView
:columns="[
{ label: 'Table', name: 'label' },
{ label: 'Status', name: 'status' },
]"
:rows="dataSource.tableList.filter((table) => !table.is_query_based)"
>
<template #actions>
<Dropdown
placement="left"
:button="{ icon: 'more-horizontal', variant: 'ghost' }"
:options="dropdownActions"
/>
</template>

<template #list-row="{ row: table }">
<ListRow
as="router-link"
:row="table"
:to="{
name: 'DataSourceTable',
params: { name: dataSource.doc.name, table: table.name },
}"
>
<ListRowItem> {{ table.label }} </ListRowItem>
<ListRowItem class="space-x-2">
<IndicatorIcon :class="table.hidden ? 'text-gray-500' : 'text-green-500'" />
<span> {{ table.hidden ? 'Disabled' : 'Enabled' }} </span>
</ListRowItem>
</ListRow>
</template>

<template #emptyState>
<div class="text-xl font-medium">No tables.</div>
<div class="mt-1 text-base text-gray-600">No tables to display.</div>
<Button class="mt-4" label="Sync Tables" variant="solid" @click="syncTables" />
</template>
</ListView>
</template>

<script setup lang="jsx">
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import ListView from '@/components/ListView.vue'
import PageBreadcrumbs from '@/components/PageBreadcrumbs.vue'
import useDataSource from '@/datasource/useDataSource'
import { Badge } from 'frappe-ui'
import { computed, inject } from 'vue'
import { ListRow, ListRowItem } from 'frappe-ui'
import { computed, inject, ref } from 'vue'
import { useRouter } from 'vue-router'
import PageBreadcrumbs from '@/components/PageBreadcrumbs.vue'
const props = defineProps({
name: {
Expand All @@ -66,15 +69,17 @@ const router = useRouter()
const dataSource = useDataSource(props.name)
dataSource.fetchTables()
const StatusCell = (props) => (
<Badge theme={props.row.hidden ? 'orange' : 'green'}>
{props.row.hidden ? 'Disabled' : 'Enabled'}
</Badge>
)
const columns = [
{ label: 'Table', key: 'label', width: '50%' },
{ label: 'Status', key: 'status', cellComponent: StatusCell, width: '50%' },
]
const searchQuery = ref('')
const filteredList = computed(() => {
if (!searchQuery.value) {
return dataSource.tableList.filter((table) => !table.is_query_based)
}
return dataSource.tableList.filter(
(table) =>
!table.is_query_based &&
table.label.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const dropdownActions = computed(() => {
return [
Expand Down
Loading

0 comments on commit 28701ab

Please sign in to comment.