From 6f5967bbc1d0e4dacfd7a55f3e1e187fac66f72f Mon Sep 17 00:00:00 2001 From: mehdi Date: Fri, 15 Mar 2024 15:30:53 +0100 Subject: [PATCH 1/4] feat: new version for react native generator --- src/generators.js | 3 + src/generators/ReactNativeGeneratorV2.js | 188 ++++++++++++++++++ src/generators/ReactNativeGeneratorV2.test.js | 60 ++++++ templates/react-native-v2/app/(tabs)/foos.tsx | 73 +++++++ .../react-native-v2/app/_layout.tsx.dist | 39 ++++ templates/react-native-v2/components/Main.tsx | 16 ++ .../react-native-v2/components/Navigation.tsx | 64 ++++++ .../components/StoreProvider.tsx | 17 ++ .../components/foo/CreateEditModal.tsx | 67 +++++++ .../react-native-v2/components/foo/Form.tsx | 120 +++++++++++ .../components/foo/LogsRenderer.tsx | 47 +++++ templates/react-native-v2/lib/api/fooApi.ts | 43 ++++ .../react-native-v2/lib/factory/logFactory.ts | 16 ++ templates/react-native-v2/lib/hooks.ts | 7 + .../react-native-v2/lib/slices/fooSlice.ts | 69 +++++++ templates/react-native-v2/lib/store.ts | 16 ++ .../react-native-v2/lib/types/ApiResource.ts | 3 + .../react-native-v2/lib/types/HydraView.ts | 6 + templates/react-native-v2/lib/types/Logs.ts | 9 + templates/react-native-v2/lib/types/foo.ts | 7 + 20 files changed, 870 insertions(+) create mode 100644 src/generators/ReactNativeGeneratorV2.js create mode 100644 src/generators/ReactNativeGeneratorV2.test.js create mode 100644 templates/react-native-v2/app/(tabs)/foos.tsx create mode 100644 templates/react-native-v2/app/_layout.tsx.dist create mode 100644 templates/react-native-v2/components/Main.tsx create mode 100644 templates/react-native-v2/components/Navigation.tsx create mode 100644 templates/react-native-v2/components/StoreProvider.tsx create mode 100644 templates/react-native-v2/components/foo/CreateEditModal.tsx create mode 100644 templates/react-native-v2/components/foo/Form.tsx create mode 100644 templates/react-native-v2/components/foo/LogsRenderer.tsx create mode 100644 templates/react-native-v2/lib/api/fooApi.ts create mode 100644 templates/react-native-v2/lib/factory/logFactory.ts create mode 100644 templates/react-native-v2/lib/hooks.ts create mode 100644 templates/react-native-v2/lib/slices/fooSlice.ts create mode 100644 templates/react-native-v2/lib/store.ts create mode 100644 templates/react-native-v2/lib/types/ApiResource.ts create mode 100644 templates/react-native-v2/lib/types/HydraView.ts create mode 100644 templates/react-native-v2/lib/types/Logs.ts create mode 100644 templates/react-native-v2/lib/types/foo.ts diff --git a/src/generators.js b/src/generators.js index 91247c1d..6e6cbdf4 100644 --- a/src/generators.js +++ b/src/generators.js @@ -3,6 +3,7 @@ import NextGenerator from "./generators/NextGenerator.js"; import NuxtGenerator from "./generators/NuxtGenerator.js"; import ReactGenerator from "./generators/ReactGenerator.js"; import ReactNativeGenerator from "./generators/ReactNativeGenerator.js"; +import ReactNativeGeneratorV2 from "./generators/ReactNativeGeneratorV2.js"; import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator.js"; import VueGenerator from "./generators/VueGenerator.js"; import VuetifyGenerator from "./generators/VuetifyGenerator.js"; @@ -28,6 +29,8 @@ export default async function generators(generator = "react") { return wrap(ReactGenerator); case "react-native": return wrap(ReactNativeGenerator); + case "react-native-v2": + return wrap(ReactNativeGeneratorV2); case "typescript": return wrap(TypescriptInterfaceGenerator); case "vue": diff --git a/src/generators/ReactNativeGeneratorV2.js b/src/generators/ReactNativeGeneratorV2.js new file mode 100644 index 00000000..fdfd9d7c --- /dev/null +++ b/src/generators/ReactNativeGeneratorV2.js @@ -0,0 +1,188 @@ +import chalk from "chalk"; +import handlebars from "handlebars"; +import BaseGenerator from "./BaseGenerator.js"; +import hbhComparison from "handlebars-helpers/lib/comparison.js"; + +export default class extends BaseGenerator { + constructor(params) { + super(params); + + handlebars.registerHelper("ifNotResource", function (item, options) { + if (item === null) { + return options.fn(this); + } + return options.inverse(this); + }); + + this.registerTemplates(`react-native-v2/`, [ + "app/(tabs)/foos.tsx", + "app/_layout.tsx.dist", + "lib/hooks.ts", + "lib/store.ts", + "lib/types/ApiResource.ts", + "lib/types/HydraView.ts", + "lib/types/Logs.ts", + "lib/types/foo.ts", + "lib/factory/logFactory.ts", + "lib/slices/fooSlice.ts", + "lib/api/fooApi.ts", + "components/Main.tsx", + "components/Navigation.tsx", + "components/StoreProvider.tsx", + "components/foo/CreateEditModal.tsx", + "components/foo/Form.tsx", + "components/foo/LogsRenderer.tsx", + ]); + + handlebars.registerHelper("compare", hbhComparison.compare); + } + + help(resource) { + const titleLc = resource.title.toLowerCase(); + + console.log( + 'Code for the "%s" resource type has been generated!', + resource.title + ); + + console.log("You must now configure the lib/store.ts"); + console.log( + chalk.green(` + // imports for ${titleLc} + import ${titleLc}Slice from './slices/${titleLc}Slice'; + import { ${titleLc}Api } from './api/${titleLc}Api'; + + // reducer for ${titleLc} + reducer: { + ... + ${titleLc}: ${titleLc}Slice, + [${titleLc}Api.reducerPath]: ${titleLc}Api.reducer, + } + + // middleware for ${titleLc} + getDefaultMiddleware().concat(..., ${titleLc}Api.middleware) + `) + ); + + console.log( + "You should replace app/_layout.tsx by the generated one and add the following route:" + ); + console.log( + chalk.green(` + + + tabs: { + ... + ${titleLc}: { + title: '${titleLc}', + headerShown: false, + tabBarIcon: ({ color }) => , + }, + } + `) + ); + } + + generate(api, resource, dir) { + const lc = resource.title.toLowerCase(); + const titleUcFirst = + resource.title.charAt(0).toUpperCase() + resource.title.slice(1); + const fields = this.parseFields(resource); + + const context = { + title: resource.title, + name: resource.name, + lc, + uc: resource.title.toUpperCase(), + fields, + formFields: this.buildFields(fields), + hydraPrefix: this.hydraPrefix, + ucf: titleUcFirst, + }; + + // Create directories + // These directories may already exist + [ + `${dir}/app/(tabs)`, + `${dir}/config`, + `${dir}/components`, + `${dir}/components/${lc}`, + `${dir}/lib`, + `${dir}/lib/api`, + `${dir}/lib/factory`, + `${dir}/lib/slices`, + `${dir}/lib/types`, + ].forEach((dir) => this.createDir(dir, false)); + + // static files + [ + "lib/hooks.ts", + "lib/store.ts", + "lib/types/ApiResource.ts", + "lib/types/HydraView.ts", + "lib/types/Logs.ts", + "lib/factory/logFactory.ts", + "components/Main.tsx", + "components/Navigation.tsx", + "components/StoreProvider.tsx", + ].forEach((file) => this.createFile(file, `${dir}/${file}`)); + + // templated files ucFirst + ["lib/types/%s.ts"].forEach((pattern) => + this.createFileFromPattern(pattern, dir, [titleUcFirst], context) + ); + + // templated files lc + [ + "app/(tabs)/%ss.tsx", + "app/_layout.tsx.dist", + "lib/slices/%sSlice.ts", + "lib/api/%sApi.ts", + "components/%s/CreateEditModal.tsx", + "components/%s/Form.tsx", + "components/%s/LogsRenderer.tsx", + ].forEach((pattern) => + this.createFileFromPattern(pattern, dir, [lc], context) + ); + + this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`); + } + + getDescription(field) { + return field.description ? field.description.replace(/"/g, "'") : ""; + } + + parseFields(resource) { + const fields = [ + ...resource.writableFields, + ...resource.readableFields, + ].reduce((list, field) => { + if (list[field.name]) { + return list; + } + + const isReferences = Boolean( + field.reference && field.maxCardinality !== 1 + ); + const isEmbeddeds = Boolean(field.embedded && field.maxCardinality !== 1); + + return { + ...list, + [field.name]: { + ...field, + type: this.getType(field), + description: this.getDescription(field), + readonly: false, + isReferences, + isEmbeddeds, + isRelations: isEmbeddeds || isReferences, + }, + }; + }, {}); + + return Object.values(fields); + } +} diff --git a/src/generators/ReactNativeGeneratorV2.test.js b/src/generators/ReactNativeGeneratorV2.test.js new file mode 100644 index 00000000..2981cf64 --- /dev/null +++ b/src/generators/ReactNativeGeneratorV2.test.js @@ -0,0 +1,60 @@ +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; +import tmp from "tmp"; +import ReactNativeGeneratorV2 from "./ReactNativeGeneratorV2.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +test("Generate a React Native V2 app", () => { + const generator = new ReactNativeGeneratorV2({ + hydraPrefix: "hydra:", + templateDirectory: `${dirname}/../../templates`, + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + + const fields = [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL", + }), + ]; + const resource = new Resource("abc", "http://example.com/foos", { + id: "abc", + title: "abc", + readableFields: fields, + writableFields: fields, + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource], + }); + generator.generate(api, resource, tmpobj.name); + + [ + "/lib/hooks.ts", + "/lib/store.ts", + "/lib/types/ApiResource.ts", + "/lib/types/HydraView.ts", + "/lib/types/Logs.ts", + "/lib/factory/logFactory.ts", + "/components/Main.tsx", + "/components/Navigation.tsx", + "/components/StoreProvider.tsx", + "/app/_layout.tsx.dist", + + "/app/(tabs)/abcs.tsx", + "/lib/slices/abcSlice.ts", + "/lib/api/abcApi.ts", + "/components/abc/CreateEditModal.tsx", + "/components/abc/Form.tsx", + "/components/abc/LogsRenderer.tsx", + ].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true)); + + tmpobj.removeCallback(); +}); diff --git a/templates/react-native-v2/app/(tabs)/foos.tsx b/templates/react-native-v2/app/(tabs)/foos.tsx new file mode 100644 index 00000000..a65beceb --- /dev/null +++ b/templates/react-native-v2/app/(tabs)/foos.tsx @@ -0,0 +1,73 @@ +import Main from "@/components/Main"; +import Navigation from "@/components/Navigation"; +import CreateEditModal from "@/components/{{{lc}}}/CreateEditModal"; +import LogsRenderer from "@/components/{{{lc}}}/LogsRenderer"; +import { useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api"; +import { useAppDispatch, useAppSelector } from "@/lib/hooks"; +import { setCurrentData, setData, setModalIsEdit, setModalIsVisible, setPage, setView } from "@/lib/slices/{{{lc}}}Slice"; +import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; +import { useLocalSearchParams } from "expo-router"; +import { useEffect } from "react"; +import { Pressable, ScrollView, Text, View } from "react-native"; + +export default function {{{ucf}}}s() { + const datas = useAppSelector(state => state.{{lc}}.data); + const view = useAppSelector(state => state.{{{lc}}}.view); + const { page = '1' } = useLocalSearchParams<{ page: string }>(); + + const dispatch = useAppDispatch(); + const [getAll] = useLazyGetAllQuery(); + + const toggleEditModal = (data: {{{ucf}}}) => { + dispatch(setCurrentData(data)); + dispatch(setModalIsVisible(true)); + dispatch(setModalIsEdit(true)); + }; + + const toggleCreateModal = () => { + dispatch(setModalIsVisible(true)); + dispatch(setModalIsEdit(false)); + } + + useEffect(() => { + const intPage = parseInt(page); + if (intPage < 0) return; + dispatch(setPage(intPage)); + getAll(intPage) + .unwrap() + .then(fulfilled => { + dispatch(setView(fulfilled["hydra:view"])); + dispatch(setData(fulfilled["hydra:member"])); + }) + }, [page]); + + return ( +
+ + {{{ucf}}}s List + toggleCreateModal()}> + Create + + + + + + { + datas.map(data => ( + toggleEditModal(data)} key={data["@id"]}> + + ID: {data['@id']} + {{#each fields}} + {{{name}}}: {data["{{{name}}}"]} + {{/each}} + + + )) + } + + + + +
+ ); +} \ No newline at end of file diff --git a/templates/react-native-v2/app/_layout.tsx.dist b/templates/react-native-v2/app/_layout.tsx.dist new file mode 100644 index 00000000..aa787aec --- /dev/null +++ b/templates/react-native-v2/app/_layout.tsx.dist @@ -0,0 +1,39 @@ +import React from 'react'; +import { FontAwesome } from "@expo/vector-icons"; +import "../global.css"; +import { Tabs } from "expo-router"; +import StoreProvider from '@/components/StoreProvider'; + +function TabBarIcon(props: { + name: React.ComponentProps['name']; + color: string; +}) { + return ; +} +const iconMargin = { marginBottom: -3 } + +export default function Layout() { + return ( + + + + + + ) +} + +const options = { + tabsContainer: { + headerShown: false, + tabBarShowLabel: false, + }, + tabs: { + home: { + title: 'Accueil', + tabBarIcon: ({ color }) => , + }, + } +}; diff --git a/templates/react-native-v2/components/Main.tsx b/templates/react-native-v2/components/Main.tsx new file mode 100644 index 00000000..fa5fcb14 --- /dev/null +++ b/templates/react-native-v2/components/Main.tsx @@ -0,0 +1,16 @@ +import { View } from "react-native"; + +export default function Main({ children }) { + return ( + + {children} + + ) +} + +const styles = { + container: { + position: 'relative', + marginHorizontal: '3%', + } +} \ No newline at end of file diff --git a/templates/react-native-v2/components/Navigation.tsx b/templates/react-native-v2/components/Navigation.tsx new file mode 100644 index 00000000..0332f5cb --- /dev/null +++ b/templates/react-native-v2/components/Navigation.tsx @@ -0,0 +1,64 @@ +import { HydraView } from "@/lib/types/HydraView"; +import { FontAwesome } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; +import { Pressable, View } from "react-native"; + +function NavigationIcon(props: { + name: React.ComponentProps['name']; + color: string; +}) { + return ; +} +const iconMargin = { marginBottom: -3 } + +export default function Navigation(props: { view: HydraView }) { + const view = props.view; + if (!view) { + return null; + } + + const router = useRouter(); + + const { + "hydra:first": first, + "hydra:previous": previous, + "hydra:next": next, + "hydra:last": last, + } = view; + + return ( + + { + if (first) router.navigate(first) + }}> + + + + { + if (previous) router.navigate(previous) + }}> + + + + { + if (next) router.navigate(next) + }}> + + + + { + if (last) router.navigate(last) + }}> + + + + ); +} + +const styles = { + container: { + position: 'absolute', + bottom: 5, + minWidth: '100%' + } +} \ No newline at end of file diff --git a/templates/react-native-v2/components/StoreProvider.tsx b/templates/react-native-v2/components/StoreProvider.tsx new file mode 100644 index 00000000..92936376 --- /dev/null +++ b/templates/react-native-v2/components/StoreProvider.tsx @@ -0,0 +1,17 @@ +import { useRef } from 'react' +import { Provider } from 'react-redux' +import { makeStore, AppStore } from '@/lib/store' + +export default function StoreProvider({ + children +}: { + children: React.ReactNode +}) { + const storeRef = useRef() + + if (!storeRef.current) { + storeRef.current = makeStore() + } + + return {children} +} diff --git a/templates/react-native-v2/components/foo/CreateEditModal.tsx b/templates/react-native-v2/components/foo/CreateEditModal.tsx new file mode 100644 index 00000000..b5fed4bb --- /dev/null +++ b/templates/react-native-v2/components/foo/CreateEditModal.tsx @@ -0,0 +1,67 @@ +import { useAppSelector } from "@/lib/hooks"; +import { Modal, Pressable, Text, View } from "react-native"; +import { useDispatch } from "react-redux"; +import Form from "./Form"; +import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice"; +import { useDeleteMutation, useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api"; +import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory"; + +export default function CreateEditModal() { + const {{{lc}}}State = useAppSelector(state => state.{{{lc}}}); + const { modalState, currentData, page } = {{{lc}}}State; + const dispatch = useDispatch(); + const [deleteMutation] = useDeleteMutation(); + const [getAll] = useLazyGetAllQuery(); + + function handleDelete() { + deleteMutation(currentData['@id']) + .unwrap() + .then(() => { + dispatch(addLog(createSuccessLog(`{{{ucf}}} ${currentData['@id']} has been deleted successfully.`))) + getAll(page) + .unwrap() + .then(fulfilled => { + dispatch(setModalIsVisible(false)); + dispatch(setView(fulfilled["hydra:view"])); + dispatch(setData(fulfilled["hydra:member"])); + }); + }) + .catch(error => { + if (error.data) { + dispatch(addLog(createErrorLog(`Error: ${error.data["hydra:description"]}`))) + } + }); + } + + return ( + + + + {modalState.edit ? `Edit {{{ucf}}}` : 'Create a new {{{ucf}}}'} +
+ { + modalState.edit && + handleDelete()}> + Delete + + } + dispatch(setModalIsVisible(false))}> + Close + + + + + ) +} + +const styles = { + container: { height: '80%', width: '100%', backgroundColor: '#e3e9e5' }, + closeButton: { position: 'absolute', right: 5, top: 5 } +} \ No newline at end of file diff --git a/templates/react-native-v2/components/foo/Form.tsx b/templates/react-native-v2/components/foo/Form.tsx new file mode 100644 index 00000000..424b0dea --- /dev/null +++ b/templates/react-native-v2/components/foo/Form.tsx @@ -0,0 +1,120 @@ +import { useCreateMutation, useLazyGetAllQuery, useUpdateMutation } from "@/lib/api/{{{lc}}}Api"; +import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory"; +import { useAppSelector } from "@/lib/hooks"; +import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice"; +import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; +import { useState } from "react"; +import { Controller, SubmitErrorHandler, useForm } from "react-hook-form"; +import { Pressable, Text, TextInput, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useDispatch } from "react-redux"; + +export default function Form() { + const [update] = useUpdateMutation(); + const [create] = useCreateMutation(); + const [getAll] = useLazyGetAllQuery(); + const {{{lc}}}Data = useAppSelector(state => state.{{{lc}}}); + const { page, currentData, modalState } = {{{lc}}}Data; + const dispatch = useDispatch(); + const [errors, setErrors] = useState([]); + const submitQuery = modalState.edit ? update : create; + + const initValues: {{{ucf}}} = modalState.edit ? currentData : { + '@id': '', + {{#each formFields}} + {{{name}}}: {{#if (compare type "==" "number")}}0{{else}}''{{/if}}, + {{/each}} + } + + const { control, handleSubmit, reset } = useForm<{{{ucf}}}>({ + defaultValues: initValues + }); + + + const onSubmit = (data: {{{ucf}}}) => { + intParser(data); + submitQuery(data) + .unwrap() + .then(() => { + dispatch(addLog(createSuccessLog(`The {{{lc}}} has been ${modalState.edit ? 'edited' : 'created'} successfully.`))) + getAll(page) + .unwrap() + .then(fulfilled => { + reset(); + dispatch(setView(fulfilled["hydra:view"])); + dispatch(setData(fulfilled["hydra:member"])); + dispatch(setModalIsVisible(false)); + }); + }) + .catch(error => { + if (error.data) { + dispatch( + addLog(createErrorLog(`Error : ${error.data["hydra:description"]}`)) + ) + } + }); + return; + }; + + const intParser = (data: {{{ucf}}}) => { + Object.keys(data).forEach(key => { + if ((typeof initValues[key] == "number") && !isNaN(parseInt(data[key]))) { + data[key] = parseInt(data[key]); + } + }); + return; + } + + const onError: SubmitErrorHandler<{{{ucf}}}> = (errors, e) => { + setErrors(Object.keys(errors)); + return; + } + + + return ( + + + {errors.length > 0 && + + + Field{errors.length > 1 ? "s" : ""} "{errors.join(', ')}" {errors.length > 1 ? "are" : "is"} empty + + + } + {{#each formFields}} + ( + + {{{name}}} : + + + )} + name="{{{name}}}" + {{#if required}}rules={fieldRequired}{{/if}} + /> + {{/each}} + + + {modalState.edit ? 'Edit' : 'Create'} + + + + ); +} + +const fieldRequired = { + required: true +} + +const styles = { + textInput: { minWidth: 200 } +} \ No newline at end of file diff --git a/templates/react-native-v2/components/foo/LogsRenderer.tsx b/templates/react-native-v2/components/foo/LogsRenderer.tsx new file mode 100644 index 00000000..4419732c --- /dev/null +++ b/templates/react-native-v2/components/foo/LogsRenderer.tsx @@ -0,0 +1,47 @@ +import { useAppSelector } from "@/lib/hooks"; +import { cleanLogs } from "@/lib/slices/{{{lc}}}Slice"; +import { Log } from "@/lib/types/Logs"; +import { Pressable, Text, View } from "react-native"; +import { useDispatch } from "react-redux"; + +export default function LogsRenderer() { + const logs = useAppSelector(state => state.{{{lc}}}.logs); + const { errors, successes } = logs; + + return ( + + { + errors.length > 0 && + + + {errors.map((error: string, index: number) => ( + - {error} + ))} + + + + } + { + successes.length > 0 && + + + {successes.map((success: string, index: number) => ( + - {success} + ))} + + + + } + + ) +} + +const CloseButton = (props: { type: keyof Log }) => { + const dispatch = useDispatch(); + + return ( + dispatch(cleanLogs(props.type))}> + X + + ) +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/api/fooApi.ts b/templates/react-native-v2/lib/api/fooApi.ts new file mode 100644 index 00000000..6fceb81e --- /dev/null +++ b/templates/react-native-v2/lib/api/fooApi.ts @@ -0,0 +1,43 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import {{{ucf}}} from '../types/{{{ucf}}}'; +import { ENTRYPOINT } from '@/config/entrypoint'; + +export const {{{lc}}}Api = createApi({ + reducerPath: '{{{lc}}}Api', + baseQuery: fetchBaseQuery({ baseUrl: ENTRYPOINT }), + endpoints: builder => ({ + getAll: builder.query({ + query: (page) => { + return `/{{{lc}}}s?page=${page}` + } + }), + delete: builder.mutation({ + query: (id) => { + return { + url: `${id}`, + method: 'DELETE' + } + } + }), + create: builder.mutation({ + query: ({{{lc}}}) => { + return { + url: `/{{{lc}}}s`, + method: 'POST', + body: {{{lc}}}, + } + } + }), + update: builder.mutation({ + query: ({{{lc}}}) => { + return { + url: {{{lc}}}['@id'], + method: 'PUT', + body: {{{lc}}}, + } + } + }), + }) +}); + +export const { useLazyGetAllQuery, useDeleteMutation, useCreateMutation, useUpdateMutation } = {{{lc}}}Api; \ No newline at end of file diff --git a/templates/react-native-v2/lib/factory/logFactory.ts b/templates/react-native-v2/lib/factory/logFactory.ts new file mode 100644 index 00000000..69af26ee --- /dev/null +++ b/templates/react-native-v2/lib/factory/logFactory.ts @@ -0,0 +1,16 @@ +import { Log, NewLog } from "../types/Logs"; + +function createLog(type: keyof Log, message: string): NewLog { + return { + type, + message + } +} + +export function createSuccessLog(message: string): NewLog { + return createLog("successes", message); +} + +export function createErrorLog(message: string): NewLog { + return createLog("errors", message); +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks.ts b/templates/react-native-v2/lib/hooks.ts new file mode 100644 index 00000000..5c8ff39c --- /dev/null +++ b/templates/react-native-v2/lib/hooks.ts @@ -0,0 +1,7 @@ +import { useDispatch, useSelector, useStore } from 'react-redux' +import type { TypedUseSelectorHook } from 'react-redux' +import type { RootState, AppDispatch, AppStore } from './store' + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector +export const useAppStore: () => AppStore = useStore \ No newline at end of file diff --git a/templates/react-native-v2/lib/slices/fooSlice.ts b/templates/react-native-v2/lib/slices/fooSlice.ts new file mode 100644 index 00000000..068480da --- /dev/null +++ b/templates/react-native-v2/lib/slices/fooSlice.ts @@ -0,0 +1,69 @@ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import {{{ucf}}} from '../types/{{{ucf}}}'; +import { HydraView } from '../types/HydraView'; +import { Log, NewLog } from '../types/Logs'; + +interface {{{ucf}}}SliceState { + page: number; + data: {{{ucf}}}[]; + currentData?: {{{ucf}}}; + modalState: ModalState; + view: HydraView; + logs: Log; +} + +interface ModalState { + open: boolean; + edit: boolean; +} + +const initialState: {{{ucf}}}SliceState = { + page: 1, + data: [], + currentData: undefined, + modalState: { + open: false, + edit: false, + }, + view: {}, + logs: { + errors: [], + successes: [], + } +} + +export const {{{lc}}}Slice = createSlice({ + name: '{{{lc}}}Slice', + initialState, + reducers: { + setPage: (state, action: PayloadAction) => { + state.page = action.payload; + }, + setData: (state, action: PayloadAction<{{{ucf}}}[]>) => { + state.data = action.payload; + }, + setView: (state, action: PayloadAction) => { + state.view = action.payload; + }, + setCurrentData: (state, action: PayloadAction<{{{ucf}}}>) => { + state.currentData = action.payload; + }, + setModalIsVisible: (state, action: PayloadAction) => { + state.modalState.open = action.payload; + }, + setModalIsEdit: (state, action: PayloadAction) => { + state.modalState.edit = action.payload; + }, + addLog: (state, action: PayloadAction) => { + state.logs[action.payload.type] = [...state.logs[action.payload.type], action.payload.message]; + }, + cleanLogs: (state, action: PayloadAction) => { + state.logs[action.payload] = []; + }, + } +}) + +export const { setPage, setData, setView, setCurrentData, setModalIsEdit, setModalIsVisible, addLog, cleanLogs } = {{{lc}}}Slice.actions; + +export default {{{lc}}}Slice.reducer; \ No newline at end of file diff --git a/templates/react-native-v2/lib/store.ts b/templates/react-native-v2/lib/store.ts new file mode 100644 index 00000000..7d829bc0 --- /dev/null +++ b/templates/react-native-v2/lib/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; + +export const makeStore = () => { + return configureStore({ + reducer: { + }, + middleware: getDefaultMiddleware => + getDefaultMiddleware() + .concat(), + }); +}; + +export type AppStore = ReturnType; +export type RootState = ReturnType; +export type AppDispatch = AppStore['dispatch']; + diff --git a/templates/react-native-v2/lib/types/ApiResource.ts b/templates/react-native-v2/lib/types/ApiResource.ts new file mode 100644 index 00000000..217fe654 --- /dev/null +++ b/templates/react-native-v2/lib/types/ApiResource.ts @@ -0,0 +1,3 @@ +export default interface ApiResource { + "@id": string; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/HydraView.ts b/templates/react-native-v2/lib/types/HydraView.ts new file mode 100644 index 00000000..c4b9e82d --- /dev/null +++ b/templates/react-native-v2/lib/types/HydraView.ts @@ -0,0 +1,6 @@ +export interface HydraView { + 'hydra:first'?: string; + 'hydra:last'?: string; + 'hydra:previous'?: string; + 'hydra:next'?: string; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/Logs.ts b/templates/react-native-v2/lib/types/Logs.ts new file mode 100644 index 00000000..46a49352 --- /dev/null +++ b/templates/react-native-v2/lib/types/Logs.ts @@ -0,0 +1,9 @@ +export interface Log { + errors: Array; + successes: Array; +} + +export interface NewLog { + type: keyof Log; + message: string; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/foo.ts b/templates/react-native-v2/lib/types/foo.ts new file mode 100644 index 00000000..d5f67698 --- /dev/null +++ b/templates/react-native-v2/lib/types/foo.ts @@ -0,0 +1,7 @@ +import ApiResource from "./ApiResource"; + +export default interface {{{ ucf }}} extends ApiResource { + {{#each fields}} + {{#if readonly}}readonly{{/if}} {{{name}}}?: {{#if (compare type "==" "Date")}}string{{else}}{{{type}}}{{/if}}; + {{/each}} +} \ No newline at end of file From 666215eb53d89b8c3e46de5fa801c7d6810359ea Mon Sep 17 00:00:00 2001 From: mehdi Date: Thu, 28 Mar 2024 15:08:22 +0100 Subject: [PATCH 2/4] feat: refacto with tanstack and mercure --- src/generators/ReactNativeGeneratorV2.js | 55 +++---- templates/react-native-v2/app/(tabs)/foos.tsx | 135 ++++++++++++------ .../react-native-v2/app/_layout.tsx.dist | 13 +- .../components/ConfirmModal.tsx | 29 ++++ .../components/StoreProvider.tsx | 17 --- .../react-native-v2/components/foo/Context.ts | 15 ++ .../components/foo/CreateEditModal.tsx | 81 ++++++----- .../react-native-v2/components/foo/Form.tsx | 71 ++++----- .../components/foo/LogsRenderer.tsx | 44 +++--- templates/react-native-v2/lib/api/fooApi.ts | 86 +++++------ .../react-native-v2/lib/factory/logFactory.ts | 16 --- templates/react-native-v2/lib/hooks.ts | 7 - templates/react-native-v2/lib/hooks/data.ts | 36 +++++ .../react-native-v2/lib/hooks/mercure.ts | 33 +++++ templates/react-native-v2/lib/hooks/modal.ts | 27 ++++ .../lib/hooks/notifications.ts | 31 ++++ .../react-native-v2/lib/slices/fooSlice.ts | 69 --------- templates/react-native-v2/lib/store.ts | 16 --- .../react-native-v2/lib/types/ApiResource.ts | 1 + .../lib/types/HydraResponse.ts | 7 + templates/react-native-v2/lib/types/Logs.ts | 9 -- templates/react-native-v2/lib/types/foo.ts | 2 +- templates/react-native-v2/lib/utils/Logs.ts | 17 +++ templates/react-native-v2/lib/utils/icons.tsx | 13 ++ .../react-native-v2/lib/utils/mercure.ts | 33 +++++ 25 files changed, 510 insertions(+), 353 deletions(-) create mode 100644 templates/react-native-v2/components/ConfirmModal.tsx delete mode 100644 templates/react-native-v2/components/StoreProvider.tsx create mode 100644 templates/react-native-v2/components/foo/Context.ts delete mode 100644 templates/react-native-v2/lib/factory/logFactory.ts delete mode 100644 templates/react-native-v2/lib/hooks.ts create mode 100644 templates/react-native-v2/lib/hooks/data.ts create mode 100644 templates/react-native-v2/lib/hooks/mercure.ts create mode 100644 templates/react-native-v2/lib/hooks/modal.ts create mode 100644 templates/react-native-v2/lib/hooks/notifications.ts delete mode 100644 templates/react-native-v2/lib/slices/fooSlice.ts delete mode 100644 templates/react-native-v2/lib/store.ts create mode 100644 templates/react-native-v2/lib/types/HydraResponse.ts delete mode 100644 templates/react-native-v2/lib/types/Logs.ts create mode 100644 templates/react-native-v2/lib/utils/Logs.ts create mode 100644 templates/react-native-v2/lib/utils/icons.tsx create mode 100644 templates/react-native-v2/lib/utils/mercure.ts diff --git a/src/generators/ReactNativeGeneratorV2.js b/src/generators/ReactNativeGeneratorV2.js index fdfd9d7c..06a34acd 100644 --- a/src/generators/ReactNativeGeneratorV2.js +++ b/src/generators/ReactNativeGeneratorV2.js @@ -17,21 +17,25 @@ export default class extends BaseGenerator { this.registerTemplates(`react-native-v2/`, [ "app/(tabs)/foos.tsx", "app/_layout.tsx.dist", - "lib/hooks.ts", - "lib/store.ts", + "lib/hooks/data.ts", + "lib/hooks/mercure.ts", + "lib/hooks/modal.ts", + "lib/hooks/notifications.ts", "lib/types/ApiResource.ts", "lib/types/HydraView.ts", - "lib/types/Logs.ts", + "lib/types/HydraResponse.ts", "lib/types/foo.ts", - "lib/factory/logFactory.ts", - "lib/slices/fooSlice.ts", + "lib/utils/Logs.ts", + "lib/utils/mercure.ts", + "lib/utils/icons.tsx", "lib/api/fooApi.ts", "components/Main.tsx", "components/Navigation.tsx", - "components/StoreProvider.tsx", + "components/ConfirmModal.tsx", "components/foo/CreateEditModal.tsx", "components/foo/Form.tsx", "components/foo/LogsRenderer.tsx", + "components/foo/Context.ts", ]); handlebars.registerHelper("compare", hbhComparison.compare); @@ -45,25 +49,6 @@ export default class extends BaseGenerator { resource.title ); - console.log("You must now configure the lib/store.ts"); - console.log( - chalk.green(` - // imports for ${titleLc} - import ${titleLc}Slice from './slices/${titleLc}Slice'; - import { ${titleLc}Api } from './api/${titleLc}Api'; - - // reducer for ${titleLc} - reducer: { - ... - ${titleLc}: ${titleLc}Slice, - [${titleLc}Api.reducerPath]: ${titleLc}Api.reducer, - } - - // middleware for ${titleLc} - getDefaultMiddleware().concat(..., ${titleLc}Api.middleware) - `) - ); - console.log( "You should replace app/_layout.tsx by the generated one and add the following route:" ); @@ -112,22 +97,26 @@ export default class extends BaseGenerator { `${dir}/components/${lc}`, `${dir}/lib`, `${dir}/lib/api`, - `${dir}/lib/factory`, - `${dir}/lib/slices`, `${dir}/lib/types`, + `${dir}/lib/hooks`, + `${dir}/lib/utils`, ].forEach((dir) => this.createDir(dir, false)); // static files [ - "lib/hooks.ts", - "lib/store.ts", "lib/types/ApiResource.ts", "lib/types/HydraView.ts", - "lib/types/Logs.ts", - "lib/factory/logFactory.ts", + "lib/types/HydraResponse.ts", + "lib/hooks/data.ts", + "lib/hooks/mercure.ts", + "lib/hooks/modal.ts", + "lib/hooks/notifications.ts", + "lib/utils/Logs.ts", + "lib/utils/mercure.ts", + "lib/utils/icons.tsx", "components/Main.tsx", "components/Navigation.tsx", - "components/StoreProvider.tsx", + "components/ConfirmModal.tsx", ].forEach((file) => this.createFile(file, `${dir}/${file}`)); // templated files ucFirst @@ -139,8 +128,8 @@ export default class extends BaseGenerator { [ "app/(tabs)/%ss.tsx", "app/_layout.tsx.dist", - "lib/slices/%sSlice.ts", "lib/api/%sApi.ts", + "components/%s/Context.ts", "components/%s/CreateEditModal.tsx", "components/%s/Form.tsx", "components/%s/LogsRenderer.tsx", diff --git a/templates/react-native-v2/app/(tabs)/foos.tsx b/templates/react-native-v2/app/(tabs)/foos.tsx index a65beceb..7d9cbcf2 100644 --- a/templates/react-native-v2/app/(tabs)/foos.tsx +++ b/templates/react-native-v2/app/(tabs)/foos.tsx @@ -2,44 +2,54 @@ import Main from "@/components/Main"; import Navigation from "@/components/Navigation"; import CreateEditModal from "@/components/{{{lc}}}/CreateEditModal"; import LogsRenderer from "@/components/{{{lc}}}/LogsRenderer"; -import { useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api"; -import { useAppDispatch, useAppSelector } from "@/lib/hooks"; -import { setCurrentData, setData, setModalIsEdit, setModalIsVisible, setPage, setView } from "@/lib/slices/{{{lc}}}Slice"; import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; -import { useLocalSearchParams } from "expo-router"; +import { useLocalSearchParams, Link } from "expo-router"; import { useEffect } from "react"; +import { useQuery } from '@tanstack/react-query' import { Pressable, ScrollView, Text, View } from "react-native"; +import { getAll } from "@/lib/api/{{{lc}}}Api"; +import { HydraResponse } from "@/lib/types/HydraResponse"; +import { {{{ucf}}}Context, {{{ucf}}}ContextData } from "@/components/{{{lc}}}/Context"; +import { useMercure } from "@/lib/hooks/mercure"; +import { useData } from "@/lib/hooks/data"; +import { useNotifications } from "@/lib/hooks/notifications"; +import { useModal } from "@/lib/hooks/modal"; +import { Icon } from "@/lib/utils/icons"; -export default function {{{ucf}}}s() { - const datas = useAppSelector(state => state.{{lc}}.data); - const view = useAppSelector(state => state.{{{lc}}}.view); +export default function Books() { const { page = '1' } = useLocalSearchParams<{ page: string }>(); + const { id = undefined } = useLocalSearchParams<{ id: Nullable }>(); - const dispatch = useAppDispatch(); - const [getAll] = useLazyGetAllQuery(); + const { member, setMember, processMercureData, view, setView, currentData, setCurrentData } = useData<{{{ucf}}}>(); + const { notifications, addNotification, clearNotifications } = useNotifications(); + const { isModalEdit, isModalVisible, toggleEditModal, toggleCreateModal, setIsModalVisible } = useModal(); - const toggleEditModal = (data: {{{ucf}}}) => { - dispatch(setCurrentData(data)); - dispatch(setModalIsVisible(true)); - dispatch(setModalIsEdit(true)); - }; + useMercure(['/{{{lc}}}s'], processMercureData); - const toggleCreateModal = () => { - dispatch(setModalIsVisible(true)); - dispatch(setModalIsEdit(false)); - } + const { isSuccess, data, isLoading, error } = useQuery>({ + queryKey: ['getAll{{{ucf}}}s', page], + queryFn: () => getAll(page), + }); useEffect(() => { - const intPage = parseInt(page); - if (intPage < 0) return; - dispatch(setPage(intPage)); - getAll(intPage) - .unwrap() - .then(fulfilled => { - dispatch(setView(fulfilled["hydra:view"])); - dispatch(setData(fulfilled["hydra:member"])); - }) - }, [page]); + if (isSuccess) { + setMember(data["hydra:member"]); + setView(data['hydra:view']); + } + }, [isSuccess, data]); + + useEffect(() => { + if (!id) return; + + const data = member.find(item => item["@id"].includes(id) == true); + if (data) { + setCurrentData(data); + toggleEditModal(); + } + }, [member, id]) + + const providerValues: {{{ucf}}}ContextData = { notifications, addNotification, clearNotifications, isModalVisible, isModalEdit, setIsModalVisible, currentData }; + const viewButtonStyle = { width: "5vw", minWidth: '100px', height: "100%" }; return (
@@ -49,24 +59,59 @@ export default function {{{ucf}}}s() { Create - - - - { - datas.map(data => ( - toggleEditModal(data)} key={data["@id"]}> - - ID: {data['@id']} - {{#each fields}} - {{{name}}}: {data["{{{name}}}"]} - {{/each}} + <{{{ucf}}}Context.Provider value={providerValues}> + + + + { + member && member.length < 1 && + + {isLoading ? 'Loading data...' : 'No data found'} + + } + { + error && + + {error.message} + + } + { + member && member.map((item: {{{ucf}}}) => ( + !item.deleted && + + + ID: {item['@id']} + {{#each fields}} + {{#if isReferences}} + {{{name}}}: + {item['{{{name}}}'].map((ref: any) => {ref})} + {{else if reference}} + {{{name}}}: {item["{{{name}}}"]} + {{else if isEmbeddeds}} + {{{name}}}: + {item['{{{name}}}'].map((emb: any) => {emb["@id"]})} + {{else if embedded}} + {{{name}}}: {item["{{{name}}}"]["@id"]} + {{else}} + {{{name}}}: {item["{{{name}}}"]} + {{/if}} + {{/each}} + + + { + setCurrentData(item); + toggleEditModal(); + }}> + + + - - )) - } - - - + )) + } + + + +
); diff --git a/templates/react-native-v2/app/_layout.tsx.dist b/templates/react-native-v2/app/_layout.tsx.dist index aa787aec..8d0825ca 100644 --- a/templates/react-native-v2/app/_layout.tsx.dist +++ b/templates/react-native-v2/app/_layout.tsx.dist @@ -2,7 +2,10 @@ import React from 'react'; import { FontAwesome } from "@expo/vector-icons"; import "../global.css"; import { Tabs } from "expo-router"; -import StoreProvider from '@/components/StoreProvider'; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query' function TabBarIcon(props: { name: React.ComponentProps['name']; @@ -13,15 +16,17 @@ function TabBarIcon(props: { const iconMargin = { marginBottom: -3 } export default function Layout() { + const queryClient = new QueryClient(); + return ( - + - + ) } @@ -36,4 +41,4 @@ const options = { tabBarIcon: ({ color }) => , }, } -}; +}; \ No newline at end of file diff --git a/templates/react-native-v2/components/ConfirmModal.tsx b/templates/react-native-v2/components/ConfirmModal.tsx new file mode 100644 index 00000000..de16ac1e --- /dev/null +++ b/templates/react-native-v2/components/ConfirmModal.tsx @@ -0,0 +1,29 @@ +import { Modal, Pressable, Text, View } from "react-native"; + +export default function ConfirmModal(props: { isVisible: boolean, onDecline: Function, onAccept: Function }) { + const { isVisible, onDecline, onAccept } = props; + + return ( + + + + Are you sure ? + + onAccept()}> + Yes + + onDecline()}> + Cancel + + + + + + ) +} + +const containerStyle = { height: '100%', width: '100%' } \ No newline at end of file diff --git a/templates/react-native-v2/components/StoreProvider.tsx b/templates/react-native-v2/components/StoreProvider.tsx deleted file mode 100644 index 92936376..00000000 --- a/templates/react-native-v2/components/StoreProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useRef } from 'react' -import { Provider } from 'react-redux' -import { makeStore, AppStore } from '@/lib/store' - -export default function StoreProvider({ - children -}: { - children: React.ReactNode -}) { - const storeRef = useRef() - - if (!storeRef.current) { - storeRef.current = makeStore() - } - - return {children} -} diff --git a/templates/react-native-v2/components/foo/Context.ts b/templates/react-native-v2/components/foo/Context.ts new file mode 100644 index 00000000..bd3f7f24 --- /dev/null +++ b/templates/react-native-v2/components/foo/Context.ts @@ -0,0 +1,15 @@ +import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; +import { Log, addNotificationFunction, clearNotificationsFunction } from "@/lib/utils/Logs"; +import { createContext } from "react"; + +export type {{{ucf}}}ContextData = { + notifications: Log[]; + addNotification: addNotificationFunction, + clearNotifications: clearNotificationsFunction, + isModalVisible: boolean; + isModalEdit: boolean; + setIsModalVisible: (visible: boolean) => void; + currentData?: {{{ucf}}}; +} + +export const {{{ucf}}}Context = createContext<{{{ucf}}}ContextData>(null); \ No newline at end of file diff --git a/templates/react-native-v2/components/foo/CreateEditModal.tsx b/templates/react-native-v2/components/foo/CreateEditModal.tsx index b5fed4bb..f0cad875 100644 --- a/templates/react-native-v2/components/foo/CreateEditModal.tsx +++ b/templates/react-native-v2/components/foo/CreateEditModal.tsx @@ -1,58 +1,73 @@ -import { useAppSelector } from "@/lib/hooks"; import { Modal, Pressable, Text, View } from "react-native"; -import { useDispatch } from "react-redux"; import Form from "./Form"; -import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice"; -import { useDeleteMutation, useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api"; -import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory"; +import { useMutation, useQueryClient } from '@tanstack/react-query' +import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; +import { remove } from "@/lib/api/{{{lc}}}Api"; +import { useContext, useEffect, useState } from "react"; +import ConfirmModal from "../ConfirmModal"; +import { {{{ucf}}}Context } from "./Context"; export default function CreateEditModal() { - const {{{lc}}}State = useAppSelector(state => state.{{{lc}}}); - const { modalState, currentData, page } = {{{lc}}}State; - const dispatch = useDispatch(); - const [deleteMutation] = useDeleteMutation(); - const [getAll] = useLazyGetAllQuery(); + const [requestDelete, setRequestDelete] = useState(false); + const queryClient = useQueryClient(); - function handleDelete() { - deleteMutation(currentData['@id']) - .unwrap() - .then(() => { - dispatch(addLog(createSuccessLog(`{{{ucf}}} ${currentData['@id']} has been deleted successfully.`))) - getAll(page) - .unwrap() - .then(fulfilled => { - dispatch(setModalIsVisible(false)); - dispatch(setView(fulfilled["hydra:view"])); - dispatch(setData(fulfilled["hydra:member"])); - }); - }) - .catch(error => { - if (error.data) { - dispatch(addLog(createErrorLog(`Error: ${error.data["hydra:description"]}`))) - } - }); + const context = useContext({{{ucf}}}Context); + const { addNotification, setIsModalVisible, isModalEdit, isModalVisible, currentData: data } = context; + + const deleteMutation = useMutation({ + mutationFn: (data: {{{ucf}}}) => remove(data), + onError: (error: string) => { + addNotification('error', error.toString()); + }, + onSuccess: (data) => { + if (data.ok) { + addNotification('success', 'The {{{lc}}} has been deleted'); + } else { + addNotification('error', `An error occured while deleting the {{{lc}}} (${data.statusText})`); + } + queryClient.invalidateQueries({ queryKey: ['getAll{{{ucf}}}s'] }); + }, + }); + + useEffect(() => { + if (data && data.deleted) { + addNotification('error', `${data["@id"]} has been deleted by another user`); + setIsModalVisible(false); + setRequestDelete(false); + } + }, [JSON.stringify(data)]) + + const onAccept = () => { + deleteMutation.mutate(data); + setIsModalVisible(false); + setRequestDelete(false); + } + + const onDecline = () => { + setRequestDelete(false); } return ( - {modalState.edit ? `Edit {{{ucf}}}` : 'Create a new {{{ucf}}}'} + + {isModalEdit ? `Edit {{{ucf}}}` : 'Create a new {{{ucf}}}'} ({ data && data['@id'] }) { - modalState.edit && - handleDelete()}> + isModalEdit && + setRequestDelete(true)}> Delete } - dispatch(setModalIsVisible(false))}> + setIsModalVisible(false)}> Close diff --git a/templates/react-native-v2/components/foo/Form.tsx b/templates/react-native-v2/components/foo/Form.tsx index 424b0dea..cc91008f 100644 --- a/templates/react-native-v2/components/foo/Form.tsx +++ b/templates/react-native-v2/components/foo/Form.tsx @@ -1,25 +1,37 @@ -import { useCreateMutation, useLazyGetAllQuery, useUpdateMutation } from "@/lib/api/{{{lc}}}Api"; -import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory"; -import { useAppSelector } from "@/lib/hooks"; -import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice"; import {{{ucf}}} from "@/lib/types/{{{ucf}}}"; -import { useState } from "react"; +import { useContext, useState } from "react"; import { Controller, SubmitErrorHandler, useForm } from "react-hook-form"; import { Pressable, Text, TextInput, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { useDispatch } from "react-redux"; +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { create, update } from "@/lib/api/{{{lc}}}Api"; +import { {{{ucf}}}Context } from "./Context"; export default function Form() { - const [update] = useUpdateMutation(); - const [create] = useCreateMutation(); - const [getAll] = useLazyGetAllQuery(); - const {{{lc}}}Data = useAppSelector(state => state.{{{lc}}}); - const { page, currentData, modalState } = {{{lc}}}Data; - const dispatch = useDispatch(); const [errors, setErrors] = useState([]); - const submitQuery = modalState.edit ? update : create; + const queryClient = useQueryClient(); - const initValues: {{{ucf}}} = modalState.edit ? currentData : { + const context = useContext({{{ucf}}}Context); + const { addNotification, isModalEdit, setIsModalVisible, currentData: data } = context; + + const queryFn = isModalEdit ? update : create; + + const mutation = useMutation({ + mutationFn: (data: {{{ucf}}}) => queryFn(data), + onError: (error) => { + addNotification('error', error.toString()); + }, + onSuccess: (data) => { + if (data.ok) { + addNotification('success', `The {{{lc}}} has been ${isModalEdit ? 'updated' : 'created'}`); + } else { + addNotification('error', `An error occured while ${isModalEdit ? 'updating' : 'creating'} the {{{lc}}} (${data.statusText})`); + } + queryClient.invalidateQueries({ queryKey: ['getAll'] }); + } + }); + + let initValues: {{{ucf}}} = (isModalEdit && data) ? data : { '@id': '', {{#each formFields}} {{{name}}}: {{#if (compare type "==" "number")}}0{{else}}''{{/if}}, @@ -30,30 +42,11 @@ export default function Form() { defaultValues: initValues }); - const onSubmit = (data: {{{ucf}}}) => { intParser(data); - submitQuery(data) - .unwrap() - .then(() => { - dispatch(addLog(createSuccessLog(`The {{{lc}}} has been ${modalState.edit ? 'edited' : 'created'} successfully.`))) - getAll(page) - .unwrap() - .then(fulfilled => { - reset(); - dispatch(setView(fulfilled["hydra:view"])); - dispatch(setData(fulfilled["hydra:member"])); - dispatch(setModalIsVisible(false)); - }); - }) - .catch(error => { - if (error.data) { - dispatch( - addLog(createErrorLog(`Error : ${error.data["hydra:description"]}`)) - ) - } - }); - return; + mutation.mutate(data); + setIsModalVisible(false); + reset(); }; const intParser = (data: {{{ucf}}}) => { @@ -62,15 +55,12 @@ export default function Form() { data[key] = parseInt(data[key]); } }); - return; } const onError: SubmitErrorHandler<{{{ucf}}}> = (errors, e) => { setErrors(Object.keys(errors)); - return; } - return ( @@ -102,9 +92,8 @@ export default function Form() { {{#if required}}rules={fieldRequired}{{/if}} /> {{/each}} - - {modalState.edit ? 'Edit' : 'Create'} + {isModalEdit ? 'Edit' : 'Create'} diff --git a/templates/react-native-v2/components/foo/LogsRenderer.tsx b/templates/react-native-v2/components/foo/LogsRenderer.tsx index 4419732c..41d9d69c 100644 --- a/templates/react-native-v2/components/foo/LogsRenderer.tsx +++ b/templates/react-native-v2/components/foo/LogsRenderer.tsx @@ -1,12 +1,18 @@ -import { useAppSelector } from "@/lib/hooks"; -import { cleanLogs } from "@/lib/slices/{{{lc}}}Slice"; -import { Log } from "@/lib/types/Logs"; +import { Log, LogType } from "@/lib/utils/Logs"; +import { useContext, useMemo } from "react"; import { Pressable, Text, View } from "react-native"; -import { useDispatch } from "react-redux"; +import { {{{ucf}}}Context } from "./Context"; export default function LogsRenderer() { - const logs = useAppSelector(state => state.{{{lc}}}.logs); - const { errors, successes } = logs; + const context = useContext({{{ucf}}}Context); + const { notifications, clearNotifications } = context; + + const filterLogs = (type: keyof LogType) => { + return notifications.filter(log => log.type == type); + } + + const errors = useMemo(() => filterLogs("error"), [notifications]); + const successes = useMemo(() => filterLogs("success"), [notifications]); return ( @@ -14,34 +20,28 @@ export default function LogsRenderer() { errors.length > 0 && - {errors.map((error: string, index: number) => ( - - {error} + {errors.map((error: Log, index: number) => ( + - {error.message} ))} - + clearNotifications('error')}> + X + } { successes.length > 0 && - {successes.map((success: string, index: number) => ( - - {success} + {successes.map((success: Log, index: number) => ( + - {success.message} ))} - + clearNotifications('success')}> + X + } ) -} - -const CloseButton = (props: { type: keyof Log }) => { - const dispatch = useDispatch(); - - return ( - dispatch(cleanLogs(props.type))}> - X - - ) } \ No newline at end of file diff --git a/templates/react-native-v2/lib/api/fooApi.ts b/templates/react-native-v2/lib/api/fooApi.ts index 6fceb81e..7865ca6a 100644 --- a/templates/react-native-v2/lib/api/fooApi.ts +++ b/templates/react-native-v2/lib/api/fooApi.ts @@ -1,43 +1,49 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import {{{ucf}}} from '../types/{{{ucf}}}'; import { ENTRYPOINT } from '@/config/entrypoint'; +import {{{ucf}}} from '../types/{{{ucf}}}'; + +const ENDPOINT = `{{{lc}}}s`; + +export function getAll(pageId: string) { + let page = parseInt(pageId); + + if (page < 1 || isNaN(page)) { + page = 1; + } + + return fetch(`${ENTRYPOINT}/${ENDPOINT}?page=${page}`).then(res => res.json()); +}; + +export function update(data: {{{ucf}}}): Promise { + return fetch( + `${ENTRYPOINT}${data['@id']}`, + { + method: 'PUT', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + } + ) +} -export const {{{lc}}}Api = createApi({ - reducerPath: '{{{lc}}}Api', - baseQuery: fetchBaseQuery({ baseUrl: ENTRYPOINT }), - endpoints: builder => ({ - getAll: builder.query({ - query: (page) => { - return `/{{{lc}}}s?page=${page}` - } - }), - delete: builder.mutation({ - query: (id) => { - return { - url: `${id}`, - method: 'DELETE' - } - } - }), - create: builder.mutation({ - query: ({{{lc}}}) => { - return { - url: `/{{{lc}}}s`, - method: 'POST', - body: {{{lc}}}, - } - } - }), - update: builder.mutation({ - query: ({{{lc}}}) => { - return { - url: {{{lc}}}['@id'], - method: 'PUT', - body: {{{lc}}}, - } - } - }), - }) -}); +export function create(data: {{{ucf}}}): Promise { + return fetch( + `${ENTRYPOINT}/${ENDPOINT}`, + { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + } + ) +} -export const { useLazyGetAllQuery, useDeleteMutation, useCreateMutation, useUpdateMutation } = {{{lc}}}Api; \ No newline at end of file +export function remove(data: {{{ucf}}}): Promise { + return fetch( + `${ENTRYPOINT}${data['@id']}`, + { + method: 'DELETE', + } + ) +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/factory/logFactory.ts b/templates/react-native-v2/lib/factory/logFactory.ts deleted file mode 100644 index 69af26ee..00000000 --- a/templates/react-native-v2/lib/factory/logFactory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Log, NewLog } from "../types/Logs"; - -function createLog(type: keyof Log, message: string): NewLog { - return { - type, - message - } -} - -export function createSuccessLog(message: string): NewLog { - return createLog("successes", message); -} - -export function createErrorLog(message: string): NewLog { - return createLog("errors", message); -} \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks.ts b/templates/react-native-v2/lib/hooks.ts deleted file mode 100644 index 5c8ff39c..00000000 --- a/templates/react-native-v2/lib/hooks.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useDispatch, useSelector, useStore } from 'react-redux' -import type { TypedUseSelectorHook } from 'react-redux' -import type { RootState, AppDispatch, AppStore } from './store' - -export const useAppDispatch: () => AppDispatch = useDispatch -export const useAppSelector: TypedUseSelectorHook = useSelector -export const useAppStore: () => AppStore = useStore \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks/data.ts b/templates/react-native-v2/lib/hooks/data.ts new file mode 100644 index 00000000..90c09a8d --- /dev/null +++ b/templates/react-native-v2/lib/hooks/data.ts @@ -0,0 +1,36 @@ +import { useCallback, useState } from "react"; +import ApiResource from "../types/ApiResource"; +import { HydraView } from "../types/HydraView"; + +type useDataType = { + member: T[]; + setMember: (member: T[]) => void, + processMercureData: (member: T) => void, + view: HydraView; + setView: (view: HydraView) => void; + currentData: Nullable; + setCurrentData: (data: T) => void; +} + +export const useData = (): useDataType => { + const [member, setMember] = useState([]); + const [view, setView] = useState({}); + const [currentData, setCurrentData] = useState>(undefined); + + const processMercureData = useCallback((data: T) => { + const currentMember = member.find(item => item["@id"] == data["@id"]); + + if (Object.keys(data).length == 1) { + data.deleted = true; + } + + if (currentMember) { + Object.assign(currentMember, data); + setMember([...member]); // force re-render + } else { + setMember([...member, data]); + } + }, [member]); + + return { member, setMember, processMercureData, view, setView, currentData, setCurrentData }; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks/mercure.ts b/templates/react-native-v2/lib/hooks/mercure.ts new file mode 100644 index 00000000..06cdb4b4 --- /dev/null +++ b/templates/react-native-v2/lib/hooks/mercure.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import ApiResource from "../types/ApiResource"; +import { ENTRYPOINT } from "@/config/entrypoint"; +import { extractHubURL, mercureSubscribe } from "../utils/mercure"; + +type useMercureType = { + hubURL: Nullable; + eventSource: Nullable; +} + +export const useMercure = (topics: string[], setData: (data: T) => void): useMercureType => { + const [hubURL, setHubURL] = useState>(undefined); + const [eventSource, setEventSource] = useState>(undefined); + if (topics.length < 1) return { hubURL, eventSource }; + + useEffect(() => { + fetch(`${ENTRYPOINT}${topics[0]}`) + .then(res => { + const extractedUrl = extractHubURL(res); + if (extractedUrl) { + setHubURL(extractedUrl.href); + } + }); + + if (hubURL) { + setEventSource(mercureSubscribe(new URL(hubURL), topics, setData)); + } + + return () => eventSource && eventSource.close(); + }, [hubURL, setData]); + + return { hubURL, eventSource }; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks/modal.ts b/templates/react-native-v2/lib/hooks/modal.ts new file mode 100644 index 00000000..d9ee8957 --- /dev/null +++ b/templates/react-native-v2/lib/hooks/modal.ts @@ -0,0 +1,27 @@ +import { useState } from "react"; + +type useModalType = { + isModalVisible: boolean; + isModalEdit: boolean; + toggleEditModal: () => void; + toggleCreateModal: () => void; + setIsModalEdit: (state: boolean) => void; + setIsModalVisible: (state: boolean) => void; +} + +export const useModal = (): useModalType => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [isModalEdit, setIsModalEdit] = useState(false); + + const toggleEditModal = () => { + setIsModalVisible(true); + setIsModalEdit(true); + }; + + const toggleCreateModal = () => { + setIsModalVisible(true); + setIsModalEdit(false); + } + + return { isModalEdit, isModalVisible, toggleCreateModal, toggleEditModal, setIsModalEdit, setIsModalVisible } +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/hooks/notifications.ts b/templates/react-native-v2/lib/hooks/notifications.ts new file mode 100644 index 00000000..d9505052 --- /dev/null +++ b/templates/react-native-v2/lib/hooks/notifications.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { Log, LogType, addNotificationFunction, clearNotificationsFunction } from "../utils/Logs"; + +type useNotificationsType = { + notifications: Log[]; + setNotifications: (notifications: Log[]) => void; + addNotification: addNotificationFunction; + clearNotifications: clearNotificationsFunction; +} + +export const useNotifications = (): useNotificationsType => { + const [notifications, setNotifications] = useState([]); + const TIMEOUT_MS = 5000; // 5 secondes + + useEffect(() => { + const timeoutId = setTimeout(() => setNotifications([]), TIMEOUT_MS); + + return () => clearTimeout(timeoutId); + }, [notifications]); + + + const addNotification = (type: keyof LogType, message: string) => { + setNotifications([...notifications, new Log(type, message)]); + }; + + const clearNotifications = (type: keyof LogType) => { + setNotifications([...notifications.filter(log => log.type !== type)]); + }; + + return { notifications, setNotifications, addNotification, clearNotifications }; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/slices/fooSlice.ts b/templates/react-native-v2/lib/slices/fooSlice.ts deleted file mode 100644 index 068480da..00000000 --- a/templates/react-native-v2/lib/slices/fooSlice.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' -import {{{ucf}}} from '../types/{{{ucf}}}'; -import { HydraView } from '../types/HydraView'; -import { Log, NewLog } from '../types/Logs'; - -interface {{{ucf}}}SliceState { - page: number; - data: {{{ucf}}}[]; - currentData?: {{{ucf}}}; - modalState: ModalState; - view: HydraView; - logs: Log; -} - -interface ModalState { - open: boolean; - edit: boolean; -} - -const initialState: {{{ucf}}}SliceState = { - page: 1, - data: [], - currentData: undefined, - modalState: { - open: false, - edit: false, - }, - view: {}, - logs: { - errors: [], - successes: [], - } -} - -export const {{{lc}}}Slice = createSlice({ - name: '{{{lc}}}Slice', - initialState, - reducers: { - setPage: (state, action: PayloadAction) => { - state.page = action.payload; - }, - setData: (state, action: PayloadAction<{{{ucf}}}[]>) => { - state.data = action.payload; - }, - setView: (state, action: PayloadAction) => { - state.view = action.payload; - }, - setCurrentData: (state, action: PayloadAction<{{{ucf}}}>) => { - state.currentData = action.payload; - }, - setModalIsVisible: (state, action: PayloadAction) => { - state.modalState.open = action.payload; - }, - setModalIsEdit: (state, action: PayloadAction) => { - state.modalState.edit = action.payload; - }, - addLog: (state, action: PayloadAction) => { - state.logs[action.payload.type] = [...state.logs[action.payload.type], action.payload.message]; - }, - cleanLogs: (state, action: PayloadAction) => { - state.logs[action.payload] = []; - }, - } -}) - -export const { setPage, setData, setView, setCurrentData, setModalIsEdit, setModalIsVisible, addLog, cleanLogs } = {{{lc}}}Slice.actions; - -export default {{{lc}}}Slice.reducer; \ No newline at end of file diff --git a/templates/react-native-v2/lib/store.ts b/templates/react-native-v2/lib/store.ts deleted file mode 100644 index 7d829bc0..00000000 --- a/templates/react-native-v2/lib/store.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit'; - -export const makeStore = () => { - return configureStore({ - reducer: { - }, - middleware: getDefaultMiddleware => - getDefaultMiddleware() - .concat(), - }); -}; - -export type AppStore = ReturnType; -export type RootState = ReturnType; -export type AppDispatch = AppStore['dispatch']; - diff --git a/templates/react-native-v2/lib/types/ApiResource.ts b/templates/react-native-v2/lib/types/ApiResource.ts index 217fe654..aff6788e 100644 --- a/templates/react-native-v2/lib/types/ApiResource.ts +++ b/templates/react-native-v2/lib/types/ApiResource.ts @@ -1,3 +1,4 @@ export default interface ApiResource { "@id": string; + deleted?: boolean; } \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/HydraResponse.ts b/templates/react-native-v2/lib/types/HydraResponse.ts new file mode 100644 index 00000000..dd7757dd --- /dev/null +++ b/templates/react-native-v2/lib/types/HydraResponse.ts @@ -0,0 +1,7 @@ +import ApiResource from "./ApiResource"; +import { HydraView } from "./HydraView"; + +export interface HydraResponse { + 'hydra:member'?: Array; + 'hydra:view'?: HydraView; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/Logs.ts b/templates/react-native-v2/lib/types/Logs.ts deleted file mode 100644 index 46a49352..00000000 --- a/templates/react-native-v2/lib/types/Logs.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface Log { - errors: Array; - successes: Array; -} - -export interface NewLog { - type: keyof Log; - message: string; -} \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/foo.ts b/templates/react-native-v2/lib/types/foo.ts index d5f67698..ebb9682a 100644 --- a/templates/react-native-v2/lib/types/foo.ts +++ b/templates/react-native-v2/lib/types/foo.ts @@ -1,6 +1,6 @@ import ApiResource from "./ApiResource"; -export default interface {{{ ucf }}} extends ApiResource { +export default interface {{{ucf}}} extends ApiResource { {{#each fields}} {{#if readonly}}readonly{{/if}} {{{name}}}?: {{#if (compare type "==" "Date")}}string{{else}}{{{type}}}{{/if}}; {{/each}} diff --git a/templates/react-native-v2/lib/utils/Logs.ts b/templates/react-native-v2/lib/utils/Logs.ts new file mode 100644 index 00000000..5dec4394 --- /dev/null +++ b/templates/react-native-v2/lib/utils/Logs.ts @@ -0,0 +1,17 @@ +export interface LogType { + "error": string; + "success": string; +} + +export class Log { + type: keyof LogType; + message: string; + + constructor(type: keyof LogType, message: string) { + this.type = type; + this.message = message; + } +} + +export type addNotificationFunction = (type: keyof LogType, message: string) => void; +export type clearNotificationsFunction = (type: keyof LogType) => void; \ No newline at end of file diff --git a/templates/react-native-v2/lib/utils/icons.tsx b/templates/react-native-v2/lib/utils/icons.tsx new file mode 100644 index 00000000..0ec42e62 --- /dev/null +++ b/templates/react-native-v2/lib/utils/icons.tsx @@ -0,0 +1,13 @@ +import { FontAwesome } from "@expo/vector-icons"; + +type IconProps = { + size?: number; + name: React.ComponentProps['name']; + color?: string; +} + +export function Icon({ size = 32, name, color = "black" }: IconProps) { + const iconMargin = { marginBottom: -3 } + + return ; +} \ No newline at end of file diff --git a/templates/react-native-v2/lib/utils/mercure.ts b/templates/react-native-v2/lib/utils/mercure.ts new file mode 100644 index 00000000..9c159b6d --- /dev/null +++ b/templates/react-native-v2/lib/utils/mercure.ts @@ -0,0 +1,33 @@ +import { ENTRYPOINT } from "@/config/entrypoint"; +import type ApiResource from "@/lib/types/ApiResource"; + +export const mercureSubscribe = ( + hubURL: URL, + topics: string[], + setData: (data: T) => void +): EventSource => { + const url = new URL(hubURL); + + topics.forEach((topic) => + url.searchParams.append("topic", new URL(topic, ENTRYPOINT).toString()) + ); + + const eventSource = new EventSource(url.toString()); + + eventSource.addEventListener("message", (event) => { + setData(JSON.parse(event.data)); + }); + + return eventSource; +}; + +export const extractHubURL = (response: Response): URL | undefined => { + const linkHeader = response.headers.get("Link"); + if (!linkHeader) return undefined; + + const matches = linkHeader.match( + /<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/ + ); + + return matches && matches[1] ? new URL(matches[1], ENTRYPOINT) : undefined; +}; \ No newline at end of file From 6ca7a1cab41a0af158ba6792260b83addb6dd20b Mon Sep 17 00:00:00 2001 From: mehdi Date: Thu, 28 Mar 2024 15:36:16 +0100 Subject: [PATCH 3/4] update test for native generator v2 --- src/generators/ReactNativeGeneratorV2.test.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/generators/ReactNativeGeneratorV2.test.js b/src/generators/ReactNativeGeneratorV2.test.js index 2981cf64..1c0626b9 100644 --- a/src/generators/ReactNativeGeneratorV2.test.js +++ b/src/generators/ReactNativeGeneratorV2.test.js @@ -37,20 +37,27 @@ test("Generate a React Native V2 app", () => { generator.generate(api, resource, tmpobj.name); [ - "/lib/hooks.ts", - "/lib/store.ts", "/lib/types/ApiResource.ts", "/lib/types/HydraView.ts", - "/lib/types/Logs.ts", - "/lib/factory/logFactory.ts", + "/lib/types/HydraResponse.ts", + "/lib/utils/Logs.ts", + "/lib/utils/icons.tsx", + "/lib/utils/Logs.ts", + "/lib/utils/mercure.ts", + "/lib/hooks/data.ts", + "/lib/hooks/mercure.ts", + "/lib/hooks/modal.ts", + "/lib/hooks/notifications.ts", + "/config/entrypoint.js", "/components/Main.tsx", "/components/Navigation.tsx", - "/components/StoreProvider.tsx", + "/components/ConfirmModal.tsx", "/app/_layout.tsx.dist", "/app/(tabs)/abcs.tsx", - "/lib/slices/abcSlice.ts", "/lib/api/abcApi.ts", + "/lib/types/Abc.ts", + "/components/abc/Context.ts", "/components/abc/CreateEditModal.tsx", "/components/abc/Form.tsx", "/components/abc/LogsRenderer.tsx", From 9f6dbf023244fbba07e09d0c825bde249a85991e Mon Sep 17 00:00:00 2001 From: mehdi Date: Thu, 28 Mar 2024 15:48:25 +0100 Subject: [PATCH 4/4] feat: add hydra prefix support --- src/generators/ReactNativeGeneratorV2.js | 4 ++-- templates/react-native-v2/app/(tabs)/foos.tsx | 4 ++-- templates/react-native-v2/components/Navigation.tsx | 8 ++++---- templates/react-native-v2/lib/types/HydraResponse.ts | 4 ++-- templates/react-native-v2/lib/types/HydraView.ts | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/generators/ReactNativeGeneratorV2.js b/src/generators/ReactNativeGeneratorV2.js index 06a34acd..3d934619 100644 --- a/src/generators/ReactNativeGeneratorV2.js +++ b/src/generators/ReactNativeGeneratorV2.js @@ -105,8 +105,6 @@ export default class extends BaseGenerator { // static files [ "lib/types/ApiResource.ts", - "lib/types/HydraView.ts", - "lib/types/HydraResponse.ts", "lib/hooks/data.ts", "lib/hooks/mercure.ts", "lib/hooks/modal.ts", @@ -133,6 +131,8 @@ export default class extends BaseGenerator { "components/%s/CreateEditModal.tsx", "components/%s/Form.tsx", "components/%s/LogsRenderer.tsx", + "lib/types/HydraView.ts", + "lib/types/HydraResponse.ts", ].forEach((pattern) => this.createFileFromPattern(pattern, dir, [lc], context) ); diff --git a/templates/react-native-v2/app/(tabs)/foos.tsx b/templates/react-native-v2/app/(tabs)/foos.tsx index 7d9cbcf2..9b6effe5 100644 --- a/templates/react-native-v2/app/(tabs)/foos.tsx +++ b/templates/react-native-v2/app/(tabs)/foos.tsx @@ -33,8 +33,8 @@ export default function Books() { useEffect(() => { if (isSuccess) { - setMember(data["hydra:member"]); - setView(data['hydra:view']); + setMember(data["{{{hydraPrefix}}}member"]); + setView(data['{{{hydraPrefix}}}view']); } }, [isSuccess, data]); diff --git a/templates/react-native-v2/components/Navigation.tsx b/templates/react-native-v2/components/Navigation.tsx index 0332f5cb..ff65f510 100644 --- a/templates/react-native-v2/components/Navigation.tsx +++ b/templates/react-native-v2/components/Navigation.tsx @@ -20,10 +20,10 @@ export default function Navigation(props: { view: HydraView }) { const router = useRouter(); const { - "hydra:first": first, - "hydra:previous": previous, - "hydra:next": next, - "hydra:last": last, + "{{{hydraPrefix}}}first": first, + "{{{hydraPrefix}}}previous": previous, + "{{{hydraPrefix}}}next": next, + "{{{hydraPrefix}}}last": last, } = view; return ( diff --git a/templates/react-native-v2/lib/types/HydraResponse.ts b/templates/react-native-v2/lib/types/HydraResponse.ts index dd7757dd..5a4f1119 100644 --- a/templates/react-native-v2/lib/types/HydraResponse.ts +++ b/templates/react-native-v2/lib/types/HydraResponse.ts @@ -2,6 +2,6 @@ import ApiResource from "./ApiResource"; import { HydraView } from "./HydraView"; export interface HydraResponse { - 'hydra:member'?: Array; - 'hydra:view'?: HydraView; + '{{{hydraPrefix}}}member'?: Array; + '{{{hydraPrefix}}}view'?: HydraView; } \ No newline at end of file diff --git a/templates/react-native-v2/lib/types/HydraView.ts b/templates/react-native-v2/lib/types/HydraView.ts index c4b9e82d..5c874e2c 100644 --- a/templates/react-native-v2/lib/types/HydraView.ts +++ b/templates/react-native-v2/lib/types/HydraView.ts @@ -1,6 +1,6 @@ export interface HydraView { - 'hydra:first'?: string; - 'hydra:last'?: string; - 'hydra:previous'?: string; - 'hydra:next'?: string; + '{{{hydraPrefix}}}first'?: string; + '{{{hydraPrefix}}}last'?: string; + '{{{hydraPrefix}}}previous'?: string; + '{{{hydraPrefix}}}next'?: string; } \ No newline at end of file