diff --git a/docs/en/tutorials/mobile/react-native/index.md b/docs/en/tutorials/mobile/react-native/index.md
index 3bfeb245c5..bddd3a99a0 100644
--- a/docs/en/tutorials/mobile/react-native/index.md
+++ b/docs/en/tutorials/mobile/react-native/index.md
@@ -1,20 +1,21 @@
```json
//[doc-seo]
{
- "Description": "Learn how to develop a mobile application using React Native with ABP Framework, focusing on UI for the Acme.BookStore app."
+ "Description": "Learn how to develop a mobile application using React Native with the ABP Framework. Build the Acme.BookStore mobile UI on top of the modernized ABP React Native template (NativeWind v4 + Bottom Tab navigation)."
}
```
# Mobile Application Development Tutorial - React Native
-React Native mobile option is *available for* ***Team*** *or higher licenses*. Therefore, if you don't have a commercial license, it's suggested to follow the article by downloading the source code of the sample application as described in the next chapter.
+The React Native mobile option is *available for* ***Team*** *or higher licenses*. If you don't have a commercial license, follow this article by downloading the source code of the sample application linked below.
## About This Tutorial
> You must have an [ABP Team or a higher license](https://abp.io/pricing) to be able to create a mobile application.
-- This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial.
-- In this tutorial, we will only focus on the UI side of the `Acme.BookStore` application and will implement the CRUD operations.
+- This tutorial assumes you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option. If you haven't completed it, you can either complete it first or download the source code below and follow this tutorial.
+- This tutorial only focuses on the **React Native UI side** of the `Acme.BookStore` application. It implements the CRUD operations for `Books` and `Authors`, plus the relation between them. The backend (entities, application services, permissions, seeder) is already in place in the downloadable sample.
+- The mobile template was modernized in 2026: it now uses **NativeWind v4** (Tailwind CSS for React Native) for styling, **Bottom Tab navigation** by default, and the **Redux Toolkit** store with hook-based access (`useSelector` / `useDispatch`). The `connectToRedux` HOC, the `DrawerNavigator`, and the legacy `DataList`/`AbpSelect` components from earlier versions no longer ship with the template — this tutorial walks through building the new equivalents.
- Before starting, please make sure that the [React Native Development Environment](../../../framework/ui/react-native/index.md) is ready on your machine.
## Download the Source Code
@@ -25,1911 +26,1198 @@ You can use the following link to download the source code of the application de
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../../../kb/windows-path-too-long-fix.md).
-## The Book List Page
-
-There is no dynamic proxy generation for the react native application, that is why we need to create the BookAPI proxy manually under the `./src/api` folder.
+The downloaded sample contains:
-```ts
-//./src/api/BookAPI.ts
-import api from './API';
+- `src/` — ABP backend (`Acme.BookStore.*` projects). It already exposes `BookAppService` and `AuthorAppService` with the CRUD endpoints we will consume.
+- `react-native/` — the React Native client. The auth, profile and settings flows ship out of the box. Throughout this tutorial we will add the `BookStore` feature to it.
-export const getList = () => api.get('/api/app/book').then(({ data }) => data);
+## Backend Setup (Quick Reference)
-export const get = id => api.get(`/api/app/book/${id}`).then(({ data }) => data);
+The backend ships ready-to-run. The relevant pieces consumed from React Native are:
-export const create = input => api.post('/api/app/book', input).then(({ data }) => data);
+- **Endpoints**
+ - `GET /api/app/book` — paged list (returns `items` with `id`, `name`, `type`, `publishDate`, `price`, `authorName`)
+ - `GET /api/app/book/{id}` — single book
+ - `POST /api/app/book` — create
+ - `PUT /api/app/book/{id}` — update
+ - `DELETE /api/app/book/{id}` — delete
+ - `GET /api/app/book/author-lookup` — `{ items: [{ id, name }] }` for the author dropdown
+ - `GET /api/app/author` — paged list (`items: [{ id, name, birthDate, shortBio }]`)
+ - `GET /api/app/author/{id}`, `POST /api/app/author`, `PUT /api/app/author/{id}`, `DELETE /api/app/author/{id}`
+- **Permissions** (use these names from the React Native side to gate UI):
+ - `BookStore.Books`, `BookStore.Books.Create`, `BookStore.Books.Edit`, `BookStore.Books.Delete`
+ - `BookStore.Authors`, `BookStore.Authors.Create`, `BookStore.Authors.Edit`, `BookStore.Authors.Delete`
-export const update = (input, id) => api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
-
-export const remove = id => api.delete(`/api/app/book/${id}`).then(({ data }) => data);
-
-```
+To run the backend, start `Acme.BookStore.DbMigrator` once (it seeds three sample authors and six sample books), then run `Acme.BookStore.HttpApi.Host`. The default `BookStore` permissions need to be granted to the `admin` user via the **Identity → Roles → admin → Permissions** screen of the web UI before testing on mobile.
-### Add the `Book Store` menu item to the navigation
+If you want to follow the backend implementation step by step instead, read the [Web Application Development tutorial](../../book-store/part-01.md). The mobile-side code below works against the API surface listed above regardless of how you produced it.
-For createing a menu item, navigate to `./src/navigators/DrawerNavigator.tsx` file and add `BookStoreStack` to `Drawer.Navigator` component.
+## Adding the Book API Proxy
-```tsx
-//Other imports..
-import BookStoreStackNavigator from './BookStoreNavigator';
-
-const Drawer = createDrawerNavigator();
-
-export default function DrawerNavigator() {
- return (
-
- {/*Added Screen*/}
- null }}}%}
- />
- {/*Added Screen*/}
-
- );
-}
-```
+There is no dynamic proxy generation for the React Native application, so we create the `BookAPI` proxy manually under `./src/api`.
-Create the `BookStoreStackNavigator` inside `./src/navigators/BookStoreNavigator.tsx`, this navigator will be used for the BookStore menu item.
+```ts
+// ./src/api/BookAPI.ts
+import api from './API';
-```tsx
-import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import { Button } from 'react-native-paper';
-import i18n from 'i18n-js';
+export const getList = (params: { maxResultCount?: number; skipCount?: number; sorting?: string } = {}) =>
+ api.get('/api/app/book', { params }).then(({ data }) => data);
-import { BookStoreScreen, CreateUpdateAuthorScreen, CreateUpdateBookScreen } from '../screens';
+export const get = (id: string) =>
+ api.get(`/api/app/book/${id}`).then(({ data }) => data);
-import { HamburgerIcon } from '../components';
-import { useThemeColors } from '../hooks';
+export const create = (input: any) =>
+ api.post('/api/app/book', input).then(({ data }) => data);
-const Stack = createNativeStackNavigator();
+export const update = (input: any, id: string) =>
+ api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
-export default function BookStoreStackNavigator() {
- const { background, onBackground } = useThemeColors();
+export const remove = (id: string) =>
+ api.delete(`/api/app/book/${id}`).then(({ data }) => data);
- return (
-
- ({
- title: i18n.t('BookStore::Menu:BookStore'),
- headerLeft: () => ,
- headerStyle: { backgroundColor: background },
- headerTintColor: onBackground,
- headerShadowVisible: false,
- })}
- />
- ({
- title: i18n.t(route.params?.bookId ? 'BookStore::Edit' : 'BookStore::NewBook'),
- headerRight: () => (
-
- ),
- headerStyle: { backgroundColor: background },
- headerTintColor: onBackground,
- headerShadowVisible: false,
- })}
- />
-
- );
-}
+export const getAuthorLookup = () =>
+ api.get('/api/app/book/author-lookup').then(({ data }) => data);
```
-- BookStoreScreen will be used to store the `books` and `authors` page
+We will create `./src/api/AuthorAPI.ts` later in the [Author Section](#author).
-Add the `BookStoreStack` to the screens object in the `./src/components/DrawerContent/DrawerContent.tsx` file. The DrawerContent component will be used to render the menu items.
+- `api` is the shared `axios` instance (`./src/api/API.ts`) that injects the access token via the request interceptor in `./src/interceptors/APIInterceptor.ts`.
+- `getList` accepts a paging payload (`maxResultCount`, `skipCount`, `sorting`) so it can be plugged into the `DataList` component we build next.
-```tsx
-// Imports..
-const screens = {
- HomeStack: { label: "::Menu:Home", iconName: "home" },
- DashboardStack: {
- label: "::Menu:Dashboard",
- requiredPolicy: "BookStore.Dashboard",
- iconName: "chart-areaspline",
- },
- UsersStack: {
- label: "AbpIdentity::Users",
- iconName: "account-supervisor",
- requiredPolicy: "AbpIdentity.Users",
- },
- //Add this property
- BookStoreStack: {
- label: "BookStore::Menu:BookStore",
- iconName: "book",
- },
- //Add this property
- TenantsStack: {
- label: "Saas::Tenants",
- iconName: "book-outline",
- requiredPolicy: "Saas.Tenants",
- },
- SettingsStack: {
- label: "AbpSettingManagement::Settings",
- iconName: "cog",
- navigation: null,
- },
-};
-// Other codes..
-```
+## Building the DataList Component
-
-
-### Create Book List page
-
-Before creating the book list page, we need to create the `BookStoreScreen.tsx` file under the `./src/screens/BookStore` folder. This file will be used to store the `books` and `authors` page.
+The earlier React Native template shipped a `DataList` component on top of React Native Paper. The new template only ships the essentials (`FormButtons`, `Loading`, `ValidationMessage`), so we add a NativeWind-based equivalent under `./src/components/DataList`.
```tsx
-import { useState, useEffect } from 'react';
-import { useSelector } from 'react-redux';
-import i18n from 'i18n-js';
-import { BottomNavigation } from 'react-native-paper';
-
-import { BooksScreen } from '../../screens';
+// ./src/components/DataList/DataList.tsx
+import { useCallback, useContext, useEffect, useState } from 'react';
+import { View, Text, FlatList, RefreshControl, ActivityIndicator } from 'react-native';
+import { LocalizationContext } from '../../contexts/LocalizationContext';
import { useThemeColors } from '../../hooks';
-const BooksRoute = nav => ;
+interface DataListProps {
+ fetchFn: (params: { maxResultCount: number; skipCount: number }) => Promise<{ items: T[]; totalCount: number }>;
+ render: (info: { item: T; index: number }) => React.ReactElement;
+ trigger?: any;
+ pageSize?: number;
+}
-function BookStoreScreen({ navigation }) {
- const [index, setIndex] = React.useState(0);
- const [routes] = React.useState([
- {
- key: "books",
- title: i18n.t("BookStore::Menu:Books"),
- focusedIcon: "book",
- unfocusedIcon: "book-outline",
+function DataList({
+ fetchFn,
+ render,
+ trigger,
+ pageSize = 20,
+}: DataListProps) {
+ const { t } = useContext(LocalizationContext);
+ const { accentColor } = useThemeColors();
+
+ const [items, setItems] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [skipCount, setSkipCount] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const loadPage = useCallback(
+ async (skip: number, append: boolean) => {
+ if (loading) return;
+ setLoading(true);
+ try {
+ const result = await fetchFn({ maxResultCount: pageSize, skipCount: skip });
+ const fetched = result?.items ?? [];
+ setTotalCount(result?.totalCount ?? 0);
+ setItems(prev => (append ? [...prev, ...fetched] : fetched));
+ setSkipCount(skip + fetched.length);
+ } catch (e) {
+ if (!append) setItems([]);
+ } finally {
+ setLoading(false);
+ }
},
- ]);
-
- const renderScene = BottomNavigation.SceneMap({
- books: BooksRoute,
- });
-
- return (
-
+ [fetchFn, pageSize, loading],
);
-}
-export default BookStoreScreen;
-```
-
-Create the `BooksScreen.tsx` file under the `./src/screens/BookStore/Books` folder.
-```tsx
-import { useSelector } from "react-redux";
-import { View } from "react-native";
-import { List } from "react-native-paper";
-import { getBooks } from "../../api/BookAPI";
-import i18n from "i18n-js";
-import DataList from "../../components/DataList/DataList";
-import { createAppConfigSelector } from "../../store/selectors/AppSelectors";
-import { useThemeColors } from '../../../hooks';
-
-function BooksScreen({ navigation }) {
- const { background, primary } = useThemeColors();
- const currentUser = useSelector(createAppConfigSelector())?.currentUser;
+ useEffect(() => {
+ setSkipCount(0);
+ loadPage(0, false);
+ }, [trigger]);
+
+ const onRefresh = useCallback(async () => {
+ setRefreshing(true);
+ await loadPage(0, false);
+ setRefreshing(false);
+ }, [loadPage]);
+
+ const onEndReached = useCallback(() => {
+ if (loading || refreshing) return;
+ if (items.length >= totalCount) return;
+ loadPage(skipCount, true);
+ }, [loading, refreshing, items.length, totalCount, skipCount, loadPage]);
return (
-
- {currentUser?.isAuthenticated && (
-
+ ListEmptyComponent={
+ loading ? null : (
+
+
+ {t('AbpUi::NoData')}
+
+
+ )
+ }
+ ListFooterComponent={
+ loading && items.length > 0 ? (
+
+
+
+ ) : null
+ }
+ />
);
}
-export default BooksScreen;
-```
-
-- `getBooks` function is used to fetch the books from the server.
-- `i18n` API to localize the given key. It uses the incoming resource from the `application-localization` endpoint.
-- `DataList` component takes the `fetchFn` property that we'll give to the API request function, it's used to fetch data and maintain the logic of lazy loading etc.
-
-
-## Creating a New Book
-
-### Add the `@react-native-community/datetimepicker` package for the date functionality.
-
-```bash
-yarn expo install @react-native-community/datetimepicker
-
-//or
-
-npx expo install @react-native-community/datetimepicker
+export default DataList;
```
-### Add the `CreateUpdateBook` Screen to the BookStoreNavigator
-
-Like the `BookStoreScreen` we need to add the `CreateUpdateBookScreen` to the `./src/navigators/BookStoreNavigator.tsx` file.
-
-```tsx
-//Other codes
-
-import { Button } from "react-native-paper"; //Added this line
+- `fetchFn` is any function that accepts `{ maxResultCount, skipCount }` and returns `{ items, totalCount }` — the shape of every ABP `ICrudAppService.GetListAsync` response.
+- `trigger` is an arbitrary value: pass a counter that you increment (`setRefresh(r => r + 1)`) after a delete or save and the list re-fetches from page zero.
+- The pull-to-refresh and the lazy "load more on end reached" behavior are built in.
-import { CreateUpdateBookScreen } from '../screens'; //Added this line
+## Building the AbpSelect Component
-//Other codes
-
-export default function BookStoreStackNavigator() {
- return (
-
- {/*Other screens*/}
- {/* Added this screen */}
- ({
- title: i18n.t(
- route.params?.bookId ? "BookStore::Edit" : "BookStore::NewBook"
- ),
- headerRight: () => (
-
- ),
- headerStyle: { backgroundColor: background },
- headerTintColor: onBackground,
- headerShadowVisible: false,
- })}
- />
-
- );
-}
-```
-
-To navigate to the `CreateUpdateBookScreen`, we need to add the `CreateUpdateBook` button to the `BooksScreen.tsx` file.
+For dropdowns (book type, author selection) we build a small modal-based picker, also under `./src/components`.
```tsx
-//Other imports..
-
-import {
- // rest imports..,
- StyleSheet,
-} from "react-native";
-
-import {
- // rest imports..,
- AnimatedFAB,
-} from "react-native-paper";
-
-function BooksScreen({ navigation }) {
- //Other codes..
+// ./src/components/AbpSelect/AbpSelect.tsx
+import { useContext } from 'react';
+import { Modal, View, Text, Pressable, FlatList } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { LocalizationContext } from '../../contexts/LocalizationContext';
+import { useThemeColors } from '../../hooks';
- return (
-
- {/* Other codes..*/}
-
- {/* Included Code */}
- {currentUser?.isAuthenticated && (
- navigation.navigate("CreateUpdateBook")}
- visible={true}
- animateFrom={"right"}
- iconMode={"static"}
- style={[styles.fabStyle, { backgroundColor: primary }]}
- />
- )}
- {/* Included Code */}
-
- );
+export interface AbpSelectItem {
+ id: string | number;
+ displayName: string;
}
-//Added lines
-const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- },
- fabStyle: {
- bottom: 16,
- right: 16,
- position: "absolute",
- },
-});
-//Added lines
-
-export default BooksScreen;
-```
-
-After adding the `CreateUpdateBook` button, we need to add the `CreateUpdateBookScreen.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder.
-
-```tsx
-import PropTypes from "prop-types";
-
-import { create } from "../../../../api/BookAPI";
-import LoadingActions from "../../../../store/actions/LoadingActions";
-import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors";
-import { connectToRedux } from "../../../../utils/ReduxConnect";
-import CreateUpdateBookForm from "./CreateUpdateBookForm";
-
-function CreateUpdateBookScreen({ navigation, startLoading, clearLoading }) {
- const submit = (data) => {
- startLoading({ key: "save" });
-
- create(data)
- .then(() => navigation.goBack())
- .finally(() => clearLoading());
- };
-
- return ;
+interface AbpSelectProps {
+ visible: boolean;
+ title: string;
+ items: AbpSelectItem[];
+ selectedItem?: string | number;
+ hasDefaultItem?: boolean;
+ hideModalFn: () => void;
+ setSelectedItem: (id: any) => void;
}
-CreateUpdateBookScreen.propTypes = {
- startLoading: PropTypes.func.isRequired,
- clearLoading: PropTypes.func.isRequired,
-};
-
-export default connectToRedux({
- component: CreateUpdateBookScreen,
- stateProps: (state) => ({ loading: createLoadingSelector()(state) }),
- dispatchProps: {
- startLoading: LoadingActions.start,
- clearLoading: LoadingActions.clear,
- },
-});
-```
-
-- In this page we will store logic, send post/put requests, get the selected book data and etc.
-- This page will wrap the `CreateUpdateBookFrom` component and pass the submit function with other properties.
-
-Create a `CreateUpdateBookForm.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder and add the following code to it.
-
-```tsx
-import * as Yup from 'yup';
-import { useRef, useState } from 'react';
-import { Platform, KeyboardAvoidingView, StyleSheet, View, ScrollView } from 'react-native';
-import { useFormik } from 'formik';
-import i18n from 'i18n-js';
-import PropTypes from 'prop-types';
-import { TextInput, Portal, Modal, Text, Divider, Button } from 'react-native-paper';
-import DateTimePicker from '@react-native-community/datetimepicker';
-
-import { FormButtons, ValidationMessage, AbpSelect } from '../../../../components';
-import { useThemeColors } from '../../../../hooks';
-
-
-const validations = {
- name: Yup.string().required("AbpValidation::ThisFieldIsRequired."),
- price: Yup.number().required("AbpValidation::ThisFieldIsRequired."),
- type: Yup.string().nullable().required("AbpValidation::ThisFieldIsRequired."),
- publishDate: Yup.string()
- .nullable()
- .required("AbpValidation::ThisFieldIsRequired."),
-};
-
-const props = {
- underlineStyle: { backgroundColor: "transparent" },
- underlineColor: "#333333bf",
-};
-
-function CreateUpdateBookForm({ submit }) {
- const { primaryContainer, background, onBackground } = useThemeColors();
-
- const [bookTypeVisible, setBookTypeVisible] = useState(false);
- const [publishDateVisible, setPublishDateVisible] = useState(false);
-
- const nameRef = useRef(null);
- const priceRef = useRef(null);
- const typeRef = useRef(null);
- const publishDateRef = useRef(null);
-
- const inputStyle = {
- ...styles.input,
- backgroundColor: primaryContainer,
- };
- const bookTypes = new Array(8).fill(0).map((_, i) => ({
- id: i + 1,
- displayName: i18n.t(`BookStore::Enum:BookType.${i + 1}`),
- }));
-
- const onSubmit = (values) => {
- if (!bookForm.isValid) {
- return;
- }
-
- submit({ ...values });
- };
-
- const bookForm = useFormik({
- enableReinitialize: true,
- validateOnBlur: true,
- validationSchema: Yup.object().shape({
- ...validations,
- }),
- initialValues: {
- name: "",
- price: "",
- type: "",
- publishDate: null,
- },
- onSubmit,
- });
-
- const isInvalidControl = (controlName = null) => {
- if (!controlName) {
- return;
- }
-
- return (
- ((!!bookForm.touched[controlName] && bookForm.submitCount > 0) ||
- bookForm.submitCount > 0) &&
- !!bookForm.errors[controlName]
- );
- };
-
- const onChange = (event, selectedDate) => {
- if (!selectedDate) {
- return;
- }
-
- setPublishDateVisible(false);
-
- if (event && event.type !== "dismissed") {
- bookForm.setFieldValue("publishDate", selectedDate, true);
- }
- };
+function AbpSelect({
+ visible,
+ title,
+ items,
+ selectedItem,
+ hasDefaultItem = false,
+ hideModalFn,
+ setSelectedItem,
+}: AbpSelectProps) {
+ const { t } = useContext(LocalizationContext);
+ const { accentColor } = useThemeColors();
+
+ const data = hasDefaultItem
+ ? [{ id: '', displayName: `-- ${t('AbpUi::PagerInfo:NoDataText')} --` } as AbpSelectItem, ...items]
+ : items;
return (
-
- setBookTypeVisible(false)}
- selectedItem={bookForm.values.type}
- setSelectedItem={(id) => {
- bookForm.setFieldValue("type", id, true);
- bookForm.setFieldValue(
- "typeDisplayName",
- bookTypes.find((f) => f.id === id)?.displayName || null,
- false
- );
- }}
- />
-
-
-
-
- priceRef.current.focus()}
- returnKeyType="next"
- onChangeText={bookForm.handleChange('name')}
- onBlur={bookForm.handleBlur('name')}
- value={bookForm.values.name}
- autoCapitalize="none"
- label={i18n.t('BookStore::Name')}
- style={inputStyle}
- {...props}
- />
- {isInvalidControl('name') && (
- {bookForm.errors.name as string}
- )}
+
+
+ {}}
+ className="w-full max-w-md bg-card dark:bg-card-dark rounded-2xl border border-card-border dark:border-card-border-dark shadow-lg overflow-hidden">
+
+
+ {title}
+
+
+
+
-
- typeRef.current.focus()}
- returnKeyType="next"
- onChangeText={bookForm.handleChange('price')}
- onBlur={bookForm.handleBlur('price')}
- value={bookForm.values.price}
- autoCapitalize="none"
- label={i18n.t('BookStore::Price')}
- style={inputStyle}
- {...props}
- />
- {isInvalidControl('price') && (
- {bookForm.errors.price as string}
+ String(item.id)}
+ style={{ maxHeight: 360 }}
+ ItemSeparatorComponent={() => (
+
)}
-
-
-
- setBookTypeVisible(true)} icon="menu-down" />}
- style={inputStyle}
- editable={false}
- value={bookForm.values.typeDisplayName}
- {...props}
- />
- {isInvalidControl('type') && (
- {bookForm.errors.type as string}
- )}
-
-
-
- setPublishDateVisible(true)}
- icon="calendar"
- iconColor={bookForm.values.publishDate ? '#4CAF50' : '#666'}
- />
- }
- style={inputStyle}
- editable={false}
- value={formatDate(bookForm.values.publishDate)}
- placeholder="Select publish date"
- {...props}
- />
- {isInvalidControl('publishDate') && (
- {bookForm.errors.publishDate as string}
- )}
-
-
-
-
-
- {i18n.t('BookStore::PublishDate')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ renderItem={({ item }) => {
+ const isSelected = String(item.id) === String(selectedItem ?? '');
+ return (
+ {
+ setSelectedItem(item.id);
+ hideModalFn();
+ }}
+ className={`px-5 py-3.5 flex-row items-center justify-between ${
+ isSelected ? 'bg-secondary dark:bg-secondary-dark' : ''
+ }`}>
+
+ {item.displayName}
+
+ {isSelected ? : null}
+
+ );
+ }}
+ />
+
+
+
);
}
-const styles = StyleSheet.create({
- inputContainer: {
- margin: 8,
- marginLeft: 16,
- marginRight: 16,
- },
- input: {
- borderRadius: 8,
- borderTopLeftRadius: 8,
- borderTopRightRadius: 8,
- },
- button: {
- marginLeft: 16,
- marginRight: 16,
- },
- dateModal: {
- padding: 20,
- margin: 20,
- borderRadius: 12,
- elevation: 5,
- shadowColor: '#000',
- shadowOffset: {
- width: 0,
- height: 2,
- },
- shadowOpacity: 0.25,
- shadowRadius: 3.84,
- },
- modalTitle: {
- textAlign: 'center',
- marginBottom: 16,
- fontWeight: '600',
- },
- divider: {
- marginBottom: 16,
- },
- modalButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: 20,
- paddingHorizontal: 8,
- },
-});
-
-CreateUpdateBookForm.propTypes = {
- book: PropTypes.object,
- authors: PropTypes.array.isRequired,
- submit: PropTypes.func.isRequired,
-};
-
-export default CreateUpdateBookForm;
+export default AbpSelect;
```
-- `formik` will manage the form state, validation and value changes.
-- `Yup` allows for the build validation schema.
-- `AbpSelect` component is used to select the book type.
-- `submit` method will pass the form values to the `CreateUpdateBookScreen` component.
-
-
-
-
-
-## Update a Book
-
-We need the navigation parameter for getting the bookId and then navigate it again after the create & update operations. That is why we will pass the navigation parameter to the `BooksScreen` component.
-
-```tsx
-//Imports..
-
-//Add navigation parameter
-const BooksRoute = (nav) => ;
-
-function BookStoreScreen({ navigation }) {
- //Other codes..
-
- const renderScene = BottomNavigation.SceneMap({
- books: () => BooksRoute(navigation), //Use this way
- });
-
- //Other codes..
-}
-
-export default BookStoreScreen;
-```
-
-Replace the code below in the `BookScreen.tsx` file under the `./src/screens/BookStore/Books` folder.
-
-```tsx
-import { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { Alert, View, StyleSheet } from 'react-native';
-import { List, IconButton, AnimatedFAB } from 'react-native-paper';
-import { useActionSheet } from '@expo/react-native-action-sheet';
-import i18n from 'i18n-js';
-
-import { getList, remove } from '../../../api/BookAPI';
-import { DataList } from '../../../components';
-import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
-import { useThemeColors } from '../../../hooks';
-
-function BooksScreen({ navigation }) {
- const { background, primary } = useThemeColors();
- const currentUser = useSelector(createAppConfigSelector())?.currentUser;
- const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
-
- const [refresh, setRefresh] = useState(null);
- const { showActionSheetWithOptions } = useActionSheet();
-
- const openContextMenu = (item: { id: string }) => {
- const options = [];
-
- if (policies['BookStore.Books.Delete']) {
- options.push(i18n.t('AbpUi::Delete'));
- }
-
- if (policies['BookStore.Books.Edit']) {
- options.push(i18n.t('AbpUi::Edit'));
- }
-
- options.push(i18n.t('AbpUi::Cancel'));
-
- showActionSheetWithOptions(
- {
- options,
- cancelButtonIndex: options.length - 1,
- destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
- },
- index => {
- switch (options[index]) {
- case i18n.t('AbpUi::Edit'):
- edit(item);
- break;
- case i18n.t('AbpUi::Delete'):
- removeOnClick(item);
- break;
- }
- },
- );
- };
-
- const removeOnClick = (item: { id: string }) => {
- Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
- {
- text: i18n.t('AbpUi::Cancel'),
- style: 'cancel',
- },
- {
- style: 'default',
- text: i18n.t('AbpUi::Ok'),
- onPress: () => {
- remove(item.id).then(() => {
- setRefresh((refresh ?? 0) + 1);
- });
- },
- },
- ]);
- };
-
- const edit = (item: { id: string }) => {
- navigation.navigate('CreateUpdateBook', { bookId: item.id });
- };
-
- return (
-
- {currentUser?.isAuthenticated && (
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- },
- fabStyle: {
- bottom: 16,
- right: 16,
- position: 'absolute',
- },
-});
-
-export default BooksScreen;
+```ts
+// ./src/components/index.ts
+export { default as FormButtons } from './FormButtons/FormButtons';
+export { default as ValidationMessage } from './ValidationMessage/ValidationMessage';
+export { default as DataList } from './DataList/DataList';
+export { default as AbpSelect } from './AbpSelect/AbpSelect';
+export type { AbpSelectItem } from './AbpSelect/AbpSelect';
```
-Replace code below for `CreateUpdateBookScreen.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook/`
+## Creating the BookStoreNavigator
-```tsx
-import PropTypes from 'prop-types';
-import { useEffect, useState } from 'react';
-
-import { getAuthorLookup, get, create, update } from '../../../../api/BookAPI';
-import LoadingActions from '../../../../store/actions/LoadingActions';
-import { createLoadingSelector } from '../../../../store/selectors/LoadingSelectors';
-import { connectToRedux } from '../../../../utils/ReduxConnect';
-import CreateUpdateBookForm from './CreateUpdateBookForm';
+The `BookStore` feature has three screens that share a stack: the list root (`BookStore`), `CreateUpdateBook`, and `CreateUpdateAuthor`. Add the route names to the typed navigator definitions first.
-function CreateUpdateBookScreen({ navigation, route, startLoading, clearLoading }) {
- const { bookId } = route.params || {};
- const [book, setBook] = useState(null);
-
- const submit = (data: any) => {
- startLoading({ key: 'save' });
-
- (data.id ? update(data, data.id) : create(data))
- .then(() => navigation.goBack())
- .finally(() => clearLoading());
- };
-
- useEffect(() => {
- if (bookId) {
- startLoading({ key: 'fetchBookDetail' });
-
- get(bookId)
- .then((response: any) => setBook(response))
- .finally(() => clearLoading());
- }
- }, [bookId]);
-
- return ;
-}
-
-CreateUpdateBookScreen.propTypes = {
- startLoading: PropTypes.func.isRequired,
- clearLoading: PropTypes.func.isRequired,
+```ts
+// ./src/navigators/types.ts (additions)
+export type BookStoreStackParamList = {
+ BookStore: undefined;
+ CreateUpdateBook: { bookId?: string } | undefined;
+ CreateUpdateAuthor: { authorId?: string } | undefined;
};
-export default connectToRedux({
- component: CreateUpdateBookScreen,
- stateProps: state => ({ loading: createLoadingSelector()(state) }),
- dispatchProps: {
- startLoading: LoadingActions.start,
- clearLoading: LoadingActions.clear,
- },
-});
+export type BookStoreScreenProps = NativeStackScreenProps;
+export type CreateUpdateBookScreenProps = NativeStackScreenProps;
+export type CreateUpdateAuthorScreenProps = NativeStackScreenProps;
```
-- `get` method is used to fetch the book details from the server.
-- `update` method is used to update the book on the server.
-- `route` parameter will be used to get the bookId from the navigation.
-
-Replace the `CreateUpdateBookForm.tsx` file with the code below. We will use this file for the create and update operations.
-
-```tsx
-//Imports..
-
-//validateSchema
-
-//props
-
-function CreateUpdateBookForm({
- submit,
- book = null, //Add book parameter with default value
-}) {
- //Other codes..
-
- const bookForm = useFormik({
- enableReinitialize: true,
- validateOnBlur: true,
- validationSchema: Yup.object().shape({
- ...validations,
- }),
- initialValues: {
- //Update initialValues
- ...book,
- name: book?.name || "",
- price: book?.price.toString() || "",
- type: book?.type || "",
- typeDisplayName:
- book?.type && i18n.t("BookStore::Enum:BookType." + book.type),
- publishDate: (book?.publishDate && new Date(book?.publishDate)) || null,
- //Update initialValues
- },
- onSubmit,
- });
-
- //Others codes..
-}
-
-//Other codes..
-```
-
-- `book` is a nullable property. It will store the selected book, if the book parameter is null then we will create a new book.
-
-
-
-
-
-## Delete a Book
-
-Replace the code below in the `BooksScreen.tsx` file under the `./src/screens/BookStore/Books` folder.
-
-```tsx
-import { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { Alert, View, StyleSheet } from 'react-native';
-import { List, IconButton, AnimatedFAB } from 'react-native-paper';
-import { useActionSheet } from '@expo/react-native-action-sheet';
-import i18n from 'i18n-js';
-
-import { getList, remove } from '../../../api/BookAPI';
-import { DataList } from '../../../components';
-import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
-import { useThemeColors } from '../../../hooks';
-
-function BooksScreen({ navigation }) {
- const { background, primary } = useThemeColors();
- const currentUser = useSelector(createAppConfigSelector())?.currentUser;
- const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
-
- const [refresh, setRefresh] = useState(null);
- const { showActionSheetWithOptions } = useActionSheet();
-
- const openContextMenu = (item: { id: string }) => {
- const options = [];
-
- if (policies['BookStore.Books.Delete']) {
- options.push(i18n.t('AbpUi::Delete'));
- }
-
- if (policies['BookStore.Books.Edit']) {
- options.push(i18n.t('AbpUi::Edit'));
- }
-
- options.push(i18n.t('AbpUi::Cancel'));
-
- showActionSheetWithOptions(
- {
- options,
- cancelButtonIndex: options.length - 1,
- destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
- },
- index => {
- switch (options[index]) {
- case i18n.t('AbpUi::Edit'):
- edit(item);
- break;
- case i18n.t('AbpUi::Delete'):
- removeOnClick(item);
- break;
- }
- },
- );
- };
-
- const removeOnClick = (item: { id: string }) => {
- Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
- {
- text: i18n.t('AbpUi::Cancel'),
- style: 'cancel',
- },
- {
- style: 'default',
- text: i18n.t('AbpUi::Ok'),
- onPress: () => {
- remove(item.id).then(() => {
- setRefresh((refresh ?? 0) + 1);
- });
- },
- },
- ]);
- };
-
- const edit = (item: { id: string }) => {
- navigation.navigate('CreateUpdateBook', { bookId: item.id });
- };
-
- return (
-
- {currentUser?.isAuthenticated && (
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- },
- fabStyle: {
- bottom: 16,
- right: 16,
- position: 'absolute',
- },
-});
-
-export default BooksScreen;
-```
-
-- `Delete` option is added to context menu list
-- `removeOnClick` method will handle the delete process. It'll show an alert before the delete operation.
-
-
-
-
-
-## Authorization
-
-### Hide Books item in tab
-
-Add `grantedPolicies` to the policies variable from the `appConfig` store
-
-```tsx
-//Other imports..
-import { useSelector } from "react-redux";
-
-function BookStoreScreen({ navigation }) {
- const [index, setIndex] = React.useState(0);
- const [routes, setRoutes] = React.useState([]);
-
- const currentUser = useSelector((state) => state.app.appConfig.currentUser);
- const policies = useSelector(
- (state) => state.app.appConfig.auth.grantedPolicies
- );
-
- const renderScene = BottomNavigation.SceneMap({
- books: () => BooksRoute(navigation),
- });
-
- React.useEffect(() => {
- if (!currentUser?.isAuthenticated || !policies) {
- setRoutes([]);
- return;
- }
-
- let _routes = [];
-
- if (!!policies["BookStore.Books"]) {
- _routes.push({
- key: "books",
- title: i18n.t("BookStore::Menu:Books"),
- focusedIcon: "book",
- unfocusedIcon: "book-outline",
- });
- }
-
- setRoutes([..._routes]);
- }, [Object.keys(policies)?.filter((f) => f.startsWith("BookStore")).length]);
-
- return (
- routes?.length > 0 && (
-
- )
- );
-}
-
-export default BookStoreScreen;
-```
-
-- In the `useEffect` function we'll check the `currentUser` and `policies` variables.
-- useEffect's conditions will be the policies of the `BookStore` permission group.
-- `Books` tab will be shown if the user has the `BookStore.Books` permission
-
-
+Also extend `BottomTabParamList`:
-### Hide the New Book Button
+```ts
+export type BottomTabParamList = {
+ HomeTab: undefined;
+ BookStoreTab: undefined;
+ SettingsTab: undefined;
+ AccountTab: undefined;
+};
+```
-`New Book` button is placed in the BooksScreen as a `+` icon button. For the toggle visibility of the button, we need to add the `policies` variable to the `BooksScreen` component like the `BookStoreScreen` component. Open the `BooksScreen.tsx` file in the `./src/screens/BookStore/Books` folder and include the code below.
+Then create the stack navigator:
```tsx
-//Imports..
+// ./src/navigators/BookStoreNavigator.tsx
+import { useContext } from 'react';
+import { Pressable, Text } from 'react-native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
-function BooksScreen({ navigation }) {
- const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
+import { useThemeColors } from '../hooks';
+import { LocalizationContext } from '../contexts/LocalizationContext';
+import {
+ BookStoreScreen,
+ CreateUpdateBookScreen,
+ CreateUpdateAuthorScreen,
+} from '../screens';
+import type { BookStoreStackParamList } from './types';
- //Other codes..
+const Stack = createNativeStackNavigator();
+
+export default function BookStoreStackNavigator() {
+ const { headerBg, headerText, accentColor } = useThemeColors();
+ const { t } = useContext(LocalizationContext);
return (
- {/*Other codes..*/}
-
- {currentUser?.isAuthenticated &&
- !!policies['BookStore.Books.Create'] && //Add this line
- (
- navigation.navigate('CreateUpdateBook')}
- visible={true}
- animateFrom={'right'}
- iconMode={'static'}
- style={[styles.fabStyle, { backgroundColor: primary }]}
- />
- )
- }
- )
+
+
+ ({
+ title: t(route.params?.bookId ? 'BookStore::Edit' : 'BookStore::NewBook'),
+ headerStyle: { backgroundColor: headerBg },
+ headerTintColor: headerText,
+ headerShadowVisible: false,
+ headerRight: () => (
+ navigation.goBack()} hitSlop={8}>
+ {t('AbpUi::Cancel')}
+
+ ),
+ })}
+ />
+ ({
+ title: t(route.params?.authorId ? 'BookStore::Edit' : 'BookStore::NewAuthor'),
+ headerStyle: { backgroundColor: headerBg },
+ headerTintColor: headerText,
+ headerShadowVisible: false,
+ headerRight: () => (
+ navigation.goBack()} hitSlop={8}>
+ {t('AbpUi::Cancel')}
+
+ ),
+ })}
+ />
+
+ );
}
```
-- Now the `+` icon button will be shown if the user has the `BookStore.Books.Create` permission.
-
-
+The screens referenced in the imports above will be created in the next sections.
-### Hide the Edit and Delete Actions
+## Adding BookStore to the BottomTabNavigator
-Update your code as below in the `./src/screens/BookStore/Books/BooksScreen.tsx` file. We'll check the `policies` variables for the `Edit` and `Delete` actions.
+Open `./src/navigators/BottomTabNavigator.tsx` and add a `BookStoreTab` between `HomeTab` and `SettingsTab`. The tab is shown only when the user has at least one of the BookStore permissions:
```tsx
-function BooksScreen() {
- //...
-
- const openContextMenu = (item) => {
- const options = [];
+// ./src/navigators/BottomTabNavigator.tsx
+import { useContext } from 'react';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Ionicons } from '@expo/vector-icons';
+import { useSelector } from 'react-redux';
- if (policies["BookStore.Books.Delete"]) {
- options.push(i18n.t("AbpUi::Delete"));
- }
+import { useThemeColors } from '../hooks';
+import { LocalizationContext } from '../contexts/LocalizationContext';
+import { appConfigSelector } from '../store/selectors/AppSelectors';
- if (policies["BookStore.Books.Update"]) {
- options.push(i18n.t("AbpUi::Edit"));
- }
+import HomeStackNavigator from './HomeNavigator';
+import SettingsStackNavigator from './SettingsNavigator';
+import AccountStackNavigator from './AccountNavigator';
+import BookStoreStackNavigator from './BookStoreNavigator';
- options.push(i18n.t("AbpUi::Cancel"));
- };
+const Tab = createBottomTabNavigator();
- //...
-}
-```
+export default function BottomTabNavigator() {
+ const { headerBg, accentColor, iconColor } = useThemeColors();
+ const { t } = useContext(LocalizationContext);
-
+ const policies = useSelector(appConfigSelector)?.auth?.grantedPolicies ?? {};
+ const showBookStore = !!policies['BookStore.Books'] || !!policies['BookStore.Authors'];
-## Author
+ return (
+
+
+
+ {showBookStore ? (
+ (
+
+ ),
+ }}
+ />
+ ) : null}
-### Create API Proxy
+
+
+
+ );
+}
+```
-```ts
-//./src/api/AuthorAPI.ts
+> Earlier versions of the template used a `DrawerNavigator`. The 2026 template defaults to `bottom-tab` instead. If your project still uses the drawer (the optional `navigation_type = "drawer"` configuration), add the same conditional `Drawer.Screen` to `DrawerNavigator.tsx` instead.
-import api from './API';
+
+
-export const getList = () => api.get('/api/app/author').then(({ data }) => data);
+## Creating the BookStoreScreen
-export const get = id => api.get(`/api/app/author/${id}`).then(({ data }) => data);
+`BookStoreScreen` is the root of the stack. It hosts a small NativeWind-based tab header that switches between the **Books** and **Authors** lists. Each tab is rendered only if the user has the corresponding permission.
-export const create = input => api.post('/api/app/author', input).then(({ data }) => data);
+```tsx
+// ./src/screens/BookStore/BookStoreScreen.tsx
+import { useContext, useEffect, useMemo, useState } from 'react';
+import { View, Text, Pressable } from 'react-native';
+import { useSelector } from 'react-redux';
-export const update = (input, id) => api.put(`/api/app/author/${id}`, input).then(({ data }) => data);
+import { LocalizationContext } from '../../contexts/LocalizationContext';
+import { appConfigSelector } from '../../store/selectors/AppSelectors';
+import type { BookStoreScreenProps } from '../../navigators/types';
-export const remove = id => api.delete(`/api/app/author/${id}`).then(({ data }) => data);
-```
+import BooksScreen from './Books/BooksScreen';
+import AuthorsScreen from './Authors/AuthorsScreen';
-## The Author List Page
+type TabKey = 'books' | 'authors';
+interface TabDef { key: TabKey; label: string; }
-### Add Authors Tab to BookStoreScreen
+function BookStoreScreen({ navigation }: BookStoreScreenProps) {
+ const { t } = useContext(LocalizationContext);
+ const policies = useSelector(appConfigSelector)?.auth?.grantedPolicies ?? {};
-Open the `./src/screens/BookStore/BookStoreScreen.tsx` file and update it with the code below.
+ const tabs = useMemo(() => {
+ const list: TabDef[] = [];
+ if (policies['BookStore.Books']) list.push({ key: 'books', label: t('BookStore::Menu:Books') });
+ if (policies['BookStore.Authors']) list.push({ key: 'authors', label: t('BookStore::Menu:Authors') });
+ return list;
+ }, [policies, t]);
-```tsx
-//Other imports
-import AuthorsScreen from "./Authors/AuthorsScreen";
+ const [activeKey, setActiveKey] = useState(tabs[0]?.key);
-//Other Routes..
-const AuthorsRoute = (nav) => ;
+ useEffect(() => {
+ if (!tabs.find(tab => tab.key === activeKey)) setActiveKey(tabs[0]?.key);
+ }, [tabs, activeKey]);
-function BookStoreScreen({ navigation }) {
- //Other codes..
+ if (tabs.length === 0) {
+ return (
+
+
+ {t('BookStore::NoAccess')}
+
+
+ );
+ }
- const renderScene = BottomNavigation.SceneMap({
- books: () => BooksRoute(navigation),
- authors: () => AuthorsRoute(navigation), //Added this line
- });
+ return (
+
+
+ {tabs.map(tab => {
+ const isActive = activeKey === tab.key;
+ return (
+ setActiveKey(tab.key)}
+ className={`flex-1 py-3 items-center border-b-2 ${
+ isActive ? 'border-accent dark:border-accent-dark' : 'border-transparent'
+ }`}>
+
+ {tab.label}
+
+
+ );
+ })}
+
- //Added this
- if (!!policies["BookStore.Authors"]) {
- _routes.push({
- key: "authors",
- title: i18n.t("BookStore::Menu:Authors"),
- focusedIcon: "account-supervisor",
- unfocusedIcon: "account-supervisor-outline",
- });
- }
- //Added this
+
+ {activeKey === 'books' ? : null}
+ {activeKey === 'authors' ? : null}
+
+
+ );
}
export default BookStoreScreen;
```
-Create a `AuthorsScreen.tsx` file under the `./src/screens/BookStore/Authors` folder and add the code below to it.
+The previous template used `react-native-paper`'s `BottomNavigation` for this. Building the tab strip with two `Pressable`s and NativeWind classes keeps the rest of the screen consistent with the modernized look and avoids paying for an extra Paper component in the bundle.
+
+## The Book List Page
+
+Create `./src/screens/BookStore/Books/BooksScreen.tsx`. The list itself is a single `DataList`. Each row is a `Pressable` that opens an action sheet with **Edit** and **Delete** entries — both gated by the corresponding permission. The floating "+" button at the bottom right is rendered only when the user has `BookStore.Books.Create`.
```tsx
-import { useState } from 'react';
+// ./src/screens/BookStore/Books/BooksScreen.tsx
+import { useContext, useState } from 'react';
+import { Alert, View, Text, Pressable } from 'react-native';
import { useSelector } from 'react-redux';
-import { Alert, View, StyleSheet } from 'react-native';
-import { List, IconButton, AnimatedFAB } from 'react-native-paper';
import { useActionSheet } from '@expo/react-native-action-sheet';
-import i18n from 'i18n-js';
+import { Ionicons } from '@expo/vector-icons';
-import { getList, remove } from '../../../api/AuthorAPI';
-import { DataList } from '../../../components';
-import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
+import { LocalizationContext } from '../../../contexts/LocalizationContext';
+import { appConfigSelector } from '../../../store/selectors/AppSelectors';
import { useThemeColors } from '../../../hooks';
+import { DataList } from '../../../components';
+import { getList, remove } from '../../../api/BookAPI';
+import type { BookStoreScreenProps } from '../../../navigators/types';
-function AuthorsScreen({ navigation }) {
- const { background, primary } = useThemeColors();
- const currentUser = useSelector(createAppConfigSelector())?.currentUser;
- const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
+interface BookListItem {
+ id: string;
+ name: string;
+ authorName: string;
+ type: number;
+}
- const [refresh, setRefresh] = useState(null);
- const { showActionSheetWithOptions } = useActionSheet();
+interface BooksScreenInnerProps { navigation: BookStoreScreenProps['navigation']; }
- const openContextMenu = (item: { id: string }) => {
- const options = [];
+function BooksScreen({ navigation }: BooksScreenInnerProps) {
+ const { t } = useContext(LocalizationContext);
+ const { accentColor, iconColor } = useThemeColors();
+ const policies = useSelector(appConfigSelector)?.auth?.grantedPolicies ?? {};
- if (policies['BookStore.Authors.Delete']) {
- options.push(i18n.t('AbpUi::Delete'));
- }
+ const [refresh, setRefresh] = useState(0);
+ const { showActionSheetWithOptions } = useActionSheet();
- if (policies['BookStore.Authors.Edit']) {
- options.push(i18n.t('AbpUi::Edit'));
- }
+ const canCreate = !!policies['BookStore.Books.Create'];
+ const canEdit = !!policies['BookStore.Books.Edit'];
+ const canDelete = !!policies['BookStore.Books.Delete'];
- options.push(i18n.t('AbpUi::Cancel'));
+ const openContextMenu = (item: BookListItem) => {
+ const options: string[] = [];
+ if (canEdit) options.push(t('BookStore::Edit'));
+ if (canDelete) options.push(t('AbpUi::Delete'));
+ options.push(t('AbpUi::Cancel'));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
- destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
+ destructiveButtonIndex: canDelete ? options.indexOf(t('AbpUi::Delete')) : undefined,
},
- (index: number) => {
- switch (options[index]) {
- case i18n.t('AbpUi::Edit'):
- edit(item);
- break;
- case i18n.t('AbpUi::Delete'):
- removeOnClick(item);
- break;
- }
+ (index?: number) => {
+ if (index === undefined) return;
+ const selected = options[index];
+ if (selected === t('BookStore::Edit')) navigation.navigate('CreateUpdateBook', { bookId: item.id });
+ else if (selected === t('AbpUi::Delete')) confirmDelete(item);
},
);
};
- const removeOnClick = ({ id }: { id: string }) => {
- Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
- {
- text: i18n.t('AbpUi::Cancel'),
- style: 'cancel',
- },
+ const confirmDelete = (item: BookListItem) => {
+ Alert.alert(t('AbpUi::AreYouSure'), t('BookStore::AreYouSureToDelete'), [
+ { text: t('AbpUi::Cancel'), style: 'cancel' },
{
- style: 'default',
- text: i18n.t('AbpUi::Ok'),
- onPress: () => {
- remove(id).then(() => {
- setRefresh((refresh ?? 0) + 1);
- });
+ text: t('AbpUi::Ok'),
+ style: 'destructive',
+ onPress: async () => {
+ await remove(item.id);
+ setRefresh(prev => prev + 1);
},
},
]);
};
- const edit = ({ id }: { id: string }) => {
- navigation.navigate('CreateUpdateAuthor', { authorId: id });
- };
-
return (
-
- {currentUser?.isAuthenticated && (
-