From 4cdc6a0a53ee146c0c87e1ae5c4c6a4cf7be86f2 Mon Sep 17 00:00:00 2001 From: David Liu Date: Fri, 19 Jul 2024 08:41:53 -0400 Subject: [PATCH 01/25] drf-spectacular cleanup (#323) --- .../client/templates/drf-yasg/swagger-ui.html | 18 ------------------ .../{{cookiecutter.project_slug}}/settings.py | 4 ++++ 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/client/templates/drf-yasg/swagger-ui.html diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/client/templates/drf-yasg/swagger-ui.html b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/client/templates/drf-yasg/swagger-ui.html deleted file mode 100644 index 03bae094a..000000000 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/client/templates/drf-yasg/swagger-ui.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "drf-yasg/swagger-ui.html" %} - -{% block extra_scripts %} - -{% endblock %} diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py index 9ede70a09..be4c00973 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py @@ -406,3 +406,7 @@ def filter(self, record): "SHOW_REQUEST_HEADERS": True, "OPERATIONS_SORTER": "alpha", } + +SPECTACULAR_SETTINGS = { + "COMPONENT_SPLIT_REQUEST": True, # Needed for file upload to work +} From 62cd289d08ef29b31bd8c80700dd082611778f1c Mon Sep 17 00:00:00 2001 From: David Liu Date: Fri, 19 Jul 2024 08:50:19 -0400 Subject: [PATCH 02/25] update to vue 3.4. removed deprecated libraries and migrate (#316) Co-authored-by: David Liu Co-authored-by: paribaker <58012003+paribaker@users.noreply.github.com> --- .../clients/web/vue3/package.json | 10 +- .../web/vue3/src/components/NavBar.vue | 10 +- .../clients/web/vue3/src/composables/Users.ts | 233 +++++++++--------- .../clients/web/vue3/src/main.ts | 19 +- .../web/vue3/src/services/AxiosClient.js | 7 +- .../clients/web/vue3/src/services/auth.js | 8 +- .../clients/web/vue3/src/store/index.js | 49 ---- .../web/vue3/src/store/mutation-types.js | 1 - .../clients/web/vue3/src/stores/user.ts | 35 +++ 9 files changed, 180 insertions(+), 192 deletions(-) delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/src/store/index.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/src/store/mutation-types.js create mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json index 1d298483e..5ce3113dc 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json @@ -28,17 +28,17 @@ "@thinknimble/vue3-alert-alert": "^0.0.8", "axios": "1.5.0", "js-cookie": "3.0.5", - "vue": "^3.3.4", + "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", + "vue": "^3.4.30", "vue-router": "4.2.4", - "vuex": "4.1.0", - "vuex-persistedstate": "4.1.0", "zod": "3.21.4" }, "devDependencies": { "@testing-library/vue": "^8.0.0", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", - "@vitejs/plugin-vue": "^4.2.3", + "@vitejs/plugin-vue": "^5.0.0", "autoprefixer": "^10.4.15", "cypress": "^13.5.1", "eslint": "^8.49.0", @@ -50,7 +50,7 @@ "prettier": "3.0.3", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", - "vite": "^4.4.5", + "vite": "^5.0.0", "vitest": "^0.34.6", "vue-tsc": "^1.8.27" } diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue index 5e4324f27..ce9ce28ba 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue @@ -135,11 +135,11 @@ import { userApi } from '@/services/users' import { computed, ref } from 'vue' import { useRouter } from 'vue-router' -import { useStore } from 'vuex' +import { useUserStore } from '@/stores/user' export default { setup() { - const store = useStore() + const userStore = useUserStore() const router = useRouter() let mobileMenuOpen = ref(false) let profileMenuOpen = ref(false) @@ -155,15 +155,15 @@ export default { } finally { profileMenuOpen.value = false mobileMenuOpen.value = false - store.dispatch('setUser', null) + userStore.clearUser() router.push({ name: 'Home' }) } } return { logout, - isLoggedIn: computed(() => store.getters.isLoggedIn), - user: computed(() => store.getters.user), + isLoggedIn: computed(() => userStore.isLoggedIn), + user: computed(() => userStore.user), mobileMenuOpen, profileMenuOpen, } diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts index 5ab079ee5..c2d1310a3 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts @@ -1,121 +1,120 @@ import { - AccountForm, - EmailForgotPasswordForm, - LoginForm, - LoginShape, - ResetPasswordForm, - ResetPasswordShape, - UserCreateShape, - UserShape, - userApi, - } from '@/services/users' - import { useMutation, useQueryClient } from '@tanstack/vue-query' - import { reactive, ref } from 'vue' - import { useRouter } from 'vue-router' - import { useStore } from 'vuex' - import { useAlert } from '@/composables/CommonAlerts' - - export function useUsers() { - const store = useStore() - const router = useRouter() - const qc = useQueryClient() - const loginForm = reactive(new LoginForm({})) - const forgotPasswordForm = reactive(new EmailForgotPasswordForm({})) - const resetPasswordForm = reactive(new ResetPasswordForm({})) - const registerForm = reactive(new AccountForm({})) - const loading = ref(false) - const { errorAlert, successAlert } = useAlert() - - const getCodeUidFromRoute = () => { - const { uid, token } = router.currentRoute.value.params - return { uid, token } - } + AccountForm, + EmailForgotPasswordForm, + LoginForm, + LoginShape, + ResetPasswordForm, + ResetPasswordShape, + UserCreateShape, + UserShape, + userApi, +} from '@/services/users' +import { useMutation, useQueryClient } from '@tanstack/vue-query' +import { reactive, ref } from 'vue' +import { useRouter } from 'vue-router' +import { useUserStore } from '@/stores/user' +import { useAlert } from '@/composables/CommonAlerts' - const { data: user, mutate: login } = useMutation({ - mutationFn: async (user: LoginShape) => { - return await userApi.csc.login(user) - }, - onMutate: async () => { - loading.value = true - }, - onError: (error: Error) => { - loading.value = false - console.log(error) - errorAlert('Invalid email or password') - }, - onSuccess: (data: UserShape) => { - loading.value = false - store.dispatch('setUser', data) - const redirectPath = router.currentRoute.value.query.redirect - if (redirectPath) { - router.push({ path: redirectPath as string }) - } else { - router.push({ name: 'Dashboard' }) - } - qc.invalidateQueries({ queryKey: ['user'] }) - }, - }) - const { mutate: requestPasswordReset } = useMutation({ - mutationFn: async (email: string) => { - await userApi.csc.requestPasswordReset({ email }) - }, - onError: (error: Error) => { - loading.value = false - console.log(error) - }, - onSuccess: () => { - loading.value = false - successAlert('Password reset link sent to your email') - qc.invalidateQueries({ queryKey: ['user'] }) - }, - }) - - const { mutate: resetPassword } = useMutation({ - mutationFn: async (data: ResetPasswordShape) => { - return await userApi.csc.resetPassword(data) - }, - onError: (error: Error) => { - loading.value = false - console.log(error) - errorAlert('There was an error attempting to reset password') - }, - onSuccess: (data: UserShape) => { - loading.value = false - store.dispatch('setUser', data) - router.push({ name: 'Dashboard' }) - qc.invalidateQueries({ queryKey: ['user'] }) - }, - }) - - const { mutate: register } = useMutation({ - mutationFn: async (data: UserCreateShape) => { - return await userApi.create(data) - }, - onError: (error: Error) => { - loading.value = false - console.log(error) - errorAlert('There was an error attempting to register') - }, - onSuccess: (data: UserShape ) => { - store.dispatch('setUser', data) +export function useUsers() { + const userStore = useUserStore() + const router = useRouter() + const qc = useQueryClient() + const loginForm = reactive(new LoginForm({})) + const forgotPasswordForm = reactive(new EmailForgotPasswordForm({})) + const resetPasswordForm = reactive(new ResetPasswordForm({})) + const registerForm = reactive(new AccountForm({})) + const loading = ref(false) + const { errorAlert, successAlert } = useAlert() + + const getCodeUidFromRoute = () => { + const { uid, token } = router.currentRoute.value.params + return { uid, token } + } + + const { data: user, mutate: login } = useMutation({ + mutationFn: async (user: LoginShape) => { + return await userApi.csc.login(user) + }, + onMutate: async () => { + loading.value = true + }, + onError: (error: Error) => { + loading.value = false + console.log(error) + errorAlert('Invalid email or password') + }, + onSuccess: (data: UserShape) => { + loading.value = false + userStore.updateUser(data) + const redirectPath = router.currentRoute.value.query.redirect + if (redirectPath) { + router.push({ path: redirectPath as string }) + } else { router.push({ name: 'Dashboard' }) - qc.invalidateQueries({ queryKey: ['user'] }) - loading.value = false - }, - }) - - return { - loginForm, - forgotPasswordForm, - resetPasswordForm, - loading, - login, - requestPasswordReset, - resetPassword, - user, - register, - registerForm, - getCodeUidFromRoute, - } + } + qc.invalidateQueries({ queryKey: ['user'] }) + }, + }) + const { mutate: requestPasswordReset } = useMutation({ + mutationFn: async (email: string) => { + await userApi.csc.requestPasswordReset({ email }) + }, + onError: (error: Error) => { + loading.value = false + console.log(error) + }, + onSuccess: () => { + loading.value = false + successAlert('Password reset link sent to your email') + qc.invalidateQueries({ queryKey: ['user'] }) + }, + }) + + const { mutate: resetPassword } = useMutation({ + mutationFn: async (data: ResetPasswordShape) => { + return await userApi.csc.resetPassword(data) + }, + onError: (error: Error) => { + loading.value = false + console.log(error) + errorAlert('There was an error attempting to reset password') + }, + onSuccess: (data: UserShape) => { + loading.value = false + userStore.updateUser(data) + router.push({ name: 'Dashboard' }) + qc.invalidateQueries({ queryKey: ['user'] }) + }, + }) + + const { mutate: register } = useMutation({ + mutationFn: async (data: UserCreateShape) => { + return await userApi.create(data) + }, + onError: (error: Error) => { + loading.value = false + console.log(error) + errorAlert('There was an error attempting to register') + }, + onSuccess: (data: UserShape) => { + userStore.updateUser(data) + router.push({ name: 'Dashboard' }) + qc.invalidateQueries({ queryKey: ['user'] }) + loading.value = false + }, + }) + + return { + loginForm, + forgotPasswordForm, + resetPasswordForm, + loading, + login, + requestPasswordReset, + resetPassword, + user, + register, + registerForm, + getCodeUidFromRoute, } - \ No newline at end of file +} diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/main.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/main.ts index 64f5bfbaa..d53d22999 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/main.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/main.ts @@ -1,15 +1,16 @@ import { createApp } from 'vue' +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' +import AlertPlugin from '@thinknimble/vue3-alert-alert' +import { VueQueryPlugin } from '@tanstack/vue-query' + import './main.css' import App from './App.vue' -import store from './store' import router from './router' + import '@thinknimble/vue3-alert-alert/dist/vue3-alert-alert.css' -import AlertPlugin from '@thinknimble/vue3-alert-alert' -import { VueQueryPlugin } from '@tanstack/vue-query' -createApp(App) - .use(AlertPlugin, {}) - .use(VueQueryPlugin) - .use(store) - .use(router) - .mount('#app') +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) + +createApp(App).use(pinia).use(AlertPlugin, {}).use(VueQueryPlugin).use(router).mount('#app') diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js index 1075e521c..1fe4b3369 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js @@ -1,5 +1,5 @@ import axios from 'axios' -import store from '@/store' +import { useUserStore } from '@/stores/user' import CSRF from '@/services/csrf' /** @@ -30,8 +30,9 @@ class ApiService { }) ApiService.session.interceptors.request.use( async (config) => { - if (store.getters.isLoggedIn) { - config.headers['Authorization'] = `Token ${store.getters.token}` + const userStore = useUserStore() + if (userStore.isLoggedIn) { + config.headers['Authorization'] = `Token ${userStore.token}` } return config }, diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js index 7f01db89f..79978a00e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js @@ -1,4 +1,4 @@ -import store from '@/store' +import { useUserStore } from '@/stores/user' /** * Route Guard. @@ -6,7 +6,8 @@ import store from '@/store' * If not logged in, a user will be redirected to the login page. */ export function requireAuth(to, from, next) { - if (!store.getters.isLoggedIn) { + const userStore = useUserStore() + if (!userStore.isLoggedIn) { next({ name: 'Login', query: { redirect: to.fullPath }, @@ -22,7 +23,8 @@ export function requireAuth(to, from, next) { * If logged in, a user will be redirected to the dashboard page. */ export function requireNoAuth(to, from, next) { - if (store.getters.isLoggedIn) { + const userStore = useUserStore() + if (userStore.isLoggedIn) { next({ name: 'Dashboard', }) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/index.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/index.js deleted file mode 100644 index 7e05bac59..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import { createStore } from 'vuex' -import createPersistedState from 'vuex-persistedstate' -import { SET_USER } from './mutation-types' - -const STORAGE_HASH = '{{ random_ascii_string(10) }}' -export const STORAGE_KEY = `{{ cookiecutter.project_slug }}-${STORAGE_HASH}` - -const state = { - user: null, -} - -const mutations = { - [SET_USER]: (state, payload) => { - state.user = payload - }, -} - -const actions = { - setUser({ commit }, user) { - commit(SET_USER, user) - }, -} - -const getters = { - isLoggedIn: (state) => { - return !!state.user - }, - user: (state) => { - return state.user - }, - token: (state) => { - return state.user ? state.user.token : null - }, -} - -const store = createStore({ - state, - mutations, - actions, - getters, - modules: {}, - plugins: [ - createPersistedState({ - key: STORAGE_KEY, - }), - ], -}) - -export default store diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/mutation-types.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/mutation-types.js deleted file mode 100644 index 0c16d58dc..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/store/mutation-types.js +++ /dev/null @@ -1 +0,0 @@ -export const SET_USER = 'SET_USER' diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts new file mode 100644 index 000000000..3ac0bb819 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts @@ -0,0 +1,35 @@ +// Pinia Store +import { defineStore } from 'pinia' +import { UserShape } from '@/services/users' + +const STORAGE_HASH = '{{ random_ascii_string(10) }}' +export const STORAGE_KEY = `{{ cookiecutter.project_slug }}-${STORAGE_HASH}` + +interface State { + user: UserShape | null +} + +export const useUserStore = defineStore('user', { + state: (): State => ({ + user: null, + }), + persist: { + key: STORAGE_KEY, + }, + getters: { + isLoggedIn: (state) => { + return !!state.user + }, + token: (state) => { + return state.user ? state.user.token : null + }, + }, + actions: { + updateUser(payload: UserShape) { + this.user = payload + }, + clearUser() { + this.$reset() + }, + }, +}) From 4861c9ce2b5d98b4b408f2ddc0fdfc02978baab0 Mon Sep 17 00:00:00 2001 From: jdephil <63667345+jdephil@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:36:49 -0400 Subject: [PATCH 03/25] Vite/Vue Docker Fix (#322) ## What this does Fix to get localhost working while using vite/vue/docker combo. Co-authored-by: paribaker <58012003+paribaker@users.noreply.github.com> --- {{cookiecutter.project_slug}}/clients/web/vue3/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/vite.config.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/vite.config.ts index ccb3b6040..25643e2b1 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/vite.config.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/vite.config.ts @@ -22,6 +22,7 @@ export default defineConfig(({ mode }) => { }, }, port: 8080, + host: true, }, test: { // enable jest-like global test APIs From 79d304eb8be7d15df8ca5acbfb2dbe748b9e4ae6 Mon Sep 17 00:00:00 2001 From: paribaker <58012003+paribaker@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:36:14 +0000 Subject: [PATCH 04/25] updated client side forgot password flow --- .../react-native/src/screens/auth/index.ts | 2 +- .../web/react/src/pages/reset-password.tsx | 27 ++++++++++++------- .../web/react/src/services/user/forms.ts | 22 +++++++-------- .../core/views.py | 1 + 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts index 11d61d3bf..505af62c4 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts @@ -1,2 +1,2 @@ export { Login } from './login' -export { SignUp } from './sign-up' +export { SignUp } from './sign-up' \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx index 0d28d45b5..637a25a88 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx @@ -2,7 +2,7 @@ import { useMutation } from '@tanstack/react-query' import { MustMatchValidator } from '@thinknimble/tn-forms' import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' import { isAxiosError } from 'axios' -import { FormEvent, useState } from 'react' +import { FormEvent, useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { AuthLayout } from 'src/components/auth-layout' import { Button } from 'src/components/button' @@ -11,8 +11,9 @@ import { PasswordInput } from 'src/components/password-input' import { ResetPasswordForm, TResetPasswordForm, userApi } from 'src/services/user' export const ResetPasswordInner = () => { - const { form, createFormFieldChangeHandler } = useTnForm() + const { form, createFormFieldChangeHandler, overrideForm } = useTnForm() const { userId, token } = useParams() + console.log(userId, token) const [error, setError] = useState('') const [success, setSuccess] = useState(false) @@ -30,13 +31,21 @@ export const ResetPasswordInner = () => { }, }) + useEffect(() => { + if (token && userId) { + overrideForm(ResetPasswordForm.create({ token: token, uid: userId }) as TResetPasswordForm) + } + }, [overrideForm, token, userId]) + const onSubmit = (e: FormEvent) => { e.preventDefault() - if (form.isValid && userId && token && form.password.value) { + console.log(form.value) + + if (form.isValid) { confirmResetPassword({ - userId, - token, - password: form.password.value, + userId: form.value.uid!, + token: form.value.token!, + password: form.value.password!, }) } } @@ -47,9 +56,7 @@ export const ResetPasswordInner = () => { title="Successfully reset password" description="You can now log in with your new password" > - - Go to login - +
) } @@ -102,4 +109,4 @@ export const ResetPassword = () => { ) -} +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts index 56585d5ca..eeddae331 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts @@ -89,22 +89,22 @@ export class EmailForgotPasswordForm extends Form { export type TEmailForgotPasswordForm = EmailForgotPasswordForm & EmailForgotPasswordInput export type ResetPasswordInput = { - email: IFormField - code: IFormField + uid: IFormField + token: IFormField password: IFormField confirmPassword: IFormField } export class ResetPasswordForm extends Form { - static email = new FormField({ - label: 'Email', - placeholder: 'Email', - type: 'emailAddress', - validators: [new EmailValidator({ message: 'Please enter a valid email' })], + static uid = new FormField({ + label: 'UID', + placeholder: 'uid', + type: 'text', + validators: [new RequiredValidator({ message: 'Please enter a valid uid' })], }) - static code = new FormField({ - placeholder: 'Verification Code', - type: 'number', + static token = new FormField({ + placeholder: 'Verification Token', + type: 'text', validators: [ new MinLengthValidator({ message: 'Please enter a valid 5 digit code', minLength: 5 }), ], @@ -167,4 +167,4 @@ export class ForgotPasswordForm extends Form { }) } -export type TForgotPasswordForm = ForgotPasswordForm & ForgotPasswordInput +export type TForgotPasswordForm = ForgotPasswordForm & ForgotPasswordInput \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index e0a214f0f..0a702dab9 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -125,6 +125,7 @@ def reset_password(request, *args, **kwargs): logger.info(f"Resetting password for user {user_id}") user.set_password(request.data.get("password")) user.save() + # COMMENT THIS WHEN USING THE PASSWORD RESET FLOW ON WEB ONLY FOR MOBILE - PARI BAKER response_data = UserLoginSerializer.login(user, request) return Response(response_data, status=status.HTTP_200_OK) From 3963604bdbffa2bc40b6135d7be43dcf3d299bae Mon Sep 17 00:00:00 2001 From: William Huster Date: Tue, 23 Jul 2024 11:39:31 -0400 Subject: [PATCH 05/25] Upgrade rollbar to version 1.0.0 (#326) --- {{cookiecutter.project_slug}}/Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/Pipfile b/{{cookiecutter.project_slug}}/Pipfile index 18c5ee73a..70d603f20 100644 --- a/{{cookiecutter.project_slug}}/Pipfile +++ b/{{cookiecutter.project_slug}}/Pipfile @@ -28,7 +28,7 @@ Pillow = "==9.0" django-currentuser = "==0.7.0" drf-nested-routers = "==0.93.3" django-anymail = "==8.4" # https://github.com/anymail/django-anymail -rollbar = "==0.16.2" +rollbar = "==1.0.0" premailer = "*" drf-spectacular = "*" From 47f05637fa6e2deebebf3775079cb350367acc9b Mon Sep 17 00:00:00 2001 From: Damian <52294448+lakardion@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:38:12 +0200 Subject: [PATCH 06/25] Clients - Mobile - Add better rules for RN eslint to prevent raw text in code (#328) --- .../clients/mobile/react-native/.eslintrc.js | 2 ++ .../clients/mobile/react-native/src/screens/dashboard.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/.eslintrc.js b/{{cookiecutter.project_slug}}/clients/mobile/react-native/.eslintrc.js index cf30932f9..3f88c3f61 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/.eslintrc.js +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/.eslintrc.js @@ -11,6 +11,8 @@ module.exports = { 'prettier/prettier': 'off', 'react/react-in-jsx-scope': 'off', '@typescript-eslint/no-unused-vars': 'warn', + 'react-native/no-raw-text': 2, + 'react-native/no-unused-styles': 2, }, ignorePatterns: ['tailwind.config.js', 'metro.config.js'], } diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx index 1d18f5aa0..562b4b129 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx @@ -4,7 +4,7 @@ import React from 'react' export const DashboardScreen = () => { return ( - Welcom to the Dash + Welcome to the Dashboard ) } From aee1ffc0dbcafc5fe1e4bcd3b7b66875926b01b2 Mon Sep 17 00:00:00 2001 From: Edward Romano Date: Thu, 1 Aug 2024 18:15:19 -0400 Subject: [PATCH 07/25] Improve several small quality of life items for devs (#331) --- hooks/post_gen_project.py | 5 +--- .../.github/workflows/expo-main.yml | 3 ++- {{cookiecutter.project_slug}}/.gitignore | 2 +- {{cookiecutter.project_slug}}/Procfile | 18 ++----------- .../clients/mobile/react-native/README.md | 20 +++++++------- .../clients/web/react/.env.local.example | 2 +- .../clients/web/vue3/.env.local.example | 2 +- .../clients/web/vue3/package.json | 4 +-- .../core/admin.py | 11 +++++--- .../core/forms.py | 26 +++++++++++++++++++ 10 files changed, 55 insertions(+), 38 deletions(-) diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 5e5d72a3f..ab6d66362 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,5 +1,5 @@ import secrets -from os import remove, rename +from os import remove from os.path import exists, join from shutil import copy2, move, rmtree @@ -76,9 +76,6 @@ def move_web_client_to_root(client): rmtree("client") move(join(web_clients_path, client), join("client")) rmtree(join(web_clients_path)) - env_path = join("client", ".env.local.example") - if exists(env_path): - rename(env_path, join("client", ".env.local")) def remove_mobile_client_files(client): diff --git a/{{cookiecutter.project_slug}}/.github/workflows/expo-main.yml b/{{cookiecutter.project_slug}}/.github/workflows/expo-main.yml index c88a064cb..2f9c98369 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/expo-main.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/expo-main.yml @@ -21,7 +21,8 @@ jobs: echo "This workflow was triggered by a workflow_dispatch event." echo "BUILD_MOBILE_APP=1" >> $GITHUB_OUTPUT else - echo "BUILD_MOBILE_APP=$(git diff-tree --no-commit-id --name-only -r {{ "${{ github.sha }}" }} | xargs | grep mobile/ | wc -l)" >> $GITHUB_OUTPUT + echo "BUILD_MOBILE_APP=$(git diff-tree --no-commit-id --name-only -r {{ "${{ github.sha }}" }} | xargs | grep mobile/ | wc -l)" >> $GITHUB_OUTPUT + fi printouts: runs-on: ubuntu-latest diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index 613140fae..614bd54fc 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -76,7 +76,7 @@ celerybeat-schedule # Environments .env* -!.env.example +!.env.local.example .venv env/ venv/ diff --git a/{{cookiecutter.project_slug}}/Procfile b/{{cookiecutter.project_slug}}/Procfile index 5a090a7c8..128c14f4d 100644 --- a/{{cookiecutter.project_slug}}/Procfile +++ b/{{cookiecutter.project_slug}}/Procfile @@ -1,19 +1,5 @@ -# -# Define the 'web' process to be run on Heroku -# +# Main webapp process web: gunicorn {{ cookiecutter.project_slug }}.wsgi --chdir=server --log-file - -# -# This is not mandatory for all projects, as our process currently utilizes long-term staging -# servers, and automatically running migrations on this staging server could cause issues when -# different feature branches have different migrations. The aim is to switch to a model where our -# staging servers have a smaller lifetime, and we would not run into the above issue. -# To make this switch the following problems need to be solved. -# 1. Setting up human-readable URLS for the short-lived staging servers. -# 2. Fixtures for common test data (ex: test users, common app-specific entities) -# 3. Up-to-date build numbers shown in app (ideally auto-generated at build-and-deploy time) -# -# Comment the line below to disable migrations automatically after a Heroku push. +# Update DB schema for any changes release: python server/manage.py migrate --noinput - - diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md b/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md index 13c9efe69..c9cfd4eef 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md @@ -29,13 +29,6 @@ alternatively if you want to use expo run command ### Set up external dependencies -#### Expo - -Set up an expo organization -Generate an expo robot and api key -Set the env secret `EXPO_TOKEN` in GH secrets for the pipeline -Set the `SENTRY_AUTH_TOKEN` in the Expo secrets see (Error Logging & Crash Analytics) - #### Error Logging and Crash Analytics Set up a rollbar instance (if using heroku 1 app in the production environment is recommended) @@ -46,12 +39,21 @@ Set up sentry for crash analytics and additional error logs (We use sentry becau Create a sentry account and set up the projects for the various environments (this can also be added to the prod instance on heroku) +Then go to `Settings` > `Developer Settings` > `Auth Tokens` and create a new token that you'll use in `Expo` + Retrieve: 1. API Key 2. Sentry DSN for each project 3. project-name +#### Expo + +Set up an expo organization +Generate an expo robot (name ex: `CI/CD`) and api key (name ex: `GH_ACTIONS`) +Set the env secret `EXPO_TOKEN` in GH secrets for the pipeline +Set the `SENTRY_AUTH_TOKEN` in Expo under `Secrets` (see `Error Logging & Crash Analytics` above) + ### Environment Variables For local run set environment variables in .env file (from [.env.example](./.env.example)) @@ -143,7 +145,7 @@ the variables for this environment are set up in the eas.json under the developm To set env variables you should use the [eas.json](./eas.json) and [app.config.js](./app.config.js) although it is possible to define variables in a .env (eg in the CI yaml files) due to inconsistencies while testing I consider this to be the best approach. Staging builds are created automatically when merging into the main branch you can also build manually -`eas build --platform all --profile stagign --non-interactive` +`eas build --platform all --profile staging --non-interactive` **Prod** @@ -172,7 +174,7 @@ We currently use `expo update` when building our staging app to get a quick and There are certain situations when this may not be possible for example we are installing a package that does not currently have an expo extension (revenue cat for in-app purchases) or we are using a native package that expo does not have access to (face id) -When mergning into main we deploy a new staging version that can be run in expo we also build a staging version of the app as a stand-alone native build that can be ran on a device. Staging versions will point to he staging backend defined in the [eas.json](./eas.json) +When merging into main we deploy a new staging version that can be run in expo we also build a staging version of the app as a stand-alone native build that can be ran on a device. Staging versions will point to he staging backend defined in the [eas.json](./eas.json) Most internal testing should be sufficient on the expo staging build however you can also provide the link for testing with the native build. When installed this build will replace the version on your device. diff --git a/{{cookiecutter.project_slug}}/clients/web/react/.env.local.example b/{{cookiecutter.project_slug}}/clients/web/react/.env.local.example index ae89d24b5..7b4e6a951 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/.env.local.example +++ b/{{cookiecutter.project_slug}}/clients/web/react/.env.local.example @@ -1,5 +1,5 @@ # When running the backend manually locally with `python manage.py runserver` -# VITE_DEV_BACKEND_URL="http://localhost:8080" +# VITE_DEV_BACKEND_URL="http://localhost:8000" # When running locally with Docker # Disable VITE_DEV_BACKEND_URL here. Docker will find the backend on it's own diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/.env.local.example b/{{cookiecutter.project_slug}}/clients/web/vue3/.env.local.example index 7b4e6a951..46c1b5e8e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/.env.local.example +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/.env.local.example @@ -1,5 +1,5 @@ # When running the backend manually locally with `python manage.py runserver` -# VITE_DEV_BACKEND_URL="http://localhost:8000" +# VITE_DEV_BACKEND_URL="http://127.0.0.1:8000" # When running locally with Docker # Disable VITE_DEV_BACKEND_URL here. Docker will find the backend on it's own diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json index 5ce3113dc..a4596fe90 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "serve": "vite", - "build": "vue-tsc && vite build", + "build": "vite build", "preview": "vite preview", "test": "vitest run", "test:dev": "vitest", @@ -54,4 +54,4 @@ "vitest": "^0.34.6", "vue-tsc": "^1.8.27" } -} \ No newline at end of file +} diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/admin.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/admin.py index 509f67629..71a55e02d 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/admin.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/admin.py @@ -5,6 +5,7 @@ from {{ cookiecutter.project_slug }}.common.admin.filters import AutocompleteAdminMedia, AutocompleteFilter +from .forms import GroupAdminForm from .models import User @@ -23,7 +24,7 @@ class CustomUserAdmin(UserAdmin): ) }, ), - ("Admin Options", {"classes": ("collapse",), "fields": ("is_staff",)}), + ("Admin Options", {"classes": ("collapse",), "fields": ("is_staff", "groups")}), ) add_fieldsets = ( ( @@ -34,7 +35,7 @@ class CustomUserAdmin(UserAdmin): }, ), ) - list_display = ("is_active", "email", "first_name", "last_name") + list_display = ("email", "first_name", "last_name", "is_active", "is_staff", "is_superuser", "permissions") list_display_links = ( "is_active", "email", @@ -52,14 +53,18 @@ class CustomUserAdmin(UserAdmin): "is_staff", "is_superuser", ) - + filter_horizontal = ("groups",) ordering = [] + def permissions(self, obj): + return ", ".join([g.name for g in obj.groups.all()]) + class Media(AutocompleteAdminMedia): pass class CustomGroupAdmin(GroupAdmin): + form = GroupAdminForm list_filter = (("permissions", AutocompleteFilter),) class Media(AutocompleteAdminMedia): diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/forms.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/forms.py index 566e54ce2..2cbffa367 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/forms.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/forms.py @@ -1,5 +1,31 @@ from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group + +User = get_user_model() class PreviewTemplateForm(forms.Form): _send_to = forms.EmailField(label="Send by email to", widget=forms.EmailInput(attrs={"size": 60})) + + +class GroupAdminForm(forms.ModelForm): + users = forms.ModelMultipleChoiceField(queryset=User.objects.all(), required=False, widget=FilteredSelectMultiple("users", False)) + + def __init__(self, *args, **kwargs): + super(GroupAdminForm, self).__init__(*args, **kwargs) + if self.instance.pk: + self.fields["users"].initial = self.instance.user_set.all() + + def save_m2m(self): + self.instance.user_set.set(self.cleaned_data["users"]) + + def save(self, *args, **kwargs): + instance = super(GroupAdminForm, self).save() + self.save_m2m() + return instance + + class Meta: + model = Group + exclude = [] From c9865fc7291e8b0058a80611b335369141cd0fe6 Mon Sep 17 00:00:00 2001 From: maudevnc <148285266+maudevnc@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:24:51 -0500 Subject: [PATCH 08/25] React/RN Clients - Add bottom sheet setup (#329) Co-authored-by: Damian Co-authored-by: Damian <52294448+lakardion@users.noreply.github.com> --- .../clients/mobile/react-native/App.tsx | 4 +++ .../clients/mobile/react-native/global.d.ts | 20 ++++++++++- .../clients/mobile/react-native/package.json | 3 +- .../components/sheets/custom-action-sheet.tsx | 34 +++++++++++++++++++ .../src/components/sheets/index.ts | 1 + .../src/components/sheets/register-sheets.ts | 9 +++++ .../src/components/sheets/sample-sheet.tsx | 18 ++++++++++ .../src/components/sheets/sheets.ts | 12 +++++++ .../src/components/sheets/styled.ts | 8 +++++ .../react-native/src/screens/auth/login.tsx | 2 +- .../react-native/src/screens/dashboard.tsx | 26 ++++++++++++-- 11 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/custom-action-sheet.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/index.ts create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/register-sheets.ts create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sheets.ts create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/styled.ts diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx index eb4910bda..aaba6cff5 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx @@ -14,6 +14,8 @@ import React, { useCallback, useEffect, useState } from 'react' import { flushSync } from 'react-dom' import { LogBox, StyleSheet } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { SheetProvider } from 'react-native-actions-sheet' +import '@components/sheets/register-sheets' import './global.css' LogBox.ignoreLogs(['Require']) @@ -57,8 +59,10 @@ export default Sentry.wrap((): JSX.Element => { return ( + + ) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/global.d.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/global.d.ts index 7059a3128..680fc5eff 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/global.d.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/global.d.ts @@ -2,6 +2,8 @@ import { ScrollViewProps } from 'react-native' import { BounceableProps } from 'rn-bounceable' +import { CustomActionSheetProps } from '@components/sheets/custom-action-sheet' +import { SheetDefinition } from 'react-native-actions-sheet' declare global { // Extend the existing BounceableProps type export interface ExtendedBounceableProps extends BounceableProps { @@ -10,4 +12,20 @@ declare global { export interface ExtendedScrollViewProps extends ScrollViewProps { contentContainerClassName?: string } -} \ No newline at end of file + + export interface ExtendedActionSheetProps extends CustomActionSheetProps { + containerClassName?: string + indicatorClassName?: string + } +} + +declare module 'react-native-actions-sheet' { + interface Sheets { + Sample: SheetDefinition<{ + payload: { + input: string + } + }> + } +} + diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json index 5ef044083..67812b23a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json @@ -73,7 +73,8 @@ "rn-navio": "0.0.6", "rollbar-react-native": "^0.9.3", "zod": "3.21.4", - "zustand": "^4.3.3" + "zustand": "^4.3.3", + "react-native-actions-sheet": "^0.9.6" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/custom-action-sheet.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/custom-action-sheet.tsx new file mode 100644 index 000000000..3992a5f5b --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/custom-action-sheet.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react' +import ActionSheet, { ActionSheetProps, ActionSheetRef } from 'react-native-actions-sheet' + +export interface CustomActionSheetProps extends ActionSheetProps { + children: React.ReactNode + actionSheetRef: React.Ref | undefined + sheetId: string +} + +export const CustomActionSheet: FC = ({ + children, + sheetId, + actionSheetRef, + snapPoints = [100], + ...rest +}) => { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/index.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/index.ts new file mode 100644 index 000000000..76e17a7c8 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/index.ts @@ -0,0 +1 @@ +export * from './sheets' \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/register-sheets.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/register-sheets.ts new file mode 100644 index 000000000..bf08f2fa9 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/register-sheets.ts @@ -0,0 +1,9 @@ +import { registerSheet } from 'react-native-actions-sheet' +import './styled' +import { sheets } from './sheets' + +for (const s of Object.values(sheets)) { + registerSheet(s.name, s) +} + +export {} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx new file mode 100644 index 000000000..477cb2f36 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx @@ -0,0 +1,18 @@ +import { useRef } from 'react' +import { View } from 'react-native' +import { ActionSheetRef, SheetProps } from 'react-native-actions-sheet' +import { Text } from '../text' +import { CustomActionSheet } from './custom-action-sheet' + +export const Sample = (props: SheetProps<'Sample'>) => { + const actionSheetRef = useRef(null) + + return ( + + + Coming soon + {props.payload?.input} + + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sheets.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sheets.ts new file mode 100644 index 000000000..b76306161 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sheets.ts @@ -0,0 +1,12 @@ +import { Sample } from './sample-sheet' + +// add any sheets here and they will be registered in the app +export const sheets = { + test: Sample, +} as const + +export const SHEET_NAMES = Object.fromEntries( + Object.entries(sheets).map(([k, v]) => [k, v.name]), +) as Record + +export type SheetName = (typeof SHEET_NAMES)[keyof typeof SHEET_NAMES] \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/styled.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/styled.ts new file mode 100644 index 000000000..e601937fc --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/styled.ts @@ -0,0 +1,8 @@ + +import { cssInterop } from 'nativewind' +import ActionSheet from 'react-native-actions-sheet' + +cssInterop(ActionSheet, { + containerClassName: 'containerStyle', + indicatorClassName: 'indicatorStyle', +}) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx index c56be3f4a..b3856972a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx @@ -39,7 +39,7 @@ const LoginInner = () => { Log in - + { + + const onOpenSheet = () => { + SheetManager.show(SHEET_NAMES.test, { + payload: { + input: 'Hello from payload', + }, + }) + } + return ( - - Welcome to the Dashboard - + + + + Welcome to the Dashboard + + + + + + ) } From 6c7a2a4867f77741e40ef64a464b77a339711afc Mon Sep 17 00:00:00 2001 From: Damian <52294448+lakardion@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:58:48 +0200 Subject: [PATCH 09/25] React/RN Clients - Add `useLogout` hook to wrap logout call in mutation (#318) --- .../clients/mobile/react-native/App.tsx | 10 +++++-- .../clients/mobile/react-native/package.json | 1 + .../react-native/src/screens/dashboard.tsx | 29 +++++++++++++++---- .../mobile/react-native/src/screens/routes.ts | 1 + .../src/services/user/{user.ts => hooks.ts} | 17 ++++++++++- .../react-native/src/services/user/index.ts | 2 +- .../mobile/react-native/src/stores/auth.ts | 9 +----- .../react-native/src/stores/navigation.ts | 4 +++ .../clients/web/react/src/pages/home.tsx | 10 +++++-- .../web/react/src/services/user/hooks.ts | 18 +++++++++++- .../clients/web/react/src/stores/auth.ts | 13 +-------- 11 files changed, 80 insertions(+), 34 deletions(-) rename {{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/{user.ts => hooks.ts} (64%) create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/navigation.ts diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx index aaba6cff5..75b1950f1 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx @@ -1,6 +1,8 @@ -import { AppRoot } from '@screens/routes' +import '@components/sheets/register-sheets' +import { AppRoot, getNavio } from '@screens/routes' import * as Sentry from '@sentry/react-native' import { useAuth } from '@stores/auth' +import { navioAtom } from '@stores/navigation' import { QueryClientProvider } from '@tanstack/react-query' import { customFonts } from '@utils/fonts' import { queryClient } from '@utils/query-client' @@ -10,12 +12,12 @@ import { loadAsync } from 'expo-font' import { setNotificationHandler } from 'expo-notifications' import * as SplashScreen from 'expo-splash-screen' import { StatusBar } from 'expo-status-bar' +import { useSetAtom } from 'jotai' import React, { useCallback, useEffect, useState } from 'react' import { flushSync } from 'react-dom' import { LogBox, StyleSheet } from 'react-native' -import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SheetProvider } from 'react-native-actions-sheet' -import '@components/sheets/register-sheets' +import { GestureHandlerRootView } from 'react-native-gesture-handler' import './global.css' LogBox.ignoreLogs(['Require']) @@ -41,11 +43,13 @@ SplashScreen.preventAutoHideAsync() export default Sentry.wrap((): JSX.Element => { const [ready, setReady] = useState(false) const hasLocalStorageHydratedState = useAuth.use.hasHydrated() + const setNavio = useSetAtom(navioAtom) const start = useCallback(async () => { await loadAsync(customFonts) await hasLocalStorageHydratedState await SplashScreen.hideAsync() + setNavio(getNavio()) flushSync(() => { setReady(true) }) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json index 67812b23a..b8308073a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json @@ -57,6 +57,7 @@ "expo-status-bar": "~1.12.1", "expo-updates": "~0.25.14", "expo-web-browser": "~13.0.3", + "jotai": "^2.7.1", "nativewind": "^4.0.36", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx index 0aa87f430..9fd3de3c1 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx @@ -1,11 +1,17 @@ +import { BButton } from '@components/Button' import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' -import { View } from 'react-native' -import { Text } from '@components/text' -import { SheetManager } from 'react-native-actions-sheet' import { SHEET_NAMES } from '@components/sheets' -import { BButton } from '@components/Button' +import { Text } from '@components/text' +import { useLogout } from '@services/user' +import { navioAtom } from '@stores/navigation' +import { useAtomValue } from 'jotai' import React from 'react' +import { View } from 'react-native' +import { SheetManager } from 'react-native-actions-sheet' + export const DashboardScreen = () => { + const navio = useAtomValue(navioAtom) + const { mutate: logout } = useLogout() const onOpenSheet = () => { SheetManager.show(SHEET_NAMES.test, { @@ -23,8 +29,21 @@ export const DashboardScreen = () => { + + { + logout(undefined, { + onSettled: () => { + navio?.setRoot('stacks', 'AuthStack') + }, + }) + }} + variant="primary-transparent" + /> + - + ) } diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts index b823529be..8ca71e2af 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts @@ -49,5 +49,6 @@ export const navio = Navio.build({ export const getNavio = () => navio export const AppRoot = navio.App +export type MyNavio = typeof navio export type AppScreens = Parameters[0] export type AppStacks = Parameters[0] diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/user.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/hooks.ts similarity index 64% rename from {{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/user.ts rename to {{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/hooks.ts index 2f8946c23..e215452c5 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/user.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/hooks.ts @@ -4,9 +4,10 @@ */ import { useEffect } from 'react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useMutation } from '@tanstack/react-query' import { useAuth } from '@stores/auth' import { userApi } from './api' +import { queryClient } from '@utils/query-client' export const useUser = () => { const userId = useAuth.use.userId() @@ -28,3 +29,17 @@ export const useUser = () => { return data } + + +/** + * To use directly in components + */ +export const useLogout = () => { + return useMutation({ + mutationFn: userApi.csc.logout, + onSettled: () => { + useAuth.getState().actions.clearAuth() + queryClient.invalidateQueries({ queryKey: ['user'] }) + }, + }) +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/index.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/index.ts index d863d5335..2e69e848c 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/index.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/index.ts @@ -1,4 +1,4 @@ export * from './forms' export * from './api' export * from './models' -export * from './user' \ No newline at end of file +export * from './hooks' \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/auth.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/auth.ts index 8323b2ab9..aeb609ef7 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/auth.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/auth.ts @@ -1,8 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' -import { UserShape as User, userApi } from '@services/user/' -import { queryClient } from '@utils/query-client' +import { UserShape as User } from '@services/user/' import { createSelectors } from '@stores/utils' type AuthState = { @@ -90,9 +89,3 @@ export const useAuth = createSelectors( ), ), ) - -export const logout = async() => { - await userApi.csc.logout() - useAuth.getState().actions.clearAuth() - queryClient.invalidateQueries({queryKey: ['user']}) -} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/navigation.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/navigation.ts new file mode 100644 index 000000000..21cf52c6c --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/stores/navigation.ts @@ -0,0 +1,4 @@ +import type { MyNavio } from '@screens/routes' +import { atom } from 'jotai' + +export const navioAtom = atom(null) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx index 91143af2d..91d5f286f 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx @@ -1,12 +1,16 @@ import React from 'react' import { useNavigate } from 'react-router-dom' -import { logout } from 'src/stores/auth' +import { useLogout } from 'src/services/user' export const Home = () => { const navigate = useNavigate() + const { mutate: logout, isPending: isLoggingOut } = useLogout() const logOutUser = () => { - logout() - navigate('/log-in') + logout(undefined,{ + onSettled:()=>{ + navigate('/log-in') + } + }) } return ( diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/hooks.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/hooks.ts index 29ad7a0e1..8bc62ef10 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/hooks.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/hooks.ts @@ -1,9 +1,11 @@ -import { useQuery } from '@tanstack/react-query' +import { useQuery, useMutation } from '@tanstack/react-query' import { useAuth } from 'src/stores/auth' import { useEffect } from 'react' import { HttpStatusCode, isAxiosError } from 'axios' import { useNavigate } from 'react-router-dom' import { userQueries } from './queries' +import { queryClient } from 'src/utils/query-client' +import { userApi } from './api' export const useUser = () => { const { clearAuth } = useAuth.use.actions() @@ -32,3 +34,17 @@ export const useUser = () => { return query } + +/** + * To use directly in components + */ +export const useLogout = () => { + return useMutation({ + mutationFn: userApi.csc.logout, + onSettled: () => { + useAuth.getState().actions.clearAuth() + queryClient.invalidateQueries({ queryKey: userQueries.all() }) + localStorage.clear() + }, + }) +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/stores/auth.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/stores/auth.ts index b618e410d..41421587b 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/stores/auth.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/stores/auth.ts @@ -1,7 +1,6 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import { User, userApi, userQueries } from '../services/user' -import { queryClient } from '../utils/query-client' +import { User } from '../services/user' import { createSelectors } from './utils' type AuthState = { @@ -85,13 +84,3 @@ export const useAuth = createSelectors( ), ), ) - -export const logout = async () => { - try { - await userApi.csc.logout() - } catch (e) { - console.error - } - useAuth.getState().actions.clearAuth() - queryClient.invalidateQueries({ queryKey: userQueries.all() }) -} From 4c5432bd8a60a5df1ad24d90ee8fdd3b3f050b3f Mon Sep 17 00:00:00 2001 From: paribaker <58012003+paribaker@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:14:10 -0500 Subject: [PATCH 10/25] Fix/expo vars (#333) Co-authored-by: Pari Work Temp --- README.md | 5 ++ .../clients/mobile/react-native/Config.js | 49 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5de9f1cb4..3b20cb157 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ A production-ready Django SPA app on Heroku in 20-min or less! First, get cookiecutter, as detailed in the [official documentation](https://cookiecutter.readthedocs.io/en/stable/installation.html#install-cookiecutter). + +You will also need some of the libraries being used to generate your artifacts + +`python -m pip install Jinja2 jinja2-time` + Now run it against this repo: ```bash diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js b/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js index 65194934f..7364cb9c1 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js @@ -1,4 +1,5 @@ import Constants, { ExecutionEnvironment } from 'expo-constants' +import * as Updates from 'expo-updates'; import { Platform } from 'react-native' import Logger from './logger' @@ -11,19 +12,58 @@ const { backendServerUrl, rollbarAccessToken, sentryDSN } = Constants?.expoConfi const isExpoGo = Constants.executionEnvironment === ExecutionEnvironment.StoreClient const isAndroid = Platform.OS === 'android' + +const config = { + 'expo-go':{ + backendServerUrl: BACKEND_SERVER_URL, + rollbarAccessToken: isAndroid ? null : ROLLBAR_ACCESS_TOKEN, + sentryDSN: SENTRY_DSN, + + }, + 'expo_build':{ + 'staging':{ + backendServerUrl: backendServerUrl || BACKEND_SERVER_URL || 'https://{{ cookiecutter.project_slug }}-staging.herokuapp.com', + rollbarAccessToken: isAndroid ? null : rollbarAccessToken || ROLLBAR_ACCESS_TOKEN || "", + sentryDSN: sentryDSN || SENTRY_DSN || "", + }, + 'production':{ + backendServerUrl: backendServerUrl || BACKEND_SERVER_URL || 'https://{{ cookiecutter.project_slug }}-staging.herokuapp.com', + rollbarAccessToken: isAndroid ? null : rollbarAccessToken || ROLLBAR_ACCESS_TOKEN || "", + sentryDSN: sentryDSN || SENTRY_DSN || "", + } + + } + +} + + const ENV = () => { if (!isExpoGo) { - let rollbarToken = isAndroid ? undefined : rollbarAccessToken - const logger = new Logger(rollbarToken).logger + /** + * + * Temporary manual hack to set variables, due to an issue we are facing with expo-updates + * Pari Baker + * 2024-05-08 + */ + if (Updates.channel === 'staging') { + const logger = new Logger(config.expo_build.staging.rollbarAccessToken).logger + return {...config.expo_build.staging, logger} + }else if(Updates.channel === 'production'){ + const logger = new Logger(config.expo_build.production.rollbarAccessToken).logger + return {...config.expo_build.production, logger} + } + return { backendServerUrl, logger, sentryDSN, + isExpoGo, } } - let rollbarToken = isAndroid ? undefined : ROLLBAR_ACCESS_TOKEN - const logger = new Logger(rollbarToken).logger + + const logger = new Logger(config['expo-go'].rollbarAccessToken).logger + return { backendServerUrl: BACKEND_SERVER_URL, logger, @@ -33,4 +73,5 @@ const ENV = () => { } const Config = { ...ENV() } + export default Config \ No newline at end of file From e81bf10a6483a7022732e55179be6016cd32b8d7 Mon Sep 17 00:00:00 2001 From: Edwin Noriega <40014117+noriega2112@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:17:15 -0600 Subject: [PATCH 11/25] React - Match styles to vue client (#255) Co-authored-by: Damian --- cookiecutter.json | 1 - .../web/react/src/assets/images/bars.svg | 3 + .../web/react/src/assets/images/glyph.svg | 3 + .../web/react/src/assets/images/icon.png | Bin 0 -> 10786 bytes .../web/react/src/assets/images/loading.svg | 4 + .../web/react/src/assets/images/logo.svg | 27 ++- .../src/assets/images/profile-circle.svg | 6 + .../web/react/src/assets/images/x-mark.svg | 3 + .../web/react/src/components/auth-layout.tsx | 23 +- .../web/react/src/components/button.tsx | 12 +- .../web/react/src/components/errors.tsx | 2 +- .../web/react/src/components/input.tsx | 41 ++-- .../clients/web/react/src/components/logo.tsx | 5 + .../web/react/src/components/nav-bar.tsx | 212 ++++++++++++++++++ .../react/src/components/password-input.tsx | 6 +- .../web/react/src/components/spinner.tsx | 2 +- .../clients/web/react/src/pages/dashboard.tsx | 12 + .../clients/web/react/src/pages/home.tsx | 37 +-- .../clients/web/react/src/pages/layout.tsx | 4 +- .../clients/web/react/src/pages/log-in.tsx | 92 ++++---- .../web/react/src/pages/page-not-found.tsx | 23 ++ .../src/pages/request-password-reset.tsx | 108 +++++++++ .../web/react/src/pages/reset-password.tsx | 66 +++--- .../clients/web/react/src/pages/sign-up.tsx | 97 ++++---- .../clients/web/react/src/utils/errors.ts | 26 +++ .../clients/web/react/src/utils/routes.tsx | 42 ++-- .../clients/web/react/tailwind.colors.js | 20 +- .../clients/web/react/tailwind.config.js | 12 + .../react/tests/e2e/specs/test-login.cy.ts | 17 +- 29 files changed, 689 insertions(+), 217 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/bars.svg create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/glyph.svg create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/icon.png create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/loading.svg create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/profile-circle.svg create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/assets/images/x-mark.svg create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/components/logo.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/components/nav-bar.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/pages/dashboard.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/pages/page-not-found.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/src/utils/errors.ts diff --git a/cookiecutter.json b/cookiecutter.json index 2c44fc413..795d7eaf9 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -12,7 +12,6 @@ "clients/web/vue3/src/components", "clients/web/react/src/components", "clients/web/react/src/pages/app-or-auth.tsx", - "clients/web/react/src/pages/home.tsx", "clients/web/react/src/pages/index.ts", "clients/web/react/src/pages/layout.tsx", "*/swagger-ui.html" diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/bars.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/bars.svg new file mode 100644 index 000000000..ea05b8c4d --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/bars.svg @@ -0,0 +1,3 @@ + + + diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/glyph.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/glyph.svg new file mode 100644 index 000000000..5cd6ccfaf --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/icon.png b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..60af5f7ff79a8ec79b7e1e704dec88dad0b23f9e GIT binary patch literal 10786 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 literal 0 HcmV?d00001 diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/loading.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/loading.svg new file mode 100644 index 000000000..354ddc1f9 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/loading.svg @@ -0,0 +1,4 @@ + + + + diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/logo.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/logo.svg index 620fe2632..17c08afe2 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/logo.svg +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/logo.svg @@ -1,10 +1,23 @@ - - - + + + + + + + + + + + + + + + + + - - - - + + + diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/profile-circle.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/profile-circle.svg new file mode 100644 index 000000000..4dc6235e7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/profile-circle.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/x-mark.svg b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/x-mark.svg new file mode 100644 index 000000000..457d8e626 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/assets/images/x-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/auth-layout.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/auth-layout.tsx index 350e2ddf2..ed6163c8e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/auth-layout.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/auth-layout.tsx @@ -1,20 +1,21 @@ import { FC, ReactNode } from 'react' -import { Outlet } from 'react-router-dom' +import { Logo } from 'src/components/logo' -export const AuthLayout: FC<{ title: string; description: string; children: ReactNode }> = ({ +export const AuthLayout: FC<{ title: string; description?: string; children: ReactNode }> = ({ children, description, title, }) => { return ( -
-
-
-
{title}
-

{description}

- - {children} - - +
+
+ +
+ {title} +
+ {description &&

{description}

} +
+ {children} +
) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/button.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/button.tsx index 7376c3a3f..c20f4268e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/button.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/button.tsx @@ -2,11 +2,13 @@ import { ButtonHTMLAttributes, FC, ReactNode } from 'react' import { Link, LinkProps } from 'react-router-dom' import { Spinner } from './spinner' -type ButtonVariant = 'primary' | 'ghost' | 'discreet' +type ButtonVariant = 'primary' | 'accent' | 'disabled' const buttonVariantMap: Record = { - primary: 'text-white bg-primary px-4 py-2 active:bg-slate-400 ', - discreet: 'text-primary hover:shadow-none', - ghost: 'border border-primary text-primary py-2 px-4 active:bg-primary active:text-white', + primary: + 'flex w-full cursor-pointer items-center justify-center rounded-md border px-3 py-2 text-sm font-semibold shadow-sm border-primary text-white hover:bg-primaryLight bg-primary', + accent: '', + disabled: + 'flex w-full cursor-pointer items-center justify-center rounded-md border px-3 py-2 text-sm font-semibold shadow-sm cursor-not-allowed border-gray-200 bg-gray-200', } type CommonButtonProps = { @@ -40,7 +42,7 @@ export const Button: FC< className={` rounded-lg transition-transform hover:scale-[1.05] hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:transform-none ${buttonVariantMap[variant]} ${extendClassName}`} disabled={props.disabled || isLoading} > - {props.isLoading ? : children} + {props.isLoading ? : children} ) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/errors.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/errors.tsx index 47810e787..6b03fc826 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/errors.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/errors.tsx @@ -2,7 +2,7 @@ import { IFormFieldError } from '@thinknimble/tn-forms' import { FC, Fragment, ReactNode } from 'react' export const ErrorMessage: FC<{ children: ReactNode }> = ({ children }) => { - return

{children}

+ return

{children}

} /** diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/input.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/input.tsx index 84873375b..ebb3a034c 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/input.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/input.tsx @@ -1,24 +1,31 @@ import { FC, InputHTMLAttributes, ReactNode } from 'react' export const Input: FC< - InputHTMLAttributes & { extendClassName?: string; icon?: ReactNode } -> = ({ className, extendClassName, icon, ...props }) => { + InputHTMLAttributes & { + extendClassName?: string + icon?: ReactNode + label?: string + } +> = ({ className, extendClassName, icon, label, ...props }) => { return ( -
- - {icon ? ( -
{icon}
- ) : null} +
+ {label && {label}} +
+ + {icon ? ( +
{icon}
+ ) : null} +
) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/logo.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/logo.tsx new file mode 100644 index 000000000..b9fc58ef1 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/logo.tsx @@ -0,0 +1,5 @@ +import LogoImage from '../assets/images/icon.png' + +export const Logo = () => { + return ThinkNimble +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/nav-bar.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/nav-bar.tsx new file mode 100644 index 000000000..17a93e359 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/nav-bar.tsx @@ -0,0 +1,212 @@ +import { NavLink, Link, useNavigate } from 'react-router-dom' +import { useAuth } from 'src/stores/auth' +import { useState } from 'react' +import { useLogout, useUser } from 'src/services/user' +import BarsIcon from '../assets/images/bars.svg' +import XMark from '../assets/images/x-mark.svg' +import Logo from '../assets/images/logo.svg' +import ProfileCircle from '../assets/images/profile-circle.svg' +import { User } from 'src/services/user/models' + +const UserInfo = ({ user }: { user: User | undefined }) => { + return ( + <> +
+ {user?.firstName} {user?.lastName} +
+
{user?.email}
+ + ) +} + +export const NavBar = () => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [profileMenuOpen, setProfileMenuOpen] = useState(false) + + const token = useAuth.use.token() + const { data: user } = useUser() + const isAuth = Boolean(token) + const { mutate: logout } = useLogout() + + const navigate = useNavigate() + const logOutUser = () => { + toggleMobileMenu() + logout(undefined, { + onSettled: () => { + navigate('/log-in') + }, + }) + } + + const toggleMobileMenu = () => setMobileMenuOpen(!mobileMenuOpen) + + return ( + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/password-input.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/password-input.tsx index a14e8e7a4..46b8285ba 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/password-input.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/password-input.tsx @@ -3,7 +3,11 @@ import { Input } from './input' import { FaEye, FaEyeSlash } from 'react-icons/fa' export const PasswordInput: FC< - InputHTMLAttributes & { extendClassName?: string; iconTabIndex?: number } + InputHTMLAttributes & { + extendClassName?: string + iconTabIndex?: number + label?: string + } > = ({ extendClassName, iconTabIndex, ...props }) => { const [showPassword, setShowPassword] = useState(false) const onTogglePassword = () => { diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/components/spinner.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/components/spinner.tsx index 873a23ced..f63fed61b 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/components/spinner.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/components/spinner.tsx @@ -2,7 +2,7 @@ import { CSSProperties, FC } from 'react' import { colors } from 'tailwind.colors' export type SizeVariant = 'xs' | 'sm' | 'md' | 'lg' | 'xl' -const BASE_SIZE = 20 +const BASE_SIZE = 12 const BASE_BORDER = 2 const PRIMARY_COLOR = colors.primary[500] const mapWidthHeightBySize: Record< diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/dashboard.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/dashboard.tsx new file mode 100644 index 000000000..0e60376f2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/dashboard.tsx @@ -0,0 +1,12 @@ +export const Dashboard = () => { + return ( +
+
+
+

Dashboard

+
+
+
Content goes here...
+
+ ) +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx index 91d5f286f..951259d10 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/home.tsx @@ -1,22 +1,31 @@ -import React from 'react' -import { useNavigate } from 'react-router-dom' -import { useLogout } from 'src/services/user' +import { Link } from 'react-router-dom' +import { useAuth } from 'src/stores/auth' export const Home = () => { - const navigate = useNavigate() - const { mutate: logout, isPending: isLoggingOut } = useLogout() - const logOutUser = () => { - logout(undefined,{ - onSettled:()=>{ - navigate('/log-in') - } - }) - } + const token = useAuth.use.token() + const isAuth = Boolean(token) return ( <> -

Dashboard

- +
+

+ Welcome to {{ cookiecutter.project_name }}! +

+

+ Here's some information about {{ cookiecutter.project_name }}. Please update and + expand on this text. This text is the first thing that users will see on the home page. +

+ {isAuth && ( +
+ + Get Started + +
+ )} +
) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/layout.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/layout.tsx index 999caa27f..db540f8e1 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/layout.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/layout.tsx @@ -1,8 +1,10 @@ import { Outlet } from 'react-router-dom' +import { NavBar } from 'src/components/nav-bar' export const Layout = () => { return ( -
+
+
) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx index 46f96f1a6..dd77bd035 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx @@ -3,33 +3,33 @@ import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' import { useState } from 'react' import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom' import { Button } from 'src/components/button' -import { ErrorsList } from 'src/components/errors' +import { ErrorMessage, ErrorsList } from 'src/components/errors' import { Input } from 'src/components/input' -import { LoginForm, LoginFormInputs, TLoginForm, userApi } from 'src/services/user' +import { LoginForm, TLoginForm, LoginFormInputs, userApi } from 'src/services/user' -import { useAuth } from 'src/stores/auth' import { useFollowupRoute } from 'src/utils/auth' +import { useAuth } from 'src/stores/auth' +import { PasswordInput } from 'src/components/password-input' +import { getErrorMessages } from 'src/utils/errors' +import { AuthLayout } from 'src/components/auth-layout' function LogInInner() { const params = useLocation() - const autoError = params.state?.autoError - const [error, setError] = useState(autoError ? true : false) + const [errorMessage, setErrorMessage] = useState() const { changeToken, changeUserId } = useAuth.use.actions() const { createFormFieldChangeHandler, form } = useTnForm() const navigate = useNavigate() - const { mutate: logIn } = useMutation({ + const { mutate: logIn, isPending } = useMutation({ mutationFn: userApi.csc.login, onSuccess: (data) => { - if (!data.token) throw new Error('Missing token from response') changeToken(data.token) changeUserId(data.id) - navigate('/home') + navigate('/dashboard') }, onError(e: any) { - if (e?.message === 'Please enter valid credentials') { - setError(true) - } + const errors = getErrorMessages(e) + setErrorMessage(errors) }, }) @@ -49,10 +49,8 @@ function LogInInner() { } return ( -
-
Login
-
-

Enter your login credentials below

+ +
{ e.preventDefault() @@ -61,47 +59,59 @@ function LogInInner() { >
createFormFieldChangeHandler(form.email)(e.target.value)} value={form.email.value ?? ''} data-cy="email" id="id" + label="Email address" />
- { - createFormFieldChangeHandler(form.password)(e.target.value) - }} - value={form.password.value ?? ''} - data-cy="password" - id="password" - /> - +
+
+ +
+ +

Forgot password?

+ +
+
+ { + createFormFieldChangeHandler(form.password)(e.target.value) + }} + value={form.password.value ?? ''} + data-cy="password" + id="password" + /> + +
-
- - Forgot password? - -
-
-
-

Don't have an account?

- - Register here + +
+

Don't have an account?

+ + Sign up.
-
+ ) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/page-not-found.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/page-not-found.tsx new file mode 100644 index 000000000..7f04b7a1a --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/page-not-found.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom' + +export const PageNotFound = () => { + return ( +
+
+

404

+

Page not found

+

+ Sorry, we couldn't find the page you're looking for. +

+
+ + Go back home + +
+
+
+ ) +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx new file mode 100644 index 000000000..31ef49e1c --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx @@ -0,0 +1,108 @@ +import { useMutation } from '@tanstack/react-query' +import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { AuthLayout } from 'src/components/auth-layout' +import { Button } from 'src/components/button' +import { ErrorMessage, ErrorsList } from 'src/components/errors' +import { Input } from 'src/components/input' +import { + EmailForgotPasswordForm, + EmailForgotPasswordInput, + TEmailForgotPasswordForm, + userApi, +} from 'src/services/user' +import { getErrorMessages } from 'src/utils/errors' + +export const RequestPasswordResetInner = () => { + const [errorMessage, setErrorMessage] = useState() + const [resetLinkSent, setResetLinkSent] = useState(false) + const { createFormFieldChangeHandler, form } = useTnForm() + const navigate = useNavigate() + + const { mutate: requestReset } = useMutation({ + mutationFn: userApi.csc.requestPasswordReset, + onSuccess: (data) => { + setErrorMessage(undefined) + setResetLinkSent(true) + }, + onError(e: any) { + const errors = getErrorMessages(e) + setErrorMessage(errors) + }, + }) + + const handleRequest = () => { + const input = { + email: form.email.value ?? '', + } + requestReset(input) + } + + return ( + +
+ {resetLinkSent ? ( + <> +

+ Your request has been submitted. If there is an account associated with the email + provided, you should receive an email momentarily with instructions to reset your + password. +

+

+ If you do not see the email in your main folder soon, please make sure to check your + spam folder. +

+
+ +
+ + ) : ( + <> +
{ + e.preventDefault() + }} + className="flex flex-col gap-2" + > + createFormFieldChangeHandler(form.email)(e.target.value)} + value={form.email.value ?? ''} + data-cy="email" + id="id" + label="Email address" + /> + +
+ {errorMessage} +
+ + + + )} +
+
+ ) +} + +export const RequestPasswordReset = () => { + return ( + formClass={EmailForgotPasswordForm}> + + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx index 637a25a88..1f27a30d3 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx @@ -56,42 +56,46 @@ export const ResetPasswordInner = () => { title="Successfully reset password" description="You can now log in with your new password" > -
+ + Go to login + ) } return ( -
-
- createFormFieldChangeHandler(form.password)(e.target.value)} - extendClassName="w-full" - placeholder={form.password.placeholder} - tabIndex={1} - iconTabIndex={4} - /> - -
-
- { - createFormFieldChangeHandler(form.confirmPassword)(e.target.value) - }} - extendClassName="w-full" - placeholder={form.confirmPassword.placeholder} - tabIndex={2} - iconTabIndex={5} - /> - -
- -
- {error ? {error} : null} +
+
+
+ createFormFieldChangeHandler(form.password)(e.target.value)} + extendClassName="w-full" + placeholder={form.password.placeholder} + tabIndex={1} + iconTabIndex={4} + /> + +
+
+ { + createFormFieldChangeHandler(form.confirmPassword)(e.target.value) + }} + extendClassName="w-full" + placeholder={form.confirmPassword.placeholder} + tabIndex={2} + iconTabIndex={5} + /> + +
+ +
+ {error ? {error} : null} +
) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/sign-up.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/sign-up.tsx index c12a993ba..630c58eab 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/sign-up.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/sign-up.tsx @@ -6,11 +6,14 @@ import { Link, useNavigate } from 'react-router-dom' import { Button } from 'src/components/button' import { ErrorMessage, ErrorsList } from 'src/components/errors' import { Input } from 'src/components/input' +import { AccountForm, TAccountForm, AccountFormInputs } from 'src/services/user/forms' import { userApi } from 'src/services/user' -import { AccountForm, AccountFormInputs, TAccountForm } from 'src/services/user/forms' import { isAxiosError } from 'axios' import { GENERIC_REQUEST_ERROR } from 'src/utils/constants' import { useAuth } from 'src/stores/auth' +import { PasswordInput } from 'src/components/password-input' +import { getErrorMessages } from 'src/utils/errors' +import { AuthLayout } from 'src/components/auth-layout' function SignUpInner() { const [errors, setErrors] = useState([]) @@ -57,80 +60,82 @@ function SignUpInner() { } return ( -
-
WELCOME
-

Enter your details below to create an account

-
-
-
- { - createFormFieldChangeHandler(form.firstName)(e.target.value) - }} - /> - + +
+ +
+
+ { + createFormFieldChangeHandler(form.firstName)(e.target.value) + }} + label="First Name" + /> + +
+
+ { + createFormFieldChangeHandler(form.lastName)(e.target.value) + }} + label="Last Name" + /> + + +
{ - createFormFieldChangeHandler(form.lastName)(e.target.value) + createFormFieldChangeHandler(form.email)(e.target.value) }} + label="Email" /> - - +
-
-
- { - createFormFieldChangeHandler(form.email)(e.target.value) - }} - /> - -
-
- { createFormFieldChangeHandler(form.password)(e.target.value) }} + label="Password" /> -
-
- { createFormFieldChangeHandler(form.confirmPassword)(e.target.value) }} + label="Confirm Password" /> -
- - + {errors.length ? errors.map((e, idx) => {e}) : null} -
-

Already have an account?

- - Log in here + +
+
+

Already have an account?

+ + + Log in.
-
+ ) } const confirmPasswordValidator = { confirmPassword: new MustMatchValidator({ - message: 'passwordsMustMatch', + message: 'Passwords must match', matcher: 'password', }), } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/utils/errors.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/errors.ts new file mode 100644 index 000000000..b51f08839 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/errors.ts @@ -0,0 +1,26 @@ +import { isAxiosError } from 'axios' + +type ErrorData = string[] | { [key: string]: string[] } + +function extractErrorMessages(data: ErrorData | undefined): string[] { + if (Array.isArray(data) && data.length && typeof data[0] === 'string') { + return data + } else if ( + typeof data === 'object' && + Object.keys(data).every((key) => Array.isArray((data as { [key: string]: string[] })[key])) + ) { + // Use type assertion within every callback to access property with key + return Object.values(data).flat() + } else { + return ['Something went wrong'] + } +} + +export function getErrorMessages(e: Error, defaultMessage = 'Something went wrong'): string[] { + if (isAxiosError(e)) { + const { data } = e.response ?? {} + return extractErrorMessages(data) + } else { + return [defaultMessage] + } +} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx index 5fff19ba2..855c23dec 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx @@ -1,30 +1,28 @@ -import { Navigate, Route, Routes } from 'react-router-dom' -import { Home, LogIn, SignUp } from 'src/pages' -import { ForgotPassword } from 'src/pages/forgot-password' +import { Route, Routes, Navigate } from 'react-router-dom' +import { Home, Layout, LogIn, SignUp } from 'src/pages' +import { Dashboard } from 'src/pages/dashboard' +import { PageNotFound } from 'src/pages/page-not-found' +import { RequestPasswordReset } from 'src/pages/request-password-reset' import { ResetPassword } from 'src/pages/reset-password' import { useAuth } from 'src/stores/auth' const PrivateRoutes = () => { return ( - - } /> + <> + } /> Hello from private
} /> - } /> - + ) } const AuthRoutes = () => { return ( - - - } /> - } /> - } /> - } /> - } /> - - + <> + } /> + } /> + } /> + } /> + ) } @@ -32,6 +30,14 @@ export const AppRoutes = () => { const token = useAuth.use.token() const isAuth = Boolean(token) - if (!isAuth) return - return + return ( + + }> + } /> + } /> + {isAuth ? PrivateRoutes() : AuthRoutes()} + } /> + + + ) } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tailwind.colors.js b/{{cookiecutter.project_slug}}/clients/web/react/tailwind.colors.js index 20d611b8b..bb8156f5f 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/tailwind.colors.js +++ b/{{cookiecutter.project_slug}}/clients/web/react/tailwind.colors.js @@ -1,17 +1,9 @@ export const colors = { - primary: { - DEFAULT: '#f0574f', - 50: '#fef3f2', - 100: '#fee3e2', - 200: '#fecdca', - 300: '#fca9a5', - 400: '#f87871', - 500: '#f0574f', - 600: '#dc2f26', - 700: '#b9241c', - 800: '#99211b', - 900: '#7f221d', - 950: '#450d0a', - }, + primary: '#042642', + primaryLight: '#183A56', + accent: '#d93a00', + success: '#4faf64', + warning: '#f4b942', + error: '#d72638', dark: '#2a2d2e', } diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tailwind.config.js b/{{cookiecutter.project_slug}}/clients/web/react/tailwind.config.js index 5763fbf84..72cbfc127 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/tailwind.config.js +++ b/{{cookiecutter.project_slug}}/clients/web/react/tailwind.config.js @@ -6,6 +6,18 @@ module.exports = { theme: { extend: { colors, + fontFamily: { + avenir: ['Avenir', 'Helvetica', 'Arial', 'sans-serif'], + }, + }, + container: { + padding: { + DEFAULT: '1rem', + sm: '2rem', + lg: '4rem', + xl: '5rem', + '2xl': '6rem', + }, }, }, plugins: [], diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts index 00c320f3b..dd9e8fcee 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts @@ -1,13 +1,14 @@ describe('Tests login workflow', () => { - it('Home page auto redirects to login', () => { + it('Home page has link to login', () => { cy.visit('/') + cy.get('[data-cy=login]').click() cy.url().should('include', '/log-in') }), - it('Home page auto redirects to login', () => { - cy.visit('/log-in') - cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) - cy.get('[data-cy=password]').type(Cypress.env('TEST_USER_PASS')) - cy.get('[data-cy="login-btn"]').click() - cy.url().should('include', '/home') - }) + it('Home page auto redirects to login', () => { + cy.visit('/log-in') + cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) + cy.get('[data-cy=password]').type(Cypress.env('TEST_USER_PASS')) + cy.get('[data-cy=submit]').click() + cy.url().should('include', '/dashboard') + }) }) From 4afeee9b6be3daff321d704e741a30df66b71eb6 Mon Sep 17 00:00:00 2001 From: paribaker <58012003+paribaker@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:03:01 -0500 Subject: [PATCH 12/25] Update the Boostrapper mobile review pipeline (#338) Co-authored-by: Damian Co-authored-by: Pari Work Temp --- .github/workflows/cypress.yml | 75 --------------- .github/workflows/mobile.yml | 96 +++++++++++++++++++ .vscode/launch.json | 17 ++++ resources/app.config.vars.txt | 8 ++ resources/eas-bs.json | 45 --------- resources/eas.vars.txt | 18 ++++ scripts/vue_or_react.sh | 2 +- .../.github/workflows/expo-pr.yml | 1 - .../clients/mobile/react-native/Config.js | 1 - .../clients/mobile/react-native/README.md | 20 +++- .../clients/mobile/react-native/eas.json | 8 +- .../clients/mobile/react-native/package.json | 2 +- .../resources/app.config.vars.template.txt | 8 ++ .../resources/eas.vars.template.txt | 17 ++++ .../scripts/setup_mobile_config.sh | 42 ++++++++ 15 files changed, 230 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/mobile.yml create mode 100644 .vscode/launch.json create mode 100644 resources/app.config.vars.txt delete mode 100644 resources/eas-bs.json create mode 100644 resources/eas.vars.txt create mode 100644 {{cookiecutter.project_slug}}/resources/app.config.vars.template.txt create mode 100644 {{cookiecutter.project_slug}}/resources/eas.vars.template.txt create mode 100644 {{cookiecutter.project_slug}}/scripts/setup_mobile_config.sh diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index a0d6d87cf..c6103eafb 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -84,78 +84,3 @@ jobs: CYPRESS_TEST_USER_EMAIL: "cypress@example.com" CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} CYPRESS_baseUrl: ${{ github.event.deployment_status.environment_url }} - - configuremobile: - needs: Setup - runs-on: ubuntu-latest - outputs: - BUILD_MOBILE_APP: ${{ steps.checkdiff.outputs.BUILD_MOBILE_APP }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 5 - name: check diff - - id: checkdiff - run: | - git fetch origin main - echo $(git diff --name-only origin/main -- | grep "/clients/mobile/react-native/" | wc -l) - echo "BUILD_MOBILE_APP=$(git diff --name-only origin/main -- | grep "/clients/mobile/react-native/" | wc -l)" >> $GITHUB_OUTPUT - - getprnumber: - runs-on: ubuntu-latest - needs: configuremobile - if: needs.configuremobile.outputs.BUILD_MOBILE_APP != 0 - steps: - - uses: jwalton/gh-find-current-pr@master - id: findPr - - name: Set name for PR - if: success() && steps.findPr.outputs.number - run: echo "PR=pr-${PR}" >> $GITHUB_ENV - env: - PR: ${{ steps.findPr.outputs.pr }} - outputs: - PR: ${{ env.PR }} - - publish: - runs-on: ubuntu-latest - needs: getprnumber - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: my_project - path: my_project/ - - uses: actions/cache@v4 - with: - path: "**/node_modules" - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - name: 🏗 Setup Node - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - name: 🏗 Setup Expo and EAS - uses: expo/expo-github-action@v7 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: 📦 Install dependencies - working-directory: ./my_project/mobile - run: yarn install - - - name: Copy Config files - run: | - ls - cp resources/app-bs.config.js my_project/mobile/app.config.js - cp resources/eas-bs.json my_project/mobile/eas.json - - - name: 🚀 Publish preview - working-directory: ./my_project/mobile - run: | - eas update --branch="${{ needs.getprnumber.outputs.PR }}" --non-interactive --auto - echo "${{ env.BACKEND_SERVER_URL }} and ${{ needs.getprnumber.outputs.PR }}" - env: - EXPO_PUBLIC_BACKEND_SERVER_URL: "${{ github.event.deployment_status.environment_url }}" - EXPO_PUBLIC_ROLLBAR_ACCESS_TOKEN: "1a19e5da05b2435b802d5a81aba2bbd7" \ No newline at end of file diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml new file mode 100644 index 000000000..16a54cc2b --- /dev/null +++ b/.github/workflows/mobile.yml @@ -0,0 +1,96 @@ +name: Mobile Deployment +on: [deployment_status, workflow_dispatch] + +jobs: + Setup: + if: github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx install pipenv + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + # Can't use cache because of https://github.com/actions/cache/issues/319 + # cache: 'pipenv' + - name: Install bootstrapper dependencies + run: pipenv install --dev --deploy + - run: | + config_file=$(./scripts/vue_or_react.sh) + pipenv run cookiecutter . --config-file $config_file --no-input -f + cat $config_file + - uses: actions/upload-artifact@v4 + with: + name: my_project + path: my_project/ + retention-days: 1 + configuremobile: + needs: Setup + runs-on: ubuntu-latest + outputs: + BUILD_MOBILE_APP: ${{ steps.checkdiff.outputs.BUILD_MOBILE_APP }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 5 + name: check diff + - id: checkdiff + run: | + git fetch origin main + if [ {{ "${{ github.event_name }}" }} == "workflow_dispatch" ]; then + echo "This workflow was triggered by a workflow_dispatch event." + echo "BUILD_MOBILE_APP=1" >> $GITHUB_OUTPUT + else + echo $(git diff --name-only origin/main -- | grep "/clients/mobile/react-native/" | wc -l) + echo "BUILD_MOBILE_APP=$(git diff --name-only origin/main -- | grep "/clients/mobile/react-native/" | wc -l)" >> $GITHUB_OUTPUT + fi + + + publish: + needs: configuremobile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: my_project + path: my_project/ + - name: Create config files + run: | + cp resources/app.config.vars.txt my_project/resources/app.config.vars.txt + cp resources/eas.vars.txt my_project/resources/eas.vars.txt + cd my_project + current_dir=$(pwd) + . scripts/setup_mobile_config.sh "$current_dir/mobile/app.config.js" "$current_dir/resources/app.config.vars.txt" + . scripts/setup_mobile_config.sh "$current_dir/mobile/eas.json" "$current_dir/resources/eas.vars.txt" + + # - uses: actions/cache@v4 + # with: + # path: "**/node_modules" + # key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} + - name: 🏗 Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18.x + # cache: npm + # cache-dependency-path: ./mobile/package-lock.json + + - name: 🏗 Setup Expo and EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: 📦 Install dependencies + working-directory: ./my_project/mobile + run: npm install + + - name: 🚀 Publish preview + working-directory: ./my_project/mobile + run: | + eas update --branch="${{ github.event.deployment_status.environment_url }}" --non-interactive --auto + echo "${{ env.BACKEND_SERVER_URL }} and ${{ github.event.deployment_status.environment_url }}" + env: + EXPO_PUBLIC_BACKEND_SERVER_URL: "${{ github.event.deployment_status.environment_url }}" + EXPO_PUBLIC_ROLLBAR_ACCESS_TOKEN: "1a19e5da05b2435b802d5a81aba2bbd7" \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..b93312356 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "zshdb", + "request": "launch", + "name": "Zsh-Debug (select script from list of sh files)", + "cwd": "${workspaceFolder}", + "program": "${command:SelectScriptName}", + "args": ["eas.json",".eas-json-values"] + }, + + ] +} \ No newline at end of file diff --git a/resources/app.config.vars.txt b/resources/app.config.vars.txt new file mode 100644 index 000000000..f1d3456c0 --- /dev/null +++ b/resources/app.config.vars.txt @@ -0,0 +1,8 @@ + +REPLACE_WITH_EXPO_APP_NAME=tn mobile bootstrapper +REPLACE_WITH_EXPO_APP_SLUG=tn-sample-app +REPLACE_WITH_EXPO_OWNER=thinknimble-bootstrapper +REPLACE_WITH_EXPO_APP_ID=ec1b86e2-2582-48cf-8a7a-c6d2772ba4f2 +REPLACE_WITH_SENTRY_ORG=tn-bootstrapper +REPLACE_WITH_IOS_BUNDLE_ID=org.thinknimble.expo.bootstrapper +REPLACE_WITH_ANDROID_PACKAGE_ID=com.example.app diff --git a/resources/eas-bs.json b/resources/eas-bs.json deleted file mode 100644 index ccb2daee3..000000000 --- a/resources/eas-bs.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "cli": { - "version": ">= 2.4.1", - "appVersionSource": "remote" - }, - "build": { - "development": { - "developmentClient": true, - "distribution": "internal", - "channel": "development" - }, - "staging": { - "distribution": "internal", - "channel": "staging", - "env": { - "BACKEND_SERVER_URL": "https://tn-spa-bootstrapper-production.herokuapp.com/", - "BUILD_ENV": "staging", - "ROLLBAR_ACCESS_TOKEN": "1a19e5da05b2435b802d5a81aba2bbd7", - "SENTRY_PROJECT_NAME": "tn-staging", - "SENTRY_DSN": "https://a7cea97f07ac42fa9e28800b037997c9@o4504899535962112.ingest.sentry.io/4504906332897280" - } - }, - "production": { - "channel": "production", - "autoIncrement": true, - "env": { - "BACKEND_SERVER_URL": "https://tn-spa-bootstrapper-staging.herokuapp.com/", - "BUILD_ENV": "production", - "ROLLBAR_ACCESS_TOKEN": "e6246274f1f2411990f0cd1c7b99b072", - "SENTRY_PROJECT_NAME": "tn-prod", - "SENTRY_DSN": "https://df747bb2e58d4f178f433f322bd41026@o4504899535962112.ingest.sentry.io/4504906701733888" - } - } - }, - "submit": { - "production": { - "ios": { - "appName": "TN Bootsrapper RN", - "appleId": "pari@thinknimble.com", - "appleTeamId": "6BNA6HFF6B", - "ascAppId": "12023" - } - } - } - } \ No newline at end of file diff --git a/resources/eas.vars.txt b/resources/eas.vars.txt new file mode 100644 index 000000000..038e4e8eb --- /dev/null +++ b/resources/eas.vars.txt @@ -0,0 +1,18 @@ +REPLACE_WITH_LOCAL_BACKEND_SERVER_URL=https://tn-spa-bootstrapper-production.herokuapp.com +REPLACE_WITH_STAGING_BACKEND_SERVER_URL=https://tn-spa-bootstrapper-production.herokuapp.com +REPLACE_WITH_PRODUCTION_BACKEND_SERVER_URL=https://tn-spa-bootstrapper-production.herokuapp.com +REPLACE_WITH_REVIEW_APP_BACKEND_SERVER_URL=https://tn-spa-bootstrapper-production.herokuapp.com +REPLACE_WITH_LOCAL_ROLLBAR_TOKEN=1a19e5da05b2435b802d5a81aba2bbd7 +REPLACE_WITH_STAGING_ROLLBAR_TOKEN=1a19e5da05b2435b802d5a81aba2bbd7 +REPLACE_WITH_PRODUCTION_ROLLBAR_TOKEN=1a19e5da05b2435b802d5a81aba2bbd7 +REPLACE_WITH_REVIEW_APP_ROLLBAR_TOKEN=1a19e5da05b2435b802d5a81aba2bbd7 +REPLACE_WITH_REVIEW_APP_SENTRY_DSN=https://a7cea97f07ac42fa9e28800b037997c9@o4504899535962112.ingest.sentry.io/4504906332897280 +REPLACE_WITH_LOCAL_SENTRY_DSN=https://a7cea97f07ac42fa9e28800b037997c9@o4504899535962112.ingest.sentry.io/4504906332897280 +REPLACE_WITH_STAGING_SENTRY_DSN=https://a7cea97f07ac42fa9e28800b037997c9@o4504899535962112.ingest.sentry.io/4504906332897280 +REPLACE_WITH_PRODUCTION_SENTRY_DSN=https://a7cea97f07ac42fa9e28800b037997c9@o4504899535962112.ingest.sentry.io/4504906332897280 +REPLACE_WITH_APPSTORE_CONNECT_APP_NAME=TN Bootsrapper RN +REPLACE_WITH_APPLE_ID_EMAIL_SIGNING_APP=pari@thinknimble.com +REPLACE_WITH_APPLESTORE_CONNECT_ID=6446805695 +REPLACE_WITH_APPLE_TEAM_ID=6BNA6HFF6B +REPLACE_WITH_REVIEW_APP_SENTRY_PROJECT_NAME=tn-bootsrapper-rn + diff --git a/scripts/vue_or_react.sh b/scripts/vue_or_react.sh index 7fc7c14c0..eb888ad7d 100755 --- a/scripts/vue_or_react.sh +++ b/scripts/vue_or_react.sh @@ -14,7 +14,7 @@ else fi if [ "$rn_count" != 0 ]; then - cp -r resources/ {{cookiecutter.project_slug}}/ + cp -r resources/ {{cookiecutter.project_slug}}/resources/ sed -i.bak 's/include_mobile: "n"/include_mobile: "y"/' $config_file_path fi diff --git a/{{cookiecutter.project_slug}}/.github/workflows/expo-pr.yml b/{{cookiecutter.project_slug}}/.github/workflows/expo-pr.yml index fd7dff43f..0e7f0a6b6 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/expo-pr.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/expo-pr.yml @@ -50,4 +50,3 @@ jobs: env: EXPO_PUBLIC_BACKEND_SERVER_URL: {{ "${{ github.event.deployment_status.environment_url }}" }} EXPO_PUBLIC_ROLLBAR_ACCESS_TOKEN: "" - diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js b/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js index 7364cb9c1..8f2e3b6e9 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/Config.js @@ -73,5 +73,4 @@ const ENV = () => { } const Config = { ...ENV() } - export default Config \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md b/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md index c9cfd4eef..e1aad464c 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/README.md @@ -15,9 +15,11 @@ User `npm run start` to run the app and source the env variables alternatively if you want to use expo run command -`source .env && SENTRY_PROJECT=${SENTRY_PROJECT_NAME} npx expo start` +`source .env && npx expo start` -**When running the app locally and working against a local backend you will need to use a proxy** +Run against a local backend using the ip address instead of local host + +**When running the app locally and working against a local backend with a proxy** 1. Download and install ngrok 2. Set up ngrok auth token (request an account from William Huster) @@ -59,6 +61,20 @@ Set the `SENTRY_AUTH_TOKEN` in Expo under `Secrets` (see `Error Logging & Crash For local run set environment variables in .env file (from [.env.example](./.env.example)) For builds set env variables in eas.json + + +## Use the helper script to enter variables + +Complete the eas.vars.template.txt + +complete the app.config.vars.template.txt + +`. scripts/setup_mobile_config.sh eas.json /resources/eas.vars.txt` + +`. scripts/setup_mobile_config.sh eas.json /resources/app.config.vars.txt` + + + ### Eas Project Configuration in [app.config.js](./app.config.js) set the confiuration variables diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/eas.json b/{{cookiecutter.project_slug}}/clients/mobile/react-native/eas.json index 552c464f8..cfa4b7397 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/eas.json +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/eas.json @@ -55,10 +55,10 @@ "channel": "production", "autoIncrement": true, "env": { - "BACKEND_SERVER_URL": "", + "BACKEND_SERVER_URL": "", "BUILD_ENV": "production", - "ROLLBAR_ACCESS_TOKEN": "", - "SENTRY_DSN": "" + "ROLLBAR_ACCESS_TOKEN": "", + "SENTRY_DSN": "" } } }, @@ -68,7 +68,7 @@ "appName": "", "appleId": "", "appleTeamId": "", - "ascAppId": "" } } } diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json index b8308073a..073e3be8a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json @@ -95,8 +95,8 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react-native": "^4.1.0", - "prettier": "^2.7.1", "tailwindcss": "^3.4.1", + "prettier": "^2.7.1", "typescript": "~5.3.3" }, "resolutions": { diff --git a/{{cookiecutter.project_slug}}/resources/app.config.vars.template.txt b/{{cookiecutter.project_slug}}/resources/app.config.vars.template.txt new file mode 100644 index 000000000..99bbfecbc --- /dev/null +++ b/{{cookiecutter.project_slug}}/resources/app.config.vars.template.txt @@ -0,0 +1,8 @@ +REPLACE_WITH_EXPO_APP_NAME= +REPLACE_WITH__EXPO_APP_SLUG= +REPLACE_WITH_EXPO_OWNER= +REPLACE_WITH_EXPO_APP_ID= +REPLACE_WITH_SENTRY_ORG= +REPLACE_WITH_IOS_BUNDLE_ID= +REPLACE_WITH_ANDROID_PACKAGE_ID= +REPLACE_WITH_EXPO_APP_ID= \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/resources/eas.vars.template.txt b/{{cookiecutter.project_slug}}/resources/eas.vars.template.txt new file mode 100644 index 000000000..f8cd02ede --- /dev/null +++ b/{{cookiecutter.project_slug}}/resources/eas.vars.template.txt @@ -0,0 +1,17 @@ +REPLACE_WITH_LOCAL_BACKEND_SERVER_URL= +REPLACE_WITH_STAGING_BACKEND_SERVER_URL= +REPLACE_WITH_PRODUCTION_BACKEND_SERVER_URL= +REPLACE_WITH_REVIEW_APP_BACKEND_SERVER_URL= +REPLACE_WITH_LOCAL_ROLLBAR_TOKEN= +REPLACE_WITH_STAGING_ROLLBAR_TOKEN= +REPLACE_WITH_PRODUCTION_ROLLBAR_TOKEN= +REPLACE_WITH_REVIEW_APP_ROLLBAR_TOKEN= +REPLACE_WITH_REVIEW_APP_SENTRY_DSN= +REPLACE_WITH_LOCAL_SENTRY_DSN= +REPLACE_WITH_STAGING_SENTRY_DSN= +REPLACE_WITH_PRODUCTION_SENTRY_DSN= +REPLACE_WITH_APPSTORE_CONNECT_APP_NAME= +REPLACE_WITH_APPLE_ID_EMAIL_SIGNING_APP= +REPLACE_WITH_APPLE_TEAM_ID= +REPLACE_WITH_APPLESTORE_CONNECT_ID= +REPLACE_WITH_REVIEW_APP_SENTRY_PROJECT_NAME= diff --git a/{{cookiecutter.project_slug}}/scripts/setup_mobile_config.sh b/{{cookiecutter.project_slug}}/scripts/setup_mobile_config.sh new file mode 100644 index 000000000..8af29dc03 --- /dev/null +++ b/{{cookiecutter.project_slug}}/scripts/setup_mobile_config.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# for debugging + +# set -e +# set -x + +# base_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +config_file=$1 + +defaults_file=$2 + + +declare -a config_vars +declare -a merged_arr + +reset_config_vars() { + unset config_vars +} +trap reset_config_vars EXIT + +while IFS= read -r line; do + config_vars+=("$line") +done < <(grep -o "REPLACE_WITH_[A-Z_]*" "$config_file") + + +for i in "${config_vars[@]}"; do + value=$(grep -o "$i=.*" "$defaults_file") || echo "" + if [[ -z $value ]]; then + echo "Skipping $i as it does not exist in $defaults_file" + continue + fi + + value=${value#*=} + value=$(printf '%q' "$value") + if [[ -n $value ]]; then + sed -i.bak "s#<$i>#$value#g" "$config_file" + else + echo "Skipping $i as it does not exist in $defaults_file" + fi +done \ No newline at end of file From fca7c4ddb38ad58d1fe48a8a70a662dd672f6129 Mon Sep 17 00:00:00 2001 From: Damian <52294448+lakardion@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:07:58 +0200 Subject: [PATCH 13/25] React Native - Add fonts to `nativewind` and remove custom `Text` component (#342) --- .../clients/mobile/react-native/App.tsx | 15 ++++---- .../react-native/src/components/Button.tsx | 10 ++--- .../react-native/src/components/errors.tsx | 5 +-- .../src/components/sheets/sample-sheet.tsx | 7 ++-- .../src/components/text-form-field.tsx | 7 +--- .../react-native/src/components/text.tsx | 12 ------ .../src/screens/ComponentsPreview.tsx | 37 ++++++++----------- .../react-native/src/screens/auth/login.tsx | 11 ++---- .../react-native/src/screens/auth/sign-up.tsx | 11 ++---- .../react-native/src/screens/dashboard.tsx | 7 +--- .../mobile/react-native/src/screens/main.tsx | 7 +--- .../mobile/react-native/src/utils/fonts.ts | 29 +-------------- .../mobile/react-native/tailwind.config.js | 12 ++++++ 13 files changed, 59 insertions(+), 111 deletions(-) delete mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx index 75b1950f1..75754d26e 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/App.tsx @@ -8,7 +8,6 @@ import { customFonts } from '@utils/fonts' import { queryClient } from '@utils/query-client' import { initSentry } from '@utils/sentry' import 'expo-dev-client' -import { loadAsync } from 'expo-font' import { setNotificationHandler } from 'expo-notifications' import * as SplashScreen from 'expo-splash-screen' import { StatusBar } from 'expo-status-bar' @@ -18,6 +17,7 @@ import { flushSync } from 'react-dom' import { LogBox, StyleSheet } from 'react-native' import { SheetProvider } from 'react-native-actions-sheet' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { useFonts } from 'expo-font' import './global.css' LogBox.ignoreLogs(['Require']) @@ -44,28 +44,29 @@ export default Sentry.wrap((): JSX.Element => { const [ready, setReady] = useState(false) const hasLocalStorageHydratedState = useAuth.use.hasHydrated() const setNavio = useSetAtom(navioAtom) + const [loaded, error] = useFonts(customFonts) const start = useCallback(async () => { - await loadAsync(customFonts) await hasLocalStorageHydratedState await SplashScreen.hideAsync() - setNavio(getNavio()) + setNavio(getNavio()) flushSync(() => { setReady(true) }) - }, [hasLocalStorageHydratedState]) + }, [hasLocalStorageHydratedState, setNavio]) useEffect(() => { + if (!loaded) return start() - }, [start]) + }, [loaded, start]) if (!ready) return <> return ( - - + + diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/Button.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/Button.tsx index a4a4f2ba1..942f7fb3a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/Button.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/Button.tsx @@ -1,15 +1,15 @@ +import colors from '@utils/colors' import React, { useMemo } from 'react' import { ActivityIndicator, Platform, StyleProp, + Text, + TouchableNativeFeedback, View, ViewStyle, - TouchableNativeFeedback, } from 'react-native' import { BounceableProps } from 'rn-bounceable' -import { Text } from './text' -import colors from '@utils/colors' import { BounceableWind } from './styled' export type BButtonVariant = 'primary' | 'primary-transparent' | 'secondary' @@ -97,11 +97,11 @@ export const BButton: React.FC = ({ ) : ( {leftIcon} - {label} + {label} {rightIcon} )} ) -} \ No newline at end of file +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/errors.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/errors.tsx index fcc590a42..448be339f 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/errors.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/errors.tsx @@ -1,10 +1,9 @@ import { IFormFieldError } from '@thinknimble/tn-forms' import { FC, Fragment, ReactNode } from 'react' -import { View } from 'react-native' -import { Text } from '@components/text' +import { Text, View } from 'react-native' export const ErrorMessage: FC<{ children: ReactNode }> = ({ children }) => { - return {children} + return {children} } /** diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx index 477cb2f36..c9ec15776 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/sheets/sample-sheet.tsx @@ -1,7 +1,6 @@ import { useRef } from 'react' -import { View } from 'react-native' +import { Text, View } from 'react-native' import { ActionSheetRef, SheetProps } from 'react-native-actions-sheet' -import { Text } from '../text' import { CustomActionSheet } from './custom-action-sheet' export const Sample = (props: SheetProps<'Sample'>) => { @@ -10,8 +9,8 @@ export const Sample = (props: SheetProps<'Sample'>) => { return ( - Coming soon - {props.payload?.input} + Coming soon + {props.payload?.input} ) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text-form-field.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text-form-field.tsx index 73cf001e8..5dd0f3403 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text-form-field.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text-form-field.tsx @@ -1,7 +1,6 @@ import Form, { IFormField } from '@thinknimble/tn-forms' import { useTnForm } from '@thinknimble/tn-forms-react' -import { TextInput, TextInputProps, View } from 'react-native' -import { Text } from '@components/text' +import { Text, TextInput, TextInputProps, View } from 'react-native' import twColors from 'tailwindcss/colors' import { ErrorsList } from '@components/errors' import { FormFieldsRecord } from '@thinknimble/tn-forms/lib/cjs/types/interfaces' @@ -14,9 +13,7 @@ export const TextFormField = , TForm extends Form() return ( - - {field.label} - + {field.label} & { variant?: FontWeightStyle; textClassName?: string } -> = ({ variant = 'regular', textClassName = '', ...props }) => { - const style = { - fontFamily: fontFamilyWeightMap[variant], - } - return -} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/ComponentsPreview.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/ComponentsPreview.tsx index 151ebfacd..66af068a8 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/ComponentsPreview.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/ComponentsPreview.tsx @@ -1,26 +1,21 @@ import React from 'react' -import {View } from 'react-native' +import { Text, View } from 'react-native' -import { Text } from '@components/text' import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { BButton } from '@components/Button' - -export function ComponentsPreview(){ - return ( - - - - Components Preview - - - - - - - - - - - ) -} \ No newline at end of file +export function ComponentsPreview() { + return ( + + + Components Preview + + + + + + + + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx index b3856972a..23c58a7f8 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/login.tsx @@ -1,11 +1,10 @@ import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { BounceableWind } from '@components/styled' -import { Text } from '@components/text' import { TextFormField } from '@components/text-form-field' import { LoginForm, LoginFormInputs, TLoginForm, userApi } from '@services/user' import { useAuth } from '@stores/auth' import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' -import { ScrollView, View } from 'react-native' +import { ScrollView, Text, View } from 'react-native' import { getNavio } from '../routes' const LoginInner = () => { @@ -35,9 +34,7 @@ const LoginInner = () => { return ( - - Log in - + Log in @@ -48,9 +45,7 @@ const LoginInner = () => { disabled={!form.isValid} > - - Log In - + Log In diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/sign-up.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/sign-up.tsx index 77090d754..8dc6b4728 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/sign-up.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/sign-up.tsx @@ -1,6 +1,5 @@ import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { BounceableWind } from '@components/styled' -import { Text } from '@components/text' import { TextFormField } from '@components/text-form-field' import { userApi } from '@services/user' import { AccountForm, TAccountForm } from '@services/user/forms' @@ -8,7 +7,7 @@ import { useAuth } from '@stores/auth' import { useMutation } from '@tanstack/react-query' import { MustMatchValidator } from '@thinknimble/tn-forms' import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' -import { ScrollView, View } from 'react-native' +import { ScrollView, Text, View } from 'react-native' import { getNavio } from '../routes' const InnerForm = () => { @@ -48,9 +47,7 @@ const InnerForm = () => { return ( - - Sign up - + Sign up @@ -60,9 +57,7 @@ const InnerForm = () => { - - Sign Up - + Sign Up diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx index 9fd3de3c1..40daa676f 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/dashboard.tsx @@ -1,12 +1,11 @@ import { BButton } from '@components/Button' import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { SHEET_NAMES } from '@components/sheets' -import { Text } from '@components/text' import { useLogout } from '@services/user' import { navioAtom } from '@stores/navigation' import { useAtomValue } from 'jotai' import React from 'react' -import { View } from 'react-native' +import { Text, View } from 'react-native' import { SheetManager } from 'react-native-actions-sheet' export const DashboardScreen = () => { @@ -24,9 +23,7 @@ export const DashboardScreen = () => { return ( - - Welcome to the Dashboard - + Welcome to the Dashboard diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/main.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/main.tsx index 645970945..82baa9f35 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/main.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/main.tsx @@ -1,6 +1,5 @@ -import { Dimensions, Image, StyleSheet, View } from 'react-native' +import { Dimensions, Image, StyleSheet, Text, View } from 'react-native' import logo from '@assets/tn-logo.png' -import { Text } from '@components/text' const { height } = Dimensions.get('screen') @@ -22,9 +21,7 @@ export const Main = () => { - - Welcome to my project - + Welcome to my project ) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts index b45f33378..784881052 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts @@ -1,4 +1,4 @@ -//TODO: change this to your family, we're using monserrat as base. You could also add more family weights! +//TODO: change this to your family, we're using montserrat as base. You could also add more family weights! const baseFamily = 'Montserrat' as const const fontFormat = 'ttf' @@ -14,30 +14,3 @@ export const customFonts = { [`${baseFamily}-MediumItalic` as const]: require(`../../assets/fonts/${baseFamily}-MediumItalic.${fontFormat}`), [`${baseFamily}-Regular` as const]: require(`../../assets/fonts/${baseFamily}-Regular.${fontFormat}`), } - -type FontFamily = keyof typeof customFonts - -export type FontWeightStyle = - | 'light' - | 'italic-light' - | 'regular' - | 'italic' - | 'medium' - | 'italic-medium' - | 'black' - | 'italic-black' - | 'bold' - | 'italic-bold' - -export const fontFamilyWeightMap: Record = { - light: `${baseFamily}-Light`, - 'italic-light': `${baseFamily}-LightItalic`, - regular: `${baseFamily}-Regular`, - italic: `${baseFamily}-Italic`, - medium: `${baseFamily}-Medium`, - 'italic-medium': `${baseFamily}-MediumItalic`, - black: `${baseFamily}-Black`, - 'italic-black': `${baseFamily}-BlackItalic`, - bold: `${baseFamily}-Bold`, - 'italic-bold': `${baseFamily}-BoldItalic`, -} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/tailwind.config.js b/{{cookiecutter.project_slug}}/clients/mobile/react-native/tailwind.config.js index 2955ac39e..4062765aa 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/tailwind.config.js +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/tailwind.config.js @@ -7,6 +7,18 @@ module.exports = { theme: { extend: { colors, + fontFamily: { + 'primary-black': `Montserrat-Black`, + 'primary-black-italic': `Montserrat-BlackItalic`, + 'primary-bold': `Montserrat-Bold`, + 'primary-bold-italic': `Montserrat-BoldItalic`, + 'primary-italic': `Montserrat-Italic`, + 'primary-light': `Montserrat-Light`, + 'primary-light-italic': `Montserrat-LightItalic`, + 'primary-medium': `Montserrat-Medium`, + 'primary-medium-italic': `Montserrat-MediumItalic`, + 'primary-regular': `Montserrat-Regular`, + }, }, }, plugins: [], From 96b41152d035ab5cc76012248302ec5dc1a21eda Mon Sep 17 00:00:00 2001 From: Damian <52294448+lakardion@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:20:41 +0200 Subject: [PATCH 14/25] Clients - Add default parameter serializer for arrays on axios. (#341) Co-authored-by: paribaker <58012003+paribaker@users.noreply.github.com> --- .../clients/mobile/react-native/package.json | 6 ++++-- .../mobile/react-native/src/services/axios-instance.ts | 6 +++++- .../clients/web/react/package.json | 2 ++ .../clients/web/react/src/services/axios-instance.ts | 4 ++++ {{cookiecutter.project_slug}}/clients/web/vue3/package.json | 4 +++- .../clients/web/vue3/src/services/AxiosClient.js | 4 ++++ 6 files changed, 22 insertions(+), 4 deletions(-) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json index 073e3be8a..10bac54fd 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/package.json @@ -59,9 +59,11 @@ "expo-web-browser": "~13.0.3", "jotai": "^2.7.1", "nativewind": "^4.0.36", + "qs": "^6.13.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.1", + "react-native-actions-sheet": "^0.9.6", "react-native-gesture-handler": "~2.16.1", "react-native-pager-view": "6.3.0", "react-native-reanimated": "~3.10.1", @@ -74,8 +76,7 @@ "rn-navio": "0.0.6", "rollbar-react-native": "^0.9.3", "zod": "3.21.4", - "zustand": "^4.3.3", - "react-native-actions-sheet": "^0.9.6" + "zustand": "^4.3.3" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -85,6 +86,7 @@ "@tanstack/eslint-plugin-query": "5.35.6", "@types/i18n-js": "^3.8.3", "@types/lodash": "^4.14.185", + "@types/qs": "^6.9.15", "@types/react": "~18.2.14", "@types/react-dom": "~18.2.25", "@typescript-eslint/eslint-plugin": "^5.37.0", diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/axios-instance.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/axios-instance.ts index f79ef7f39..f8ee12c88 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/axios-instance.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/axios-instance.ts @@ -1,5 +1,6 @@ -import axios, { AxiosError } from 'axios' import { useAuth } from '@stores/auth' +import axios, { AxiosError } from 'axios' +import qs from 'qs' import Config from '../../Config' const baseUrl = @@ -8,6 +9,9 @@ const baseUrl = : `${Config?.backendServerUrl}` export const axiosInstance = axios.create({ baseURL: `${baseUrl}/api`, + paramsSerializer: (params) => { + return qs.stringify(params, { arrayFormat: 'comma' }) + }, }) console.log('axiosInstance', `${baseUrl}/api`) axiosInstance.interceptors.request.use( diff --git a/{{cookiecutter.project_slug}}/clients/web/react/package.json b/{{cookiecutter.project_slug}}/clients/web/react/package.json index 1e9b4c404..60257c716 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/react/package.json @@ -21,6 +21,7 @@ "@thinknimble/tn-forms-react": "^1.0.3", "@thinknimble/tn-models": "^3.0.0", "axios": "^1.5.0", + "qs": "^6.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.10.1", @@ -33,6 +34,7 @@ "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.4.3", + "@types/qs": "^6.9.15", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/services/axios-instance.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/services/axios-instance.ts index 6f3e5d9bb..0aedc5b6c 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/services/axios-instance.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/services/axios-instance.ts @@ -1,4 +1,5 @@ import axios, { AxiosError } from 'axios' +import qs from 'qs' import { useAuth } from 'src/stores/auth' import { getCookie } from 'src/utils/get-cookie' @@ -9,6 +10,9 @@ export const axiosInstance = axios.create({ headers: { 'Content-Type': 'application/json', }, + paramsSerializer: (params) => { + return qs.stringify(params, { arrayFormat: 'comma' }) + }, }) axiosInstance.interceptors.request.use( diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json index a4596fe90..dbc9ca376 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json @@ -27,6 +27,7 @@ "@tanstack/vue-query": "^5.28.9", "@thinknimble/vue3-alert-alert": "^0.0.8", "axios": "1.5.0", + "qs": "^6.13.0", "js-cookie": "3.0.5", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", @@ -39,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "@vitejs/plugin-vue": "^5.0.0", + "@types/qs": "^6.9.15", "autoprefixer": "^10.4.15", "cypress": "^13.5.1", "eslint": "^8.49.0", @@ -54,4 +56,4 @@ "vitest": "^0.34.6", "vue-tsc": "^1.8.27" } -} +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js index 1fe4b3369..d8ad9395b 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js @@ -1,6 +1,7 @@ import axios from 'axios' import { useUserStore } from '@/stores/user' import CSRF from '@/services/csrf' +import qs from 'qs' /** * Get the axios API client. @@ -27,6 +28,9 @@ class ApiService { headers: { ...CSRF.getHeaders(), }, + paramsSerializer: (params) => { + return qs.stringify(params, { arrayFormat: 'comma' }) + }, }) ApiService.session.interceptors.request.use( async (config) => { From e1454684b741cf66ffa34ae4487a875571b982f5 Mon Sep 17 00:00:00 2001 From: Edwin Noriega <40014117+noriega2112@users.noreply.github.com> Date: Tue, 27 Aug 2024 07:48:48 -0700 Subject: [PATCH 15/25] [React - Vue] - Migrate E2E Tests from Cypress to Playwright (#335) --- .github/workflows/cypress.yml | 86 ------------------- .github/workflows/e2e.yml | 59 +++++++++++++ hooks/post_gen_project.py | 5 +- .../.github/pull_request_template.md | 2 +- .../.github/workflows/cypress.yml | 43 ---------- .../.github/workflows/playwright.yml | 24 ++++++ {{cookiecutter.project_slug}}/.gitignore | 5 -- {{cookiecutter.project_slug}}/README.md | 16 ++-- {{cookiecutter.project_slug}}/app.json | 10 +-- .../clients/web/react/.gitignore | 7 ++ .../clients/web/react/README.md | 15 +++- .../clients/web/react/cypress.config.ts | 11 --- .../web/react/cypress.example.env.json | 4 - .../clients/web/react/package.json | 6 +- .../clients/web/react/playwright.config.ts | 55 ++++++++++++ .../clients/web/react/src/pages/log-in.tsx | 8 +- .../clients/web/react/src/pages/sign-up.tsx | 15 +++- .../clients/web/react/tests/e2e/.eslintrc.js | 10 --- .../web/react/tests/e2e/plugins/index.js | 23 ----- .../web/react/tests/e2e/specs/home.spec.ts | 14 +++ .../web/react/tests/e2e/specs/login.spec.ts | 14 +++ .../web/react/tests/e2e/specs/sign-up.spec.ts | 23 +++++ .../react/tests/e2e/specs/test-login.cy.ts | 14 --- .../web/react/tests/e2e/support/commands.js | 25 ------ .../web/react/tests/e2e/support/e2e.js | 20 ----- .../clients/web/react/tests/tsconfig.json | 6 -- .../clients/web/react/tsconfig.json | 2 +- .../clients/web/vue3/.gitignore | 4 + .../clients/web/vue3/README.md | 11 ++- .../clients/web/vue3/cypress.config.js | 11 --- .../clients/web/vue3/cypress.env.json.example | 4 - .../clients/web/vue3/package.json | 6 +- .../clients/web/vue3/playwright.config.ts | 53 ++++++++++++ .../web/vue3/src/components/HelloWorld.vue | 8 -- .../clients/web/vue3/src/views/Login.vue | 4 +- .../clients/web/vue3/src/views/Signup.vue | 8 +- .../clients/web/vue3/tests/e2e/.eslintrc.js | 10 --- .../web/vue3/tests/e2e/plugins/index.js | 23 ----- .../web/vue3/tests/e2e/specs/home.spec.ts | 14 +++ .../web/vue3/tests/e2e/specs/login.spec.ts | 12 +++ .../web/vue3/tests/e2e/specs/sign-up.spec.ts | 24 ++++++ .../web/vue3/tests/e2e/specs/test-login.cy.ts | 15 ---- .../tests/e2e/specs/test-password-reset.cy.ts | 15 ---- .../web/vue3/tests/e2e/support/commands.js | 25 ------ .../clients/web/vue3/tests/e2e/support/e2e.js | 20 ----- .../clients/web/vue3/tsconfig.json | 1 - .../management/commands/create_test_data.py | 4 +- 47 files changed, 366 insertions(+), 428 deletions(-) delete mode 100644 .github/workflows/cypress.yml create mode 100644 .github/workflows/e2e.yml delete mode 100644 {{cookiecutter.project_slug}}/.github/workflows/cypress.yml create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/playwright.yml delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts create mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example create mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/.eslintrc.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/plugins/index.js create mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/home.spec.ts create mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/login.spec.ts create mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/sign-up.spec.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-login.cy.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-password-reset.cy.ts delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/commands.js delete mode 100644 {{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/e2e.js diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml deleted file mode 100644 index c6103eafb..000000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Cypress Tests -on: [deployment_status] - -jobs: - Setup: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: pipx install pipenv - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - # Can't use cache because of https://github.com/actions/cache/issues/319 - # cache: 'pipenv' - - name: Install bootstrapper dependencies - run: pipenv install --dev --deploy - - run: | - config_file=$(./scripts/vue_or_react.sh) - pipenv run cookiecutter . --config-file $config_file --no-input -f - cat $config_file - - uses: actions/upload-artifact@v4 - with: - name: my_project - path: my_project/ - retention-days: 1 - Chrome: - needs: Setup - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: my_project - path: my_project/ - - uses: actions/setup-node@v4 - with: - node-version: 16 - - name: Install frontend dependencies - env: - NPM_CONFIG_PRODUCTION: false - working-directory: ./my_project/client - run: npm install - - name: Run against ${{ github.event.deployment_status.environment_url }} - uses: cypress-io/github-action@v6 - with: - working-directory: my_project/client - browser: chrome - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} - CYPRESS_baseUrl: ${{ github.event.deployment_status.environment_url }} - Firefox: - needs: Setup - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: my_project - path: my_project/ - - uses: actions/setup-node@v4 - with: - node-version: 16 - - name: Install frontend dependencies - env: - NPM_CONFIG_PRODUCTION: false - working-directory: ./my_project/client - run: npm install - - name: Run against ${{ github.event.deployment_status.environment_url }} - uses: cypress-io/github-action@v6 - with: - working-directory: my_project/client - browser: firefox - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} - CYPRESS_baseUrl: ${{ github.event.deployment_status.environment_url }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..07d5a363d --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,59 @@ +name: E2E Tests +on: [deployment_status] + +jobs: + Setup: + if: github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx install pipenv + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + # Can't use cache because of https://github.com/actions/cache/issues/319 + # cache: 'pipenv' + - name: Install bootstrapper dependencies + run: pipenv install --dev --deploy + - run: | + config_file=$(./scripts/vue_or_react.sh) + pipenv run cookiecutter . --config-file $config_file --no-input -f + cat $config_file + - uses: actions/upload-artifact@v4 + with: + name: my_project + path: my_project/ + retention-days: 1 + Playwright: + needs: Setup + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: my_project + path: my_project/ + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: 📦 Install frontend dependencies + working-directory: ./my_project/client + run: npm install + - name: 🎭 Install Playwright + working-directory: ./my_project/client + run: npx playwright install --with-deps + - name: Run Playwright tests against ${{ github.event.deployment_status.environment_url }} + working-directory: ./my_project/client + run: npx playwright test --reporter=html + env: + NPM_CONFIG_PRODUCTION: false + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.environment_url }} + CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: my_project/client/playwright-report/ + retention-days: 30 diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index ab6d66362..125d4242a 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -118,15 +118,12 @@ def set_keys_in_envs(django_secret, postgres_secret): cookie_cutter_settings_path = join("app.json") postgres_init_file = join("scripts/init-db.sh") set_flag(env_file_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) + set_flag(env_file_path, "!!!PLAYWRIGHT_SECRET_KEY!!!", django_secret) set_flag(pull_request_template_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) set_flag(cookie_cutter_settings_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) set_flag(env_file_path, "!!!POSTGRES_PASSWORD!!!", postgres_secret) set_flag(postgres_init_file, "!!!POSTGRES_PASSWORD!!!", postgres_secret) copy2(env_file_path, join(".env")) - cypress_example_file_dir = join(web_clients_path, "react") - cypress_example_file = join(cypress_example_file_dir, "cypress.example.env.json") - set_flag(cypress_example_file, "!!!POSTGRES_PASSWORD!!!", postgres_secret) - copy2(cypress_example_file, join(cypress_example_file_dir, "cypress.env.json")) def get_secrets(): diff --git a/{{cookiecutter.project_slug}}/.github/pull_request_template.md b/{{cookiecutter.project_slug}}/.github/pull_request_template.md index 1b49d4c79..53606cdae 100644 --- a/{{cookiecutter.project_slug}}/.github/pull_request_template.md +++ b/{{cookiecutter.project_slug}}/.github/pull_request_template.md @@ -22,4 +22,4 @@ Add user steps to achieve desired functionality for this feature. | user | password | has admin | notes | | --- | --- | --- | --- | | `admin@thinknimble.com` | !!!DJANGO_SECRET_KEY!!! | :white_check_mark: | | -| `cypress@example.com` | !!!DJANGO_SECRET_KEY!!! | :x: | Only use for automated E2E testing | +| `playwright@thinknimble.com` | !!!DJANGO_SECRET_KEY!!! | :x: | Only use for automated E2E testing | diff --git a/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml b/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml deleted file mode 100644 index ec927b2bb..000000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Cypress Tests -on: [deployment_status] - -jobs: - Chrome: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/setup-node@v4 - with: - node-version: 16 - - uses: actions/checkout@v4 - - name: Run against {{ "${{ github.event.deployment_status.environment_url }}" }} - uses: cypress-io/github-action@v6 - with: - working-directory: client - browser: chrome - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} - CYPRESS_baseUrl: {{ "${{ github.event.deployment_status.environment_url }}" }} - Firefox: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - name: Run against {{ "${{ github.event.deployment_status.environment_url }}" }} - uses: cypress-io/github-action@v6 - with: - working-directory: client - browser: firefox - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} - CYPRESS_baseUrl: {{ "${{ github.event.deployment_status.environment_url }}" }} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml b/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml new file mode 100644 index 000000000..d5d94976a --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml @@ -0,0 +1,24 @@ +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm --prefix ./client i + - name: Install Playwright + run: npx --prefix ./client playwright install --with-deps + - name: Run Playwright tests + run: npx --prefix ./client playwright test --reporter=html + env: + PLAYWRIGHT_TEST_BASE_URL: + {{ "${{ github.event.deployment_status.environment_url }}" }} + CYPRESS_TEST_USER_PASS: + {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index 614bd54fc..367110f06 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -31,11 +31,6 @@ wheels/ .installed.cfg *.egg -# Ignore Cypress environment variables & media -client/cypress.env.json -client/tests/e2e/screenshots/* -client/tests/e2e/videos/* - # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 275dbc7fc..22bf640b1 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -5,34 +5,38 @@ ## Setup ### Docker + If this is your first time... + 1. [Install Docker](https://www.docker.com/) 1. Run `pipenv lock` to generate a Pipfile.lock 1. Run `cd client && npm install` so you have node_modules available outside of Docker 1. Back in the root directory, run `make build` 1. `make run` to start the app 1. If the DB is new, run `make create-test-data` - 1. SuperUser `admin@thinknimble.com` with credentials from your `.env` - 1. User `cypress@thinknimble.com` with credentials from your `.env` is used by the Cypress - tests + 1. SuperUser `admin@thinknimble.com` with credentials from your `.env` + 1. User `playwright@thinknimble.com` with credentials from your `.env` is used by the Playwright + tests 1. View other available scripts/commands with `make commands` 1. `localhost:8080` to view the app. 1. `localhost:8000/staff/` to log into the Django admin 1. `localhost:8000/api/docs/` to view backend API endpoints available for frontend development - ### Backend + If not using Docker... See the [backend README](server/README.md) ### Frontend + If not using Docker... See the [frontend README](client/README.md) - ## Testing & Linting Locally + 1. `pipenv install --dev` 1. `pipenv run pytest server` 1. `pipenv run black server` 1. `pipenv run isort server --diff` (shows you what isort is expecting) -1. `npm run cypress` +1. `npx playwright test` +1. `npx playwright codegen localhost:8080` (generate your tests through manual testing) diff --git a/{{cookiecutter.project_slug}}/app.json b/{{cookiecutter.project_slug}}/app.json index 9a8281135..85e1b66b8 100644 --- a/{{cookiecutter.project_slug}}/app.json +++ b/{{cookiecutter.project_slug}}/app.json @@ -29,16 +29,10 @@ "generator": "secret" } }, - "addons": [ - "heroku-postgresql:standard-0", - "papertrail:choklad" - ], + "addons": ["heroku-postgresql:standard-0", "papertrail:choklad"], "environments": { "review": { - "addons": [ - "heroku-postgresql:essential-0", - "papertrail:choklad" - ] + "addons": ["heroku-postgresql:essential-0", "papertrail:choklad"] } }, "buildpacks": [ diff --git a/{{cookiecutter.project_slug}}/clients/web/react/.gitignore b/{{cookiecutter.project_slug}}/clients/web/react/.gitignore index 4d29575de..2b8d731ad 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/.gitignore +++ b/{{cookiecutter.project_slug}}/clients/web/react/.gitignore @@ -21,3 +21,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Playwright files +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/README.md b/{{cookiecutter.project_slug}}/clients/web/react/README.md index 1a82fd924..ec9d7fd75 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/README.md +++ b/{{cookiecutter.project_slug}}/clients/web/react/README.md @@ -17,7 +17,7 @@ This app includes basic configurations for developers to have a starting point o - TN Forms - Vitest - React testing library -- Cypress +- Playwright ## Getting started @@ -49,6 +49,7 @@ npm i First, create .env.local at the top-level of the client directory, and copy the contents of .env.local.example into it. Update the value of VITE_DEV_BACKEND_URL to point to your desired backend. Then run the project with: + ``` npm run serve ``` @@ -73,10 +74,16 @@ If you want to watch a single test you can specify its path as an argument to: npm run test:single path/to/test/file ``` -### Run e2e tests with Cypress +### Run e2e tests with Playwright ``` -npm run cypress +npm run test:e2e ``` -Will open cypress wizard. Make sure you run your app locally with `npm run start` and them choose the test you want to run from the wizard. +Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. + +To open last HTML report run: + +``` +npx playwright show-report +``` diff --git a/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts b/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts deleted file mode 100644 index 9d63d2ecb..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'cypress' -import pluginsFile from './tests/e2e/plugins' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:8080', - setupNodeEvents: pluginsFile, - supportFile: 'tests/e2e/support/e2e.js', - specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}', - }, -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json b/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json deleted file mode 100644 index 939ade11d..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_USER_PASS": "!!!POSTGRES_PASSWORD!!!", - "TEST_USER_EMAIL": "cypress@example.com" -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/package.json b/{{cookiecutter.project_slug}}/clients/web/react/package.json index 60257c716..532b4fad6 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/react/package.json @@ -5,8 +5,8 @@ "scripts": { "serve": "vite --host 0.0.0.0", "build": "vite build --base=/static/", - "cypress": "source ../.env && cypress open", "test": "vitest run", + "test:e2e": "npx playwright test --reporter=html", "test:dev": "vitest", "test:watch": "vitest run", "test:single": "vitest $0", @@ -30,10 +30,12 @@ "zustand": "^4.4.0" }, "devDependencies": { + "@playwright/test": "^1.46.0", "@tanstack/eslint-plugin-query": "5.35.6", "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.4.3", + "@types/node": "^22.1.0", "@types/qs": "^6.9.15", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", @@ -42,7 +44,7 @@ "@typescript-eslint/parser": "^6.19.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.15", - "cypress": "^13.5.1", + "dotenv": "^16.4.5", "eslint": "^8.48.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts b/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts new file mode 100644 index 000000000..39186a038 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts @@ -0,0 +1,55 @@ +import { defineConfig, devices } from '@playwright/test' +import dotenv from 'dotenv' +import path from 'path' + +dotenv.config({ path: path.resolve(__dirname, '.env') }) + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e/specs', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx index dd77bd035..3e8d4f53e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx @@ -51,7 +51,7 @@ function LogInInner() { return (
-
{ e.preventDefault() }} @@ -62,7 +62,7 @@ function LogInInner() { placeholder="Enter email..." onChange={(e) => createFormFieldChangeHandler(form.email)(e.target.value)} value={form.email.value ?? ''} - data-cy="email" + data-testid="email" id="id" label="Email address" /> @@ -84,7 +84,7 @@ function LogInInner() { createFormFieldChangeHandler(form.password)(e.target.value) }} value={form.password.value ?? ''} - data-cy="password" + data-testid="password" id="password" /> @@ -96,7 +96,7 @@ function LogInInner() { {errorMessage} - {errors.length + {errors.length ? errors.map((e, idx) => {e}) : null} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js deleted file mode 100644 index a3e436bc3..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - plugins: ['cypress'], - env: { - mocha: true, - 'cypress/globals': true, - }, - rules: { - strict: 'off', - }, -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js deleted file mode 100644 index b150c40be..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable arrow-body-style */ -// https://docs.cypress.io/guides/guides/plugins-guide.html - -// if you need a custom webpack configuration you can uncomment the following import -// and then use the `file:preprocessor` event -// as explained in the cypress docs -// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples - -// /* eslint-disable import/no-extraneous-dependencies, global-require */ -// const webpack = require('@cypress/webpack-preprocessor') - -export default (on, config) => { - // on('file:preprocessor', webpack({ - // webpackOptions: require('@vue/cli-service/webpack.config'), - // watchOptions: {} - // })) - - return Object.assign({}, config, { - fixturesFolder: 'tests/e2e/fixtures', - screenshotsFolder: 'tests/e2e/screenshots', - videosFolder: 'tests/e2e/videos', - }) -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts new file mode 100644 index 000000000..9b551d4f7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Has Welcome text', async ({ page }) => { + await expect(page.getByText('Welcome')).toBeVisible() +}) + +test('Login and signup buttons are visible', async ({ page }) => { + await expect(page.getByText('Login')).toBeVisible() + await expect(page.getByText('Signup')).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts new file mode 100644 index 000000000..af34bae87 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts @@ -0,0 +1,14 @@ +// @ts-check +import { test, expect } from '@playwright/test' +import dotenv from 'dotenv' + +test('Login workflow', async ({ page }) => { + expect(process.env.CYPRESS_TEST_USER_PASS).toBeTruthy() + + await page.goto('/log-in') + await page.getByTestId('email').fill('playwright@thinknimble.com') + await page.getByTestId('password').fill(process.env.CYPRESS_TEST_USER_PASS ?? '') + await page.getByTestId('submit').click() + + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts new file mode 100644 index 000000000..8c6316db7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test' + +const PASSWORD = 'PASSWORD' + +function generateUniqueEmail() { + const timestamp = Date.now().toString(); + return `playwright-${timestamp}@thinknimble.com`; +} + +test('Login workflow', async ({ page }) => { + const uniqueEmail = generateUniqueEmail() + + await page.goto('/sign-up') + await page.getByTestId('first-name').fill('playwright') + await page.getByTestId('last-name').fill('e2e test') + await page + .getByTestId('email') + .fill(uniqueEmail) + await page.getByTestId('password').fill(PASSWORD) + await page.getByTestId('confirm-password').fill(PASSWORD) + await page.getByTestId('submit').click() + await expect(page.getByText('Welcome to')).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts deleted file mode 100644 index dd9e8fcee..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts +++ /dev/null @@ -1,14 +0,0 @@ -describe('Tests login workflow', () => { - it('Home page has link to login', () => { - cy.visit('/') - cy.get('[data-cy=login]').click() - cy.url().should('include', '/log-in') - }), - it('Home page auto redirects to login', () => { - cy.visit('/log-in') - cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) - cy.get('[data-cy=password]').type(Cypress.env('TEST_USER_PASS')) - cy.get('[data-cy=submit]').click() - cy.url().should('include', '/dashboard') - }) -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js deleted file mode 100644 index c1f5a772e..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js deleted file mode 100644 index d68db96df..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json b/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json deleted file mode 100644 index aa9df84f7..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "isolatedModules": false, - "types": ["cypress", "node"] - } -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json b/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json index 6b33553df..76a139e18 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json +++ b/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json @@ -20,7 +20,7 @@ "noEmit": true, "jsx": "react-jsx", "baseUrl": ".", - "types": ["cypress","node"] + "types": ["node"] }, "include": [ "src", diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore b/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore index 4ec828127..291b779aa 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore @@ -24,3 +24,7 @@ pnpm-debug.log* *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/README.md b/{{cookiecutter.project_slug}}/clients/web/vue3/README.md index 6516bd004..4ce3fba4d 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/README.md +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/README.md @@ -20,7 +20,7 @@ Swap out the logo files in these locations: ## Initial Setup for non-Docker local First, create `.env.local` at the top-level of the **client** directory, and copy the contents of `.env.local.example` into it. -Un-comment the value of `VUE_APP_DEV_SERVER_BACKEND` that is appropriate for your situation. +Un-comment the value of `VITE_DEV_BACKEND_URL` that is appropriate for your situation. ``` npm install @@ -44,12 +44,19 @@ npm run build npm run test:unit ``` -### Run your end-to-end tests +### Run e2e tests with Playwright ``` npm run test:e2e ``` +Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. + +To open last HTML report run: + +``` +npx playwright show-report + ### Lints and fixes files ``` diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js b/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js deleted file mode 100644 index 9d63d2ecb..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'cypress' -import pluginsFile from './tests/e2e/plugins' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:8080', - setupNodeEvents: pluginsFile, - supportFile: 'tests/e2e/support/e2e.js', - specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}', - }, -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example b/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example deleted file mode 100644 index 3422f0062..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_USER_EMAIL": "cypress@example.com", - "TEST_USER_PASS": "" -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json index dbc9ca376..654ef18fc 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json @@ -9,8 +9,8 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run", + "test:e2e": "npx playwright test --reporter=html", "test:dev": "vitest", - "cypress:dev": "cypress open", "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "format:write": "prettier --write ./src", "format:check": "prettier --check ./src", @@ -36,13 +36,15 @@ "zod": "3.21.4" }, "devDependencies": { + "@playwright/test": "^1.46.0", "@testing-library/vue": "^8.0.0", + "@types/node": "^22.2.0", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "@vitejs/plugin-vue": "^5.0.0", "@types/qs": "^6.9.15", "autoprefixer": "^10.4.15", - "cypress": "^13.5.1", + "dotenv": "^16.4.5", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts new file mode 100644 index 000000000..5595fef3e --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from '@playwright/test' +import dotenv from 'dotenv' +dotenv.config({ path: '.env' }) + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e/specs', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue index ae6789498..dec049020 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue @@ -48,14 +48,6 @@ >unit-mocha -
  • - e2e-cypress -
  • Essential Links