UNPKG

react-simple-crud-ui

Version:

Written in React with hooks, simple CRUD UI with authentication.

797 lines (761 loc) 27.8 kB
# One example worths thoudsands line of explanations. ### App.tsx ```javascript import React, { useReducer, useState } from 'react'; import Axios from 'axios'; import './App.css'; import CountryForm from './CountryForm'; import LanguageForm from './LanguageForm'; import 'react-toastify/dist/ReactToastify.css'; import { ToastContainer, toast } from 'react-toastify'; import RSCU from 'react-simple-crud-ui'; import { CrudSettingModel, AuthSettingModel } from 'react-simple-crud-ui/dist/CRUD/Model'; function App() { const AuthenticationLayer = RSCU.components.AuthenticationLayer; const CrudStateModel = RSCU.models.CrudStateModel; const CRUD = RSCU.components.CRUD; const [appState, dispatchApp] = useReducer(RSCU.reducers.appReducer, new RSCU.models.AppStateModel()); const reducer = { appState, dispatchApp } const countrySetting = { initialCrudState: new CrudStateModel(10, 'Country'), Form: CountryForm, ListGenerator: (crudState, dispatchCrud, mode) => { return ( <div> <div className="row bold border-bottom"> <div className="col-4"> Country Name </div> <div className="col-4"> Code </div> <div className="col-4"> Year </div> </div> <div className="row bold border-bottom"> <div className="col-4"> <input className="form-control" placeholder="Filter By Name" defaultValue={crudState.filter.value} onChange={(e) => dispatchCrud({ type: 'FILTER', payload: { value: e.target.value } })} /> </div> <div className="col-4"> </div> <div className="col-4"> </div> </div> { crudState.allDataPagination.map(i => { const key = new Date().getTime() + '' + i.id; return ( <div key={key} className="row data-record" title="Click to edit" onClick={() => dispatchCrud({ type: 'CHANGE_MODE', payload: mode.Edit, editId: i.id })}> <div className="col-4 grid-left ellipsis"> {i.name} </div> <div className="col-4 grid-center ellipsis"> {i.code} </div> <div className="col-4 grid-right ellipsis"> {i.year} </div> </div> ) }) } </div> ) }, filterFn: (item: any, filterObject: any) => { return item.name.toLowerCase().includes(filterObject.value); }, loadAllData: (appReducer, crudReducer, mounted: boolean) => { const { appState, dispatchApp } = appReducer; const { crudState, dispatchCrud } = crudReducer; Axios.post("https://some-graphql-api-endpoint", { query: `query { countries { id code name year flagUrl createdDate updatedDate userId } }` }, { headers: { "token": appState.account.token } }).then(axiosData => { const data = axiosData.data; if (data.data !== undefined && data.data !== null) { if (!!mounted) { dispatchCrud({ type: 'GET_ALL_SUCCESS', payload: data.data.countries }) if (crudState.filter !== '') { dispatchCrud({ type: 'FILTER', payload: crudState.filter }) } } else { console.log('list is unmounted ', mounted); } } else { if (!!mounted) { dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(data) }) } else { console.log('list is unmounted ', mounted); console.log(data); } } }).catch( err => { console.log('list is unmounted ', mounted); console.log(err); if (!!mounted) { dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(err) }); } localStorage.removeItem('account'); dispatchApp({ type: 'LOGOUT' }); } ); }, addCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation createCountry($data: CountryModel!) { createCountry(data: $data) { id code name year flagUrl createdDate updatedDate userId } } `, variables: { "data": { ...data, year: parseInt(data.year) } } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const createCountry = data.data.createCountry; // console.log(createCountry); toast.success("Country is created!"); } else { console.log(data); toast.warning("Create failed!"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Country can not be created! Something goes wrong here!"); }) }, loadOneCrud: (appReducer, crudReducer, setThisCrud) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `query country($id: ID!) { country(id: $id) { id code name year flagUrl createdDate updatedDate userId } }`, variables: { "id": crudState.editId } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== null && data.data !== undefined) { const country = data.data.country; // console.log(country); setThisCrud(country); dispatchCrud({ type: 'END' }); } else { console.log('error ', data); dispatchCrud({ type: 'END' }); } }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); }) }, editCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation updateCountry($data: CountryModel!) { updateCountry(data: $data) { id code name year flagUrl createdDate updatedDate userId } } `, variables: { "data": { ...data, year: parseInt(data.year) } } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const updateCountry = data.data.updateCountry; // console.log(updateCountry); toast.success("Country is updated!"); } else { console.log(data); toast.warning("Update failed"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Country can not be updated! Something goes wrong here!"); }) }, deleteCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation deleteCountry($data: CountryModel!) { deleteCountry(data: $data) { id code name year flagUrl } } `, variables: { "data": { "id": data.id } } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const deleteCountry = data.data.deleteCountry; // console.log(deleteCountry); toast.success("Country is deleted!"); } else { console.log(data); toast.warning("Delete failed"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Country can not be deleted! Something goes wrong here!"); }) } } as CrudSettingModel; const languageSetting: CrudSettingModel = { initialCrudState: new CrudStateModel(10, 'Language'), Form: LanguageForm, ListGenerator: (crudState, dispatchCrud, MODE) => { return ( <div> <div className="row bold border-bottom"> <div className="col-4"> Name </div> <div className="col-4"> Native Name </div> <div className="col-4"> Code </div> </div> <div className="row bold border-bottom"> <div className="col-4"> <input className="form-control" placeholder="Filter By Name" defaultValue={crudState.filter.value} onChange={(e) => dispatchCrud({ type: 'FILTER', payload: { value: e.target.value } })} /> </div> <div className="col-4"> </div> <div className="col-4"> </div> </div> { crudState.allDataPagination.map(i => { const key = new Date().getTime() + '' + i.id; return ( <div key={key} className="row data-record" title="Click to edit" onClick={() => dispatchCrud({ type: 'CHANGE_MODE', payload: MODE.Edit, editId: i.id })}> <div className="col-4 grid-left ellipsis"> {i.name} </div> <div className="col-4 grid-center ellipsis"> {i.nativeName} </div> <div className="col-4 grid-right ellipsis"> {i.code} </div> </div> ) }) } </div> ) }, filterFn: (item, filterObject) => { return item.name.toLowerCase().includes(filterObject.value); }, loadAllData: (appReducer, crudReducer, mounted) => { const { appState, dispatchApp } = appReducer; const { crudState, dispatchCrud } = crudReducer; Axios.post("https://some-graphql-api-endpoint", { query: `query { languages { id code name nativeName createdDate updatedDate userId } }` }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { if (!!mounted) { dispatchCrud({ type: 'GET_ALL_SUCCESS', payload: data.data.languages }) if (crudState.filter !== '') { dispatchCrud({ type: 'FILTER', payload: crudState.filter }) } } else { console.log('unmounted'); } } else { console.log(data); if (!!mounted) { dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(data) }) } else { console.log('unmounted'); } } } ).catch( i => { console.log(i); if (!!mounted) { dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(i) }) } else { console.log('unmounted'); } localStorage.removeItem('account'); dispatchApp({ type: 'LOGOUT' }); } ); }, addCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation createLanguage($data: LanguageModel!) { createLanguage(data: $data) { id code name nativeName createdDate updatedDate userId } }`, variables: { "data": data } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const createLanguage = data.data.createLanguage; // console.log(createLanguage); toast.success("Language is created!"); } else { console.log(data); toast.warning("Create failed!"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Language can not be created! Something goes wrong here!"); }) }, loadOneCrud: (appReducer, crudReducer, setThisCrud) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `query language($id: ID!) { language(id: $id) { id code name nativeName createdDate updatedDate userId } }`, variables: { "id": crudState.editId } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== null && data.data !== undefined) { const language = data.data.language; // console.log(language); setThisCrud(language); dispatchCrud({ type: 'END' }); } else { console.log('error ', data); dispatchCrud({ type: 'END' }); } }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); }) }, editCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation updateLanguage($data: LanguageModel!) { updateLanguage(data: $data) { id code name nativeName createdDate updatedDate userId } }`, variables: { "data": data } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const updateLanguage = data.data.updateLanguage; // console.log(updateLanguage); toast.success("Language is updated!"); } else { console.log(data); toast.warning("Update failed"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Language can not be updated! Something goes wrong here!"); }) }, deleteCrud: (appReducer, crudReducer, data) => { // eslint-disable-next-line const { appState, dispatchApp } = appReducer; // eslint-disable-next-line const { crudState, dispatchCrud } = crudReducer; dispatchCrud({ type: 'START' }); Axios.post("https://some-graphql-api-endpoint", { query: `mutation deleteLanguage($data: LanguageModel!) { deleteLanguage(data: $data) { id code name nativeName } } `, variables: { "data": { "id": data.id } } }, { headers: { "token": appState.account.token } }).then(i => { const data = i.data; // console.log(data); if (data.data !== undefined && data.data !== null) { // const deleteLanguage = data.data.deleteLanguage; // console.log(deleteLanguage); toast.success("Language is deleted!"); } else { console.log(data); toast.warning("Delete failed"); } dispatchCrud({ type: 'END' }); }).catch(i => { console.log(i); dispatchCrud({ type: 'END' }); toast.error("Language can not be deleted! Something goes wrong here!"); }) } } as CrudSettingModel; const MODE = { COUNTRY: "COUNTRY", LANGUAGE: "LANGUAGE" } const [currentMode, setCurrentMode] = useState(MODE.COUNTRY); const isCountryMode = currentMode === MODE.COUNTRY; const isLanguageMode = currentMode === MODE.LANGUAGE; const authSetting = { app: { title: 'Demo Site', loginTitle: 'Gateway' }, components: { CustomComponentGenerator: () => { if (!!appState.auth) { return ( <div className="width-100" style={{ marginTop: '20px' }}> <button type="button" className={'btn ' + (!!isCountryMode ? 'btn-success' : 'btn-dark')} onClick={() => setCurrentMode(MODE.COUNTRY)}>Country</button> <button type="button" className={'btn ' + (!!isLanguageMode ? 'btn-success' : 'btn-dark')} onClick={() => setCurrentMode(MODE.LANGUAGE)}>Language</button> <hr></hr> {!!isCountryMode ? <CRUD setting={countrySetting} reducer={reducer} /> : ''} {!!isLanguageMode ? <CRUD setting={languageSetting} reducer={reducer} /> : ''} </div> ) } return ( <div> </div> ); } }, login: { url: "https://some-authenticated-graphql-api-endpoins", data: (username: string, password: string) => { return { query: ` query login($username: String!, $password: String!) { login(username: $username, password: $password) { _id token username firstName lastName email role } } `, variables: { username: username, password: password } } }, headers: {} }, } as AuthSettingModel; return ( <div className="App container"> <AuthenticationLayer reducer={reducer} setting={authSetting} /> <ToastContainer /> </div> ) } export default App; ``` ### CountryForm.tsx ```javascript import React from 'react'; import { useForm } from 'react-hook-form'; function CountryForm(props: any) { const { id: inputId, name: inputName, code: inputCode, flagUrl: inputFlagUrl, year: inputYear, updatedDate: inputUpdatedDate } = props.form; const { onSubmit, onSubmitDeleting } = props; const { register, handleSubmit, errors } = useForm(); const currentUpdatedDate = () => { if (inputUpdatedDate === undefined) { return new Date().toDateString() + ' ' + new Date().toTimeString(); } return new Date(parseInt(inputUpdatedDate)).toDateString() + ' ' + new Date(parseInt(inputUpdatedDate)).toTimeString() } const onFormSubmit = (data: any) => { // console.log('on form submit, using handle submit from useForm, ', data); onSubmit(inputId === undefined || inputId === '' ? data : { id: inputId, ...data}); } const onFormSubmitDeleting = (e: any) => { e.preventDefault(); onSubmitDeleting({ id: inputId }); } return ( <form onSubmit={handleSubmit(onFormSubmit)}> <div className="form-row"> <div className="form-group col-md-6"> <label htmlFor="name">Name</label> <input type="text" className="form-control" id="name" name="name" defaultValue={inputName} ref={register({ required: true })} /> <p className="error">{errors.name && "Name is required"}</p> </div> <div className="form-group col-md-6"> <label htmlFor="code">Code</label> <input type="text" className="form-control" id="code" name="code" defaultValue={inputCode} ref={register({ required: true })}/> <p className="error">{errors.code && "Code is required"}</p> </div> </div> <div className="form-group"> <label htmlFor="flagUrl">Flag URL</label> <input type="text" className="form-control" id="flagUrl" name="flagUrl" placeholder="https://photo.jpg" defaultValue={inputFlagUrl} ref={register({ required: true })} /> <p className="error">{errors.flagUrl && "Flag URL is required"}</p> </div> <div className="form-row"> <div className="form-group col-md-6"> <label htmlFor="year">Year</label> <input type="number" className="form-control" id="year" name="year" defaultValue={inputYear} ref={register({ required: true, min: 1200, max: 2222 })} /> <p className="error">{errors.year?.type === "required" && "Year is required"}</p> <p className="error">{errors.year?.type === "min" && "Year must be greater than 1200"}</p> <p className="error">{errors.year?.type === "max" && "Year must be less than 2222"}</p> </div> <div className="form-group col-md-6"> <label htmlFor="updatedDate">Latest Update Date</label> <input type="text" disabled className="form-control" id="updatedDate" readOnly defaultValue={currentUpdatedDate()} /> </div> </div> <button type="submit" className="btn btn-primary">Submit</button> <hr></hr> { inputId !== undefined ? <button className="btn btn-danger" onClick={onFormSubmitDeleting}>Delete</button> : '' } </form> ) } export default CountryForm; ``` ### LanguageForm.tsx ```typescript import React from 'react'; import { useForm } from 'react-hook-form'; function LanguageForm(props: any) { // console.log(props); const { id: inputId, name: inputName, code: inputCode, nativeName: inputNativeName, updatedDate: inputUpdatedDate } = props.form; const { onSubmit, onSubmitDeleting } = props; const { register, handleSubmit, errors } = useForm(); const currentUpdatedDate = () => { if (inputUpdatedDate === undefined) { return new Date().toDateString() + ' ' + new Date().toTimeString(); } return new Date(parseInt(inputUpdatedDate)).toDateString() + ' ' + new Date(parseInt(inputUpdatedDate)).toTimeString() } const onFormSubmit = (data: any) => { // console.log('on form submit, using handle submit from useForm, ', data); onSubmit(inputId === undefined || inputId === '' ? data : { id: inputId, ...data}); } const onFormSubmitDeleting = (e: any) => { e.preventDefault(); onSubmitDeleting({ id: inputId }); } return ( <form onSubmit={handleSubmit(onFormSubmit)}> <div className="form-row"> <div className="form-group col-md-6"> <label htmlFor="name">Name</label> <input type="text" className="form-control" id="name" name="name" defaultValue={inputName} ref={register({ required: true })} /> <p className="error">{errors.name && "Name is required"}</p> </div> <div className="form-group col-md-6"> <label htmlFor="code">Code</label> <input type="text" className="form-control" id="code" name="code" defaultValue={inputCode} ref={register({ required: true })}/> <p className="error">{errors.code && "Code is required"}</p> </div> </div> <div className="form-group"> <label htmlFor="nativeName">Native Name</label> <input type="text" className="form-control" id="nativeName" name="nativeName" placeholder="日本語" defaultValue={inputNativeName} ref={register({ required: true })} /> <p className="error">{errors.nativeName && "Flag URL is required"}</p> </div> <div className="form-row"> <div className="form-group col-md-12"> <label htmlFor="updatedDate">Latest Update Date</label> <input type="text" disabled className="form-control" id="updatedDate" readOnly defaultValue={currentUpdatedDate()} /> </div> </div> <button type="submit" className="btn btn-primary">Submit</button> <hr></hr> { inputId !== undefined ? <button className="btn btn-danger" onClick={onFormSubmitDeleting}>Delete</button> : '' } </form> ) } export default LanguageForm; ```