notsapui
v0.0.15
Published
Type-safe Vue 3 component library for building data-driven applications with OData services
Readme
notsapui
Overview
Vue 3 component library for building OData-driven applications. Provides smart tables, dialogs, and filters that work with OData metadata.
Prerequisites
- Configure
notsapodatavite plugin to generate types from OData metadata (setup guide) - Set up development proxy for OData services
- Import generated types from
.odata.types.ts
Architecture
Uses renderless components with Vue's provide/inject pattern to share OData context through component trees without prop drilling.
Provide/Inject Composables
Composables from notsapui/pi create typed context:
import { useODataEntitySetPI } from 'notsapui/pi';
// Provider
const { provide } = useODataEntitySetPI();
provide(props);
// Consumer
const { inject } = useODataEntitySetPI();
const { entity, fields, model } = inject();Key Components
1. NotSapApp
Provides application namespace for storing user presets (variants in SAP, relevant only for SAP based services).
Usage:
<NotSapApp namespace="com.mycompany.app">
<!-- Child components access namespace -->
</NotSapApp>2. ODataEntitySet
Provides OData model and entity set context.
Provides:
model- OData instanceentity- Entity set for queriesfields- Field definitionsfieldsMap- Field metadatametadata- OData metadata
Usage:
<ODataEntitySet :model="model" :entity-set="entitySet">
<SmartTable />
<SmartFilter />
</ODataEntitySet>Access in children:
import { useODataEntitySetPI } from 'notsapui/pi';
const { entity, fields, model } = useODataEntitySetPI().inject();3. UseEntitySetContext
Exposes OData context as slot props.
<UseEntitySetContext v-slot="{ entity, fields, model }">
<div>Entity: {{ entity?.getName() }}</div>
<div>Fields: {{ fields.length }}</div>
</UseEntitySetContext>4. SmartTableRoot
Manages table state and queries.
Provides:
- Query functions and state
- Filter state and controls
- Column configuration
- Selection and results
- Export functionality
Usage:
<SmartTableRoot :top="50" v-slot="{ queryImmediate, inlineCount }">
<button @click="queryImmediate">Refresh</button>
<SmartTable />
</SmartTableRoot>Access in children:
import { useSmartTablePI } from 'notsapui/pi';
const { queryImmediate, results, selected } = useSmartTablePI().inject();Composables
useODataEntitySetPI
import { useODataEntitySetPI } from 'notsapui/pi';
const { entity, fields, fieldsMap, model } = useODataEntitySetPI().inject();useSmartTablePI
import { useSmartTablePI } from 'notsapui/pi';
const { queryImmediate, results, selected, fieldsFilters } = useSmartTablePI().inject();Data Flow
<NotSapApp namespace="com.company.app">
<ODataEntitySet :model="model" :entity-set="'Products'">
<SmartTableRoot>
<SmartTable />
<SmartTableFilter />
</SmartTableRoot>
</ODataEntitySet>
</NotSapApp>Child components access parent context via inject() without prop drilling.
Components
SmartTable
Table with sorting, filtering, and pagination.
<SmartTable
v-model:results="results"
v-model:selected="selected"
v-model:columns-names="columnsNames"
v-model:fields-filters="fieldsFilters"
:select-mode="'multi'"
>
<template #cell-ProductId="{ value, row }">
<a @click="showDetails(row)">{{ value }}</a>
</template>
</SmartTable>SmartTableFilter
Filter UI for table columns.
<SmartTableFilter
v-model="fieldsFilters"
:fields="filterFields"
/>SmartRecordDialog
Dialog for viewing/editing records.
<SmartRecordDialog
v-model:open="dialogOpen"
:record="selectedRecord"
:header-fields="['ProductId', 'Name']"
title-field="Name"
/>Development Setup
Configure Vite proxy for OData services:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/sap/opu': {
target: process.env.SAP_HOST,
changeOrigin: true,
headers: { cookie: process.env.SAP_COOKIE },
},
},
},
});Using Generated Types
import { ProductModel } from '@/.odata.types';
const model = ProductModel.getInstance();
const entitySet = ProductModel.entitySetAliases.Products;See notsapodata docs for type generation setup.
Complete Example
<template>
<ODataEntitySet :model="model" :entity-set="entitySet">
<SmartTableRoot
:top="50"
v-slot="{
showConfigDialog,
inlineCount,
queryImmediate,
querying,
fieldsFiltersCount,
resetFilters,
mustRefresh,
loadedCount,
}"
>
<!-- Filters -->
<SmartTableFilter v-model="fieldsFilters" :fields="filtersNames" />
<!-- Main table -->
<SmartTable
v-model:results="results"
v-model:selected="selected"
v-model:filters-names="filtersNames"
v-model:columns-names="columnsNames"
v-model:fields-filters="fieldsFilters"
:force-select="forceSelect"
:select-mode="'multi'"
:row-height="40"
:custom-query="customQuery"
>
<!-- Custom cell rendering -->
<template #cell-ProductId="{ value, row }">
<a @click="showDetails(row)">{{ value }}</a>
</template>
<template #cell-Status="{ value }">
<span :class="getStatusClass(value)">{{ value }}</span>
</template>
</SmartTable>
<!-- Toolbar -->
<div v-if="selected.length > 0">
<button @click="exportSelected">Export {{ selected.length }} items</button>
</div>
<!-- Detail dialog -->
<SmartRecordDialog
v-model:open="detailsDialog"
v-if="selectedRow"
:record="selectedRow"
:header-fields="['ProductId', 'Name', 'Category']"
title-field="Name"
sub-title-field="ProductId"
fetch-data
:groups="{
'Basic Info': ['Description', 'Price', 'Currency'],
Stock: ['Quantity', 'Location', 'LastUpdate'],
}"
/>
</SmartTableRoot>
</ODataEntitySet>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ProductModel, type TProductModelProductsFields } from '@/.odata.types';
import ODataEntitySet from 'notsapui/ODataEntitySet.vue';
import SmartTableRoot from 'notsapui/SmartTableRoot.vue';
import SmartTable from 'notsapui/SmartTable.vue';
import SmartTableFilter from 'notsapui/SmartTableFilter.vue';
import SmartRecordDialog from 'notsapui/SmartRecordDialog.vue';
import { checkODataFilter, type TODataFieldsFilters } from 'notsapodata';
import type { TODataEntityCustomQuery } from 'notsapui/pi';
const model = ProductModel.getInstance();
const entitySet = ProductModel.entitySetAliases.Products;
const results = ref([]);
const selected = ref([]);
const fieldsFilters = ref({});
const filtersNames = ref(['ProductId', 'Name', 'Status']);
const columnsNames = ref(['ProductId', 'Name', 'Price']);
const detailsDialog = ref(false);
const selectedRow = ref();
function showDetails(row) {
selectedRow.value = row;
detailsDialog.value = true;
}
</script>Custom Query
import type { TODataEntityCustomQuery } from 'notsapui/pi';
const customQuery: TODataEntityCustomQuery = async (entity, params) => {
const result = await entity.query(params);
return {
records: result.data,
inlineCount: result.count,
};
};Utilities
import { fieldsFiltersToODataFilters } from 'notsapui/utils';
const filters = fieldsFiltersToODataFilters(fieldsFilters.value);See notsapodata docs for more utilities.
Best Practices
- Use generated types from
notsapodatafor field names and filters - Implement query blocking when filters are required
- Use
forceSelectto ensure critical fields are always fetched - Leverage provide/inject for shared state instead of props
See notsapodata best practices for OData-specific guidance.
Styling
Import pre-compiled styles:
import 'notsapui/styles.css';Or configure UnoCSS shortcuts and icons for raw components:
// uno.config.ts
import { defineConfig, type Preset } from 'unocss';
import { presetVunor, vunorShortcuts } from 'vunor/theme';
import { notSapUiVunorShortcuts } from 'notsapui/vunor';
import { notSapIconsPreset } from 'notsapui/icons';
export default defineConfig({
presets: [
presetVunor() as Preset,
notSapIconsPreset({
// Override default icons (optional)
'light-mode': 'line-md:light-dark-loop',
'dark-mode': 'line-md:light-dark-loop',
'details': 'ix:jigsaw-details-filled',
'minimal': 'streamline-plump:table',
'advanced': 'si:dashboard-vert-fill',
}),
],
shortcuts: [vunorShortcuts(notSapUiVunorShortcuts)],
});Icons
notSapIconsPreset provides default icons used by notsapui components. Icons come from Iconify.
Override default icons:
notSapIconsPreset({
'refresh': 'mdi:refresh', // Override default refresh icon
'search': 'mdi:magnify', // Find icons at icon-sets.iconify.design
})Add custom icons:
notSapIconsPreset({
'my-custom-icon': 'mdi:star', // Use as: i--my-custom-icon
})Use icons in templates with i--<icon-name> class:
<div class="i--refresh" />
<div class="i--search" />
<div class="i--my-custom-icon" />