Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 42 additions & 43 deletions cli/create-vc-app/src/templates/module/composables/useList.ts.ejs
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import { ref, type Ref } from "vue";
import { useAsync, useLoading } from "@vc-shell/framework";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function use<%- ModuleNamePascalCase %>List() {
const data: Ref<Record<string, any>[]> = ref([]);
const totalCount = ref(0);
const currentPage = ref(1);
const searchQuery = ref("");

const { loading: itemsLoading, action: fetchItems } = useAsync(async () => {
// TODO: Replace with real API call
// const result = await apiClient.search({
// keyword: searchQuery.value,
// skip: (currentPage.value - 1) * 20,
// take: 20,
// });
// data.value = result.results ?? [];
// totalCount.value = result.totalCount ?? 0;
});

const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => {
// TODO: Replace with real API call
// await Promise.all((ids ?? []).map((id) => apiClient.delete(id)));
// await fetchItems();
});

const loading = useLoading(itemsLoading, deleteLoading);

async function getItems() {
await fetchItems();
}

return {
data,
loading,
totalCount,
currentPage,
searchQuery,
getItems,
removeItems,
};
}
import { ref, type Ref } from "vue";
import { useAsync, useLoading } from "@vc-shell/framework";

export interface <%- ModuleNamePascalCase %>ListQuery {
keyword?: string;
sort?: string;
skip?: number;
take?: number;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function use<%- ModuleNamePascalCase %>List() {
const data: Ref<Record<string, any>[]> = ref([]);
const totalCount = ref(0);

const { loading: itemsLoading, action: getItems } = useAsync<<%- ModuleNamePascalCase %>ListQuery>(async (query) => {
// TODO: Replace with real API call
// const result = await apiClient.search({
// keyword: query?.keyword,
// sort: query?.sort,
// skip: query?.skip ?? 0,
// take: query?.take ?? 20,
// });
// data.value = result.results ?? [];
// totalCount.value = result.totalCount ?? 0;
});

const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => {
// TODO: Replace with real API call
// await Promise.all((ids ?? []).map((id) => apiClient.delete(id)));
});

const loading = useLoading(itemsLoading, deleteLoading);

return {
data,
loading,
totalCount,
getItems,
removeItems,
};
}
84 changes: 60 additions & 24 deletions cli/create-vc-app/src/templates/module/pages/list.vue.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@
width="50%"
>
<VcDataTable
v-model:search-value="searchValue"
v-model:sort-field="sortField"
v-model:sort-order="sortOrder"
:items="data"
:total-count="totalCount"
:current-page="currentPage"
:search-value="searchQuery"
:pagination="{ currentPage, pages }"
:searchable="true"
:state-key="'<%- ModuleNameScreamingSnake %>'"
@search:change="(val: string) => { searchQuery = val; getItems(); }"
@item-click="openDetails"
@pagination-click="(page: number) => { currentPage = page; getItems(); }"
@row-click="onRowClick"
@pagination-click="onPaginationClick"
>
<!-- Add your columns here -->
<VcColumn id="name" :header="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.NAME')" sortable />
<VcColumn id="createdDate" :header="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.CREATED_DATE')" type="datetime" sortable />
<VcColumn id="name" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.NAME')" sortable />
<VcColumn id="createdDate" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.CREATED_DATE')" type="datetime" sortable />
</VcDataTable>
</VcBlade>
</template>

<script setup lang="ts">
import { useBlade, type IBladeToolbar } from "@vc-shell/framework";
import { useBlade, useDataTableSort, useTableQueryState, useFunctions, type IBladeToolbar } from "@vc-shell/framework";
import { VcBlade, VcDataTable, VcColumn } from "@vc-shell/framework/ui";
import { ref, onMounted } from "vue";
import { computed, ref, watch } from "vue";
import use<%- ModuleNamePascalCase %>List from "../composables/useList";
import { useI18n } from "vue-i18n";

Expand All @@ -42,22 +44,57 @@ defineBlade({

const { t } = useI18n({ useScope: "global" });
const { openBlade, exposeToChildren } = useBlade();
const { debounce } = useFunctions();

const {
data,
loading,
totalCount,
currentPage,
searchQuery,
getItems,
} = use<%- ModuleNamePascalCase %>List();
const PAGE_SIZE = 20;

const { sortField, sortOrder, sortExpression } = useDataTableSort({
initialField: "createdDate",
initialDirection: "DESC",
});

const { data, loading, totalCount, getItems } = use<%- ModuleNamePascalCase %>List();

const searchValue = ref<string>();
const currentPage = ref(1);
const pages = computed(() => Math.ceil(totalCount.value / PAGE_SIZE) || 0);

// Restore sort/search/page from the URL, then load once below.
const restored = useTableQueryState("<%- ModuleNameScreamingSnake %>").read();
if (restored.sort) {
const [field, direction] = restored.sort.split(":");
sortField.value = field;
sortOrder.value = direction === "DESC" ? -1 : 1;
}
if (restored.search) searchValue.value = restored.search;
if (restored.page) currentPage.value = restored.page;

function load() {
return getItems({
sort: sortExpression.value,
keyword: searchValue.value || undefined,
skip: (currentPage.value - 1) * PAGE_SIZE,
take: PAGE_SIZE,
});
}

// One loader for sort/search/page. Debounced so typing doesn't fetch per keystroke.
load();
watch(searchValue, () => (currentPage.value = 1));
watch([sortExpression, searchValue, currentPage], debounce(load, 300));

const onPaginationClick = (page: number) => {
currentPage.value = page;
};

const reload = () => load();

const bladeToolbar = ref<IBladeToolbar[]>([
{
id: "refresh",
title: t("<%- ModuleNameScreamingSnake %>.PAGES.LIST.TOOLBAR.REFRESH"),
icon: "lucide-refresh-cw",
clickHandler: () => getItems(),
clickHandler: () => reload(),
},
{
id: "add",
Expand All @@ -72,15 +109,14 @@ function openDetails(item?: { id?: string }) {
name: "<%- ModuleNamePascalCase %>Details",
param: item?.id,
async onClose() {
await getItems();
await reload();
},
});
}

onMounted(async () => {
await getItems();
})

function onRowClick(event: { data: { id?: string } }) {
openDetails(event.data);
}

exposeToChildren({ reload: getItems });
exposeToChildren({ reload });
</script>
80 changes: 36 additions & 44 deletions cli/create-vc-app/src/templates/sample-module/pages/list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<!-- Blade contents -->
<VcDataTable
v-model:search-value="searchValue"
v-model:sort-field="sortField"
v-model:sort-order="sortOrder"
v-model:active-item-id="selectedItemId"
v-model:selection="selectedItems"
:loading="loading"
Expand All @@ -32,7 +34,6 @@
:total-label="$t('SAMPLE_APP.PAGES.LIST.TABLE.TOTALS')"
:total-count="totalCount"
state-key="SAMPLE_APP"
@search="onSearchList"
@row-click="onItemClick"
@pagination-click="onPaginationClick"
>
Expand Down Expand Up @@ -73,8 +74,8 @@
</template>

<script lang="ts" setup>
import { computed, ref, onMounted, watch } from "vue";
import { IBladeToolbar, useBlade, usePopup, useTableSort, useFunctions } from "@vc-shell/framework";
import { computed, ref, watch } from "vue";
import { IBladeToolbar, useBlade, usePopup, useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
import type { TableAction } from "@vc-shell/framework";
import { VcColumn, VcDataTable, VcBlade } from "@vc-shell/framework/ui";
import { useI18n } from "vue-i18n";
Expand All @@ -97,22 +98,35 @@ const { param, openBlade, exposeToChildren } = useBlade();
const { showConfirmation } = usePopup();
const { debounce } = useFunctions();

const { sortExpression } = useTableSort({
initialProperty: "createdDate",
const PAGE_SIZE = 20;

const { sortField, sortOrder, sortExpression } = useDataTableSort({
initialField: "createdDate",
initialDirection: "DESC",
});

const { getItems, removeItems, data, loading, totalCount, pages, currentPage, searchQuery } = useList({
const { getItems, removeItems, data, loading, totalCount, pages } = useList({
sort: sortExpression.value,
pageSize: 20,
pageSize: PAGE_SIZE,
});

const searchValue = ref();
const searchValue = ref<string>();
const currentPage = ref(1);
const selectedItemId = ref<string>();
const selectedItems = ref<MockedItem[]>([]);

const selectedIds = computed(() => selectedItems.value.map((item) => item.id).filter(Boolean) as string[]);

// Restore sort/search/page from the URL, then load once below.
const restored = useTableQueryState("SAMPLE_APP").read();
if (restored.sort) {
const [field, direction] = restored.sort.split(":");
sortField.value = field;
sortOrder.value = direction === "DESC" ? -1 : 1;
}
if (restored.search) searchValue.value = restored.search;
if (restored.page) currentPage.value = restored.page;

watch(
param,
(newVal) => {
Expand All @@ -121,47 +135,29 @@ watch(
{ immediate: true },
);

onMounted(async () => {
await getItems({
...searchQuery.value,
function load() {
return getItems({
sort: sortExpression.value,
keyword: searchValue.value || undefined,
skip: (currentPage.value - 1) * PAGE_SIZE,
});
});

watch(sortExpression, async (value) => {
await getItems({
...searchQuery.value,
sort: value,
});
});
}

const onSearchList = debounce(async (keyword: string) => {
searchValue.value = keyword;
await getItems({
...searchQuery.value,
keyword,
});
}, 1000);
// One loader for sort/search/page. Debounced so typing doesn't fetch per keystroke.
load();
watch(searchValue, () => (currentPage.value = 1));
watch([sortExpression, searchValue, currentPage], debounce(load, 300));

const clearSearch = async () => {
const clearSearch = () => {
searchValue.value = "";
await getItems({
...searchQuery.value,
keyword: "",
});
};

const addItem = () => {
openBlade({
name: "SampleDetails",
});
openBlade({ name: "SampleDetails" });
};

const onPaginationClick = async (page: number) => {
await getItems({
...searchQuery.value,
skip: (page - 1) * (searchQuery.value.take ?? 20),
});
const onPaginationClick = (page: number) => {
currentPage.value = page;
};

const bladeToolbar = ref<IBladeToolbar[]>([
Expand All @@ -188,11 +184,7 @@ const title = computed(() => t("SAMPLE_APP.PAGES.LIST.TITLE"));

const reload = async () => {
selectedItems.value = [];
await getItems({
...searchQuery.value,
skip: (currentPage.value - 1) * (searchQuery.value.take ?? 10),
sort: sortExpression.value,
});
await load();
};

const onItemClick = (event: { data: MockedItem; index: number; originalEvent: Event }) => {
Expand Down
5 changes: 5 additions & 0 deletions framework/core/blade-navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export { useBladeStack } from "@core/blade-navigation/useBladeStack";
export { useBladeMessaging } from "@core/blade-navigation/useBladeMessaging";
export * from "@core/blade-navigation/types";
export { __registerBladeConfig, getBladeConfig, getAllBladeConfigs } from "@core/blade-navigation/bladeConfigRegistry";
// Table query-state: page-facing pull-accessor only. The TableQueryStateKey service
// and createBladeQueryState factory stay internal (consumed directly by VcDataTable
// and the blade rendering layer).
export { useTableQueryState } from "@core/blade-navigation/table-query-state";
export type { UseTableQueryStateReturn, TableQueryPatch } from "@core/blade-navigation/table-query-state";
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./types";
export * from "./serialization";
export * from "./createBladeQueryState";
export * from "./useTableQueryState";
Loading
Loading