diff --git a/package.json b/package.json index edd7bf9..fb2aaac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fohn-ui", - "version": "1.4.0", + "version": "1.5.0", "description": "Javascript library for Fohn-Ui php framework.", "main": "dist/fohn-ui.min.js", "files": [ diff --git a/src/components/components-install.js b/src/components/components-install.js index 503b3e3..d8b0339 100644 --- a/src/components/components-install.js +++ b/src/components/components-install.js @@ -13,6 +13,7 @@ import TableRow from './table/row.component.vue'; import TableCell from './table/cell.component.vue'; import ExceptionModal from './modal/exception-modal.component.vue'; import TablePaginator from './table/paginator.component.vue'; +import TableAction from "./table/table.action.component.vue"; import Dummy from './dummy.component.vue'; import Modal from './modal/modal.component.vue'; import Tabs from './tabs/tabs.component.vue'; @@ -33,6 +34,7 @@ const fohnComponents = [ {name: 'fohn-table-row', def: TableRow}, {name: 'fohn-table-cell', def: TableCell}, {name: 'fohn-table-paginator', def: TablePaginator}, + {name: 'fohn-table-action', def: TableAction}, {name: 'fohn-modal', def: Modal}, {name: 'fohn-ui-exception', def: ExceptionModal}, {name: 'fohn-tab', def: Tab}, diff --git a/src/components/form/form.store.js b/src/components/form/form.store.js index 03e61a1..6afe145 100644 --- a/src/components/form/form.store.js +++ b/src/components/form/form.store.js @@ -24,7 +24,7 @@ export const useFormStoreFactory = (id) => { this.fetchControlValues(); } }, - clearControlValues() { + clearControlsValue() { for (const value of this.controls.values()) { value.value = ''; } diff --git a/src/components/modal/modal.component.vue b/src/components/modal/modal.component.vue index 8ddee02..5a24882 100644 --- a/src/components/modal/modal.component.vue +++ b/src/components/modal/modal.component.vue @@ -1,5 +1,5 @@ + + diff --git a/src/components/table/table.action.component.vue b/src/components/table/table.action.component.vue new file mode 100644 index 0000000..fe37c65 --- /dev/null +++ b/src/components/table/table.action.component.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/table/table.component.vue b/src/components/table/table.component.vue index 4805eac..45cbfeb 100644 --- a/src/components/table/table.component.vue +++ b/src/components/table/table.component.vue @@ -3,14 +3,14 @@ * Todo serve two different mode. Load all items and use fuse search internally or * use as it is now, loading items per page load. */ -import {onMounted, ref} from 'vue'; +import {onMounted, ref, provide, computed} from 'vue'; import debounce from 'lodash.debounce'; import { useTableStoreFactory } from './table.store'; export default { name: 'fohn-table', props: { - actions: { + rowActions: { type: Object, }, searchDebounceValue: { @@ -20,16 +20,31 @@ export default { columns: { type: Array, }, + hasSelectableRows: { + type: Boolean, + default: false, + }, storeId: String, dataUrl: String, itemsPerPage: Number, keepTableState: { type: Boolean, default: true, + }, + keepSelectionAcrossPage: { + type: Boolean, + default: false, } }, setup(props, { attrs, slots, emit }) { - const { columns, dataUrl, searchDebounceValue, storeId, keepTableState } = props; + const { columns, + dataUrl, + searchDebounceValue, + storeId, + keepTableState, + hasSelectableRows, + keepSelectionAcrossPage } = props; + const rows = ref([]); const isFetching = ref(false); const currentPage = ref(1); @@ -37,12 +52,17 @@ export default { const sortDirection = ref(''); const itemsPerPage = ref(props.itemsPerPage); const totalItems = ref(0); + const selectedRows = ref(new Set()); const query = ref(''); + // each table get its own tableStore. const tableStore = useTableStoreFactory(storeId)(); const debounceSearch = debounce((query) => { tableStore.searchItems(query); + if (!keepSelectionAcrossPage) { + clearSelectedRows(); + } }, searchDebounceValue); tableStore.setDataUrl(dataUrl); @@ -63,9 +83,36 @@ export default { sortDirection.value = state.tableState.sort.direction; itemsPerPage.value = state.tableState.itemsPerPage; query.value = state.tableState.currentQuery; + selectedRows.value = new Set(state.selectedRows) ; + }); + + const hasAllRowSelected = computed(() => { + return (selectedRows.value.size === 0) ? false : rows.value.every((row) => selectedRows.value.has(row.id)); + }); + + const hasSomeRowSelected = computed( () => { + return rows.value.reduce((acc, row) => { + if (selectedRows.value.has(row.id)) { + acc.push(row.id); + } + return acc; + }, []).length > 0; + }); + + const selectedRowSize = computed(() => selectedRows.value.size); + + const pageSelectState = computed(() => { + return { + all : hasAllRowSelected.value, + partial: hasSomeRowSelected.value && !hasAllRowSelected.value, + none: !hasAllRowSelected.value && !hasSomeRowSelected.value, + }; }); const loadPage = (pageNumber) => { + if (!keepSelectionAcrossPage) { + tableStore.clearSelectedRows(); + } tableStore.loadPage(pageNumber); }; @@ -78,6 +125,18 @@ export default { tableStore.sortTable(columnName, dir); }; + const togglePageRows = () => { + if (hasAllRowSelected.value) { + rows.value.forEach( (row) => tableStore.removeRowIdFromSelection(row.id)); + } else { + rows.value.forEach( (row) => tableStore.addRowIdToSelection(row.id)); + } + } + + const clearSelectedRows = () => { + tableStore.clearSelectedRows(); + } + const searchItems = (query) => { debounceSearch(query); } @@ -87,20 +146,23 @@ export default { } /** - * Execute a table action, i.e. call a javascript function pass into props.action. + * Execute a table row action, i.e. call a javascript function pass into props.action. * The function is executed with the row id as first param. * * @param actionName * @param id */ - const executeAction = (actionName, id) => { - props.actions[actionName](id); + const executeRowAction = (actionName, id) => { + props.rowActions[actionName](id); } onMounted(() => { tableStore.fetchItems(); }); + // have storeId available to children component + provide('tableStoreId', storeId); + return { isFetching, query, @@ -116,7 +178,12 @@ export default { sortTable, clearSearch, setItemsPerPage, - executeAction, + hasSelectableRows, + selectedRowSize, + togglePageRows, + clearSelectedRows, + pageSelectState, + executeRowAction, }; }, }; @@ -139,7 +206,12 @@ export default { :sortTable="sortTable" :clearSearch="clearSearch" :setItemsPerPage="setItemsPerPage" - :executeAction="executeAction" + :executeRowAction="executeRowAction" + :hasSelectableRows="hasSelectableRows" + :selectedRowSize="selectedRowSize" + :togglePageRows="togglePageRows" + :pageSelectState="pageSelectState" + :clearSelectedRows="clearSelectedRows" v-bind="$attrs">table diff --git a/src/components/table/table.store.js b/src/components/table/table.store.js index 102eccd..a1907a6 100644 --- a/src/components/table/table.store.js +++ b/src/components/table/table.store.js @@ -26,10 +26,17 @@ export const useTableStoreFactory = (id) => { } }), currentRows: [], + selectedRows: new Set(), totalItems: 0, isFetching: false, }), getters: { + isRowSelected: (state) => { + return (id) => state.selectedRows.has(id); + }, + hasRowSelected: (state) => { + return state.selectedRows.size > 0; + }, }, actions: { /** @@ -75,10 +82,27 @@ export const useTableStoreFactory = (id) => { } }); }, + toggleRow(id) { + if(this.isRowSelected(id)) { + this.selectedRows.delete(id); + } else { + this.selectedRows.add(id); + } + }, + addRowIdToSelection (id) { + this.selectedRows.add(id); + }, + removeRowIdFromSelection (id) { + this.selectedRows.delete(id); + }, + clearSelectedRows() { + this.selectedRows = new Set(); + }, deleteRow(id) { this.currentRows = [...this.currentRows.filter((tableRow) => { return tableRow.id !== id })]; + this.fetchItems(); }, loadPage(pageNumber) { this.tableState.currentPage = pageNumber; @@ -127,6 +151,43 @@ export const useTableStoreFactory = (id) => { setDataUrl(url) { this.url = url; }, + /** + * Callback server for the purpose of executing an action. + * Callback will trigger onTrigger event in TriggerCtrl. + */ + executeAction(url, targetElement) { + const options = { + method: 'POST', + body: utils().json().stringify({ + ids: Array.from(this.selectedRows), + }), + } + + targetElement.classList.add('loading'); + const { isFetching, data, onFetchFinally, onFetchError } = apiService.fetchAsResponse(url, options); + + watch(isFetching, (inProgress) => { + this.isFetching = inProgress; + }); + + onFetchFinally( () => { + const results = data.value || {}; + if (results.jsRendered) { + apiService.evalResponse(results.jsRendered); + } + if (results?.state?.reload) { + this.fetchItems(); + } + if (results?.state?.keepSelection === false) { + this.selectedRows = new Set(); + } + targetElement.classList.remove('loading'); + }); + + onFetchError( (error) => { + console.error(error); + }); + } }, }); fohn.vueService.addStore(id, store);