From 42188411d5a3678094aff6d93e777460ac16234a Mon Sep 17 00:00:00 2001 From: jho406 Date: Fri, 6 Oct 2023 20:20:44 -0400 Subject: [PATCH] Use redux toolkit (#25) This transitions superglue to use redux toolkit. Notable changes here include: 1. The `flash` is no longer managed by superglue. Instead, its a slice that listens to dispatched superglue events. The slice is generated automatically when you first install superglue. This is backward breaking, to add the flash back you would have to add a flash slice: ```js import { createSlice, createAction } from '@reduxjs/toolkit' import { SAVE_RESPONSE, BEFORE_VISIT} from '@thoughtbot/superglue' const saveResponse = createAction(SAVE_RESPONSE) const beforeVisit = createAction(BEFORE_VISIT) export const flashSlice = createSlice({ name: 'flash', initialState: {}, extraReducers: (builder) => { builder.addCase(beforeVisit, (state, action) => { return {} }) builder.addCase(saveResponse, (state, action) => { const { page } = action.payload; return { ...state, ...page.slices.flash } }) } }) ``` and select the state via ```js const flash = useSelector((state) => state.flash) ``` 2. Similarly, we created a `slices/pages` to more easily allow for developer customizations. This replaces the `applicationPagesReducer` in the previous `reducer.js` file. 3. We refactor the creation of the redux store into its own `store.js` file to make it more obvious for developers to customize. --- superglue/lib/action_creators/requests.js | 19 ++------ superglue/lib/actions.js | 1 - superglue/lib/index.js | 4 ++ superglue/lib/reducers/index.js | 22 ---------- superglue/lib/utils/react.js | 5 +-- superglue/spec/features/navigation.spec.js | 14 ------ superglue/spec/fixtures.js | 2 - superglue/spec/lib/action_creators.spec.js | 17 +++---- superglue/spec/lib/reducers.spec.js | 18 -------- superglue/spec/lib/utils/react.spec.js | 2 - .../rails/templates/edit.json.props | 6 +-- .../generators/rails/templates/new.json.props | 6 +-- .../generators/rails/templates/web/edit.js | 7 +-- .../generators/rails/templates/web/index.js | 8 ++-- .../lib/generators/rails/templates/web/new.js | 7 +-- .../generators/rails/templates/web/show.js | 6 +-- .../install/templates/web/action_creators.js | 14 ------ .../lib/install/templates/web/actions.js | 9 ++-- .../lib/install/templates/web/application.js | 39 ++++------------ .../templates/web/application.json.props | 5 ++- .../lib/install/templates/web/flash.js | 19 ++++++++ .../lib/install/templates/web/pages.js | 15 +++++++ .../lib/install/templates/web/reducer.js | 44 ------------------- .../lib/install/templates/web/store.js | 32 ++++++++++++++ superglue_rails/lib/install/web.rb | 15 ++++--- superglue_rails/lib/tasks/install.rake | 12 +---- .../superglue_installation_acceptance.rb | 5 ++- 27 files changed, 131 insertions(+), 222 deletions(-) delete mode 100644 superglue_rails/lib/install/templates/web/action_creators.js create mode 100644 superglue_rails/lib/install/templates/web/flash.js create mode 100644 superglue_rails/lib/install/templates/web/pages.js delete mode 100644 superglue_rails/lib/install/templates/web/reducer.js create mode 100644 superglue_rails/lib/install/templates/web/store.js diff --git a/superglue/lib/action_creators/requests.js b/superglue/lib/action_creators/requests.js index b459d430..e9143a3b 100644 --- a/superglue/lib/action_creators/requests.js +++ b/superglue/lib/action_creators/requests.js @@ -8,7 +8,6 @@ import { removePropsAt, } from '../utils' import { - CLEAR_FLASH, BEFORE_FETCH, BEFORE_VISIT, BEFORE_REMOTE, @@ -66,15 +65,6 @@ function buildMeta(pageKey, page, state) { } } -export function clearFlash({ pageKey }) { - return { - type: CLEAR_FLASH, - payload: { - pageKey, - }, - } -} - export function remote( path, { @@ -95,8 +85,9 @@ export function remote( body, }) pageKey = pageKey || getState().superglue.currentPageKey + const currentPageKey = getState().superglue.currentPageKey - dispatch(beforeRemote({ fetchArgs })) + dispatch(beforeRemote({ currentPageKey, fetchArgs })) dispatch(beforeFetch({ fetchArgs })) return fetch(...fetchArgs) @@ -142,9 +133,6 @@ export function visit( let pageKey = urlToPageKey(path) return (dispatch, getState) => { - const currentKey = getState().superglue.currentPageKey - dispatch(clearFlash({ pageKey: currentKey })) - placeholderKey = placeholderKey && urlToPageKey(placeholderKey) const hasPlaceholder = !!getState().pages[placeholderKey] @@ -175,7 +163,8 @@ export function visit( signal, }) - dispatch(beforeVisit({ fetchArgs })) + const currentPageKey = getState().superglue.currentPageKey + dispatch(beforeVisit({ currentPageKey, fetchArgs })) dispatch(beforeFetch({ fetchArgs })) lastVisitController.abort() diff --git a/superglue/lib/actions.js b/superglue/lib/actions.js index fa4b22b5..525f1cd1 100644 --- a/superglue/lib/actions.js +++ b/superglue/lib/actions.js @@ -4,7 +4,6 @@ export const BEFORE_REMOTE = '@@superglue/BEFORE_REMOTE' export const SAVE_RESPONSE = '@@superglue/SAVE_RESPONSE' export const HANDLE_GRAFT = '@@superglue/HANDLE_GRAFT' -export const CLEAR_FLASH = '@@superglue/CLEAR_FLASH' export const SUPERGLUE_ERROR = '@@superglue/ERROR' export const GRAFTING_ERROR = '@@superglue/GRAFTING_ERROR' diff --git a/superglue/lib/index.js b/superglue/lib/index.js index bfca6fd0..bae238f7 100644 --- a/superglue/lib/index.js +++ b/superglue/lib/index.js @@ -27,6 +27,7 @@ export { REMOVE_PAGE, GRAFTING_ERROR, GRAFTING_SUCCESS, + HISTORY_CHANGE, } from './actions' export { @@ -46,8 +47,11 @@ export { getIn } from './utils/immutability' export { urlToPageKey } function pageToInitialState(key, page) { + const slices = page.slices || {} + return { pages: { [key]: page }, + ...slices, } } diff --git a/superglue/lib/reducers/index.js b/superglue/lib/reducers/index.js index 860a4cc6..a2a518c5 100644 --- a/superglue/lib/reducers/index.js +++ b/superglue/lib/reducers/index.js @@ -1,7 +1,6 @@ import { setIn, getIn, urlToPageKey } from '../utils' import { REMOVE_PAGE, - CLEAR_FLASH, SAVE_RESPONSE, HANDLE_GRAFT, HISTORY_CHANGE, @@ -96,15 +95,6 @@ export function appendReceivedFragmentsOntoPage( return nextState } -export function addFlash(state, pageKey, receivedFlash) { - const nextState = { ...state } - const nextPage = { ...state[pageKey] } - nextPage.flash = { ...nextPage.flash, ...receivedFlash } - nextState[pageKey] = nextPage - - return nextState -} - export function graftNodeOntoPage(state, pageKey, node, pathToNode) { if (!node) { console.warn( @@ -132,7 +122,6 @@ export function handleGraft(state, pageKey, page) { data: receivedNode, path: pathToNode, fragments: receivedFragments = [], - flash: receivedFlash, } = page return [ @@ -144,7 +133,6 @@ export function handleGraft(state, pageKey, page) { pageKey, receivedFragments ), - (nextState) => addFlash(nextState, pageKey, receivedFlash), ].reduce((memo, fn) => fn(memo), state) } @@ -185,16 +173,6 @@ export function pageReducer(state = {}, action) { return nextState } - case CLEAR_FLASH: { - const { pageKey } = action.payload - const nextState = { ...state } - const nextPage = { ...state[pageKey] } - - nextPage.flash = {} - nextState[pageKey] = nextPage - - return nextState - } case COPY_PAGE: { const nextState = { ...state } const { from, to } = action.payload diff --git a/superglue/lib/utils/react.js b/superglue/lib/utils/react.js index f747de8b..7c061cd3 100644 --- a/superglue/lib/utils/react.js +++ b/superglue/lib/utils/react.js @@ -14,12 +14,11 @@ export function mapStateToProps( let params = ownProps const csrfToken = state.superglue.csrfToken pageKey = urlToPageKey(pageKey) - const { data, flash, fragments } = state.pages[pageKey] || { + const { data, fragments } = state.pages[pageKey] || { data: {}, - flash: {}, fragments: [], } - return { ...data, ...params, pageKey, csrfToken, flash, fragments } + return { ...data, ...params, pageKey, csrfToken, fragments } } export const mapDispatchToProps = { diff --git a/superglue/spec/features/navigation.spec.js b/superglue/spec/features/navigation.spec.js index 7d0ca846..1f87c358 100644 --- a/superglue/spec/features/navigation.spec.js +++ b/superglue/spec/features/navigation.spec.js @@ -71,7 +71,6 @@ describe('start', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -106,7 +105,6 @@ describe('start', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -137,7 +135,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -165,7 +162,6 @@ describe('navigation', () => { const pageState = { data: { heading: 'Some heading 2' }, - flash: {}, csrfToken: 'token', assets: ['application-123.js', 'application-123.js'], componentIdentifier: 'about', @@ -194,7 +190,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -230,7 +225,6 @@ describe('navigation', () => { const pageState = { data: { heading: 'Some heading 2' }, - flash: {}, csrfToken: 'token', assets: ['application-123.js', 'application-123.js'], componentIdentifier: 'about', @@ -272,7 +266,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -315,7 +308,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -369,7 +361,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -419,7 +410,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -496,7 +486,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -540,7 +529,6 @@ describe('navigation', () => { data: { heading: 'this is page 1', }, - flash: {}, componentIdentifier: 'home', assets: ['123.js', '123.css'], fragments: [], @@ -561,7 +549,6 @@ describe('navigation', () => { const pageState = { data: { heading: 'Some heading 2' }, - flash: {}, csrfToken: 'token', assets: ['application-123.js', 'application-123.js'], componentIdentifier: 'about', @@ -591,7 +578,6 @@ describe('navigation', () => { heading: 'this is page 1', address: undefined, }, - flash: {}, fragments: [], componentIdentifier: 'home', } diff --git a/superglue/spec/fixtures.js b/superglue/spec/fixtures.js index 1911b76a..5832d471 100644 --- a/superglue/spec/fixtures.js +++ b/superglue/spec/fixtures.js @@ -5,7 +5,6 @@ export const visitSuccess = () => { csrfToken: 'token', assets: ['application-123.js', 'application-123.js'], componentIdentifier: 'about', - flash: {}, fragments: [], }), headers: { @@ -23,7 +22,6 @@ export const graftSuccessWithNewZip = () => { path: 'data.address', csrfToken: 'token', assets: ['application-new123.js', 'application-new123.js'], - flash: {}, fragments: [], }), headers: { diff --git a/superglue/spec/lib/action_creators.spec.js b/superglue/spec/lib/action_creators.spec.js index 7f21edd3..6cac93d9 100644 --- a/superglue/spec/lib/action_creators.spec.js +++ b/superglue/spec/lib/action_creators.spec.js @@ -272,6 +272,7 @@ describe('action creators', () => { { type: '@@superglue/BEFORE_REMOTE', payload: { + currentPageKey: "/bar", fetchArgs: [ '/foo?props_at=data.body&__=0', { @@ -327,6 +328,7 @@ describe('action creators', () => { { type: '@@superglue/BEFORE_REMOTE', payload: { + currentPageKey: "/bar", fetchArgs: [ '/foo?props_at=data.body.aside.top&__=0', { @@ -780,7 +782,7 @@ describe('action creators', () => { const expectedActions = [ { type: '@@superglue/BEFORE_REMOTE', - payload: { fetchArgs: ['/foo?__=0', expect.any(Object)] }, + payload: { currentPageKey: "/bar", fetchArgs: ['/foo?__=0', expect.any(Object)] }, }, { type: '@@superglue/BEFORE_FETCH', @@ -856,7 +858,7 @@ describe('action creators', () => { const expectedActions = [ { type: '@@superglue/BEFORE_REMOTE', - payload: { fetchArgs: ['/foo?__=0', expect.any(Object)] }, + payload: { currentPageKey: '/bar', fetchArgs: ['/foo?__=0', expect.any(Object)] }, }, { type: '@@superglue/BEFORE_FETCH', @@ -987,7 +989,7 @@ describe('action creators', () => { const expectedActions = [ { type: '@@superglue/BEFORE_REMOTE', - payload: { fetchArgs: ['/foo?__=0', expect.any(Object)] }, + payload: { currentPageKey: "/bar", fetchArgs: ['/foo?__=0', expect.any(Object)] }, }, { type: '@@superglue/BEFORE_FETCH', @@ -1021,7 +1023,7 @@ describe('action creators', () => { const expectedActions = [ { type: '@@superglue/BEFORE_REMOTE', - payload: { fetchArgs: ['/foo?__=0', expect.any(Object)] }, + payload: { currentPageKey: "/bar", fetchArgs: ['/foo?__=0', expect.any(Object)] }, }, { type: '@@superglue/BEFORE_FETCH', @@ -1060,7 +1062,7 @@ describe('action creators', () => { const expectedActions = [ { type: '@@superglue/BEFORE_REMOTE', - payload: { fetchArgs: ['/foo?__=0', expect.any(Object)] }, + payload: { currentPageKey: "/bar", fetchArgs: ['/foo?__=0', expect.any(Object)] }, }, { type: '@@superglue/BEFORE_FETCH', @@ -1316,7 +1318,6 @@ describe('action creators', () => { data: { address: {}, }, - flash: {}, csrfToken: 'token', assets: [ 'application-new123.js', @@ -1333,10 +1334,6 @@ describe('action creators', () => { fetchMock.mock('/details?props_at=data.address&__=0', mockResponse) const expectedActions = [ - { - type: '@@superglue/CLEAR_FLASH', - payload: { pageKey: '/current' }, - }, { type: '@@superglue/COPY_PAGE', payload: { from: '/current', to: '/details' }, diff --git a/superglue/spec/lib/reducers.spec.js b/superglue/spec/lib/reducers.spec.js index 90ea785f..8f204f1b 100644 --- a/superglue/spec/lib/reducers.spec.js +++ b/superglue/spec/lib/reducers.spec.js @@ -83,7 +83,6 @@ describe('reducers', () => { a: { b: { c: {} } }, }, fragments: [], - flash: {}, }, } @@ -97,7 +96,6 @@ describe('reducers', () => { path: 'data.a.b.c', }, ], - flash: {}, } const nextState = pageReducer(prevState, { @@ -120,7 +118,6 @@ describe('reducers', () => { path: 'data.a.b.c', }, ], - flash: {}, }, }) }) @@ -138,7 +135,6 @@ describe('reducers', () => { path: 'data.a.b.c', }, ], - flash: {}, }, } const receivedPage = { @@ -151,7 +147,6 @@ describe('reducers', () => { path: 'data.a.b.c', }, ], - flash: {}, } const nextState = pageReducer(prevState, { @@ -174,7 +169,6 @@ describe('reducers', () => { path: 'data.a.b.c', }, ], - flash: {}, }, }) }) @@ -189,7 +183,6 @@ describe('reducers', () => { d: { e: { f: {} } }, }, fragments: [], - flash: {}, }, } @@ -203,7 +196,6 @@ describe('reducers', () => { path: 'data.d.e.f', }, ], - flash: {}, } const nextState = pageReducer(prevState, { @@ -227,7 +219,6 @@ describe('reducers', () => { path: 'data.d.e.f', }, ], - flash: {}, }, }) }) @@ -246,7 +237,6 @@ describe('reducers', () => { path: 'data.d.e.f', }, ], - flash: {}, }, } @@ -254,7 +244,6 @@ describe('reducers', () => { data: {}, path: 'data.a.b.c', fragments: [], - flash: {}, } const nextState = pageReducer(prevState, { @@ -275,13 +264,11 @@ describe('reducers', () => { '/foo': { data: { a: { b: { c: {} } } }, fragments: [], - flash: {}, }, } const receivedPage = { data: { foo: 1 }, fragments: [], - flash: {}, } const pageKey = '/foo' @@ -301,7 +288,6 @@ describe('reducers', () => { '/foo': { data: { a: { b: { c: {} } } }, fragments: [], - flash: {}, }, } @@ -309,7 +295,6 @@ describe('reducers', () => { data: { foo: 1 }, path: 'data.a.b.c', fragments: [], - flash: {}, } const nextState = pageReducer(prevState, { @@ -324,7 +309,6 @@ describe('reducers', () => { '/foo': { data: { a: { b: { c: { foo: 1 } } } }, fragments: [], - flash: {}, }, }) }) @@ -359,13 +343,11 @@ describe('reducers', () => { '/foo': { data: { a: { b: { c: {} } } }, fragments: [], - flash: {}, }, } const receivedPage = { path: 'data.a.b.c', - flash: {}, } const nextState = pageReducer(prevState, { diff --git a/superglue/spec/lib/utils/react.spec.js b/superglue/spec/lib/utils/react.spec.js index 001bd774..196b04c6 100644 --- a/superglue/spec/lib/utils/react.spec.js +++ b/superglue/spec/lib/utils/react.spec.js @@ -6,7 +6,6 @@ describe('mapStateToToProps', () => { pages: { '/foo': { data: { heading: 'hi' }, - flash: {}, }, }, superglue: { @@ -19,7 +18,6 @@ describe('mapStateToToProps', () => { heading: 'hi', pageKey: '/foo', csrfToken: 'token123', - flash: {}, }) }) }) diff --git a/superglue_rails/lib/generators/rails/templates/edit.json.props b/superglue_rails/lib/generators/rails/templates/edit.json.props index 6173407f..455957d4 100644 --- a/superglue_rails/lib/generators/rails/templates/edit.json.props +++ b/superglue_rails/lib/generators/rails/templates/edit.json.props @@ -1,10 +1,8 @@ if @post.errors.any? - content = { + json.errors({ explanation: "#{pluralize(@<%= singular_table_name %>.errors.count, "error")} prohibited this post from being saved:", messages: @<%= singular_table_name %>.errors.full_messages.map{|msg| {body: msg}} - } - - flash.now[:form_error] = content + }) end json.form(partial: 'form') do diff --git a/superglue_rails/lib/generators/rails/templates/new.json.props b/superglue_rails/lib/generators/rails/templates/new.json.props index 6cd98c5c..4fe6e0b3 100644 --- a/superglue_rails/lib/generators/rails/templates/new.json.props +++ b/superglue_rails/lib/generators/rails/templates/new.json.props @@ -1,10 +1,8 @@ if @post.errors.any? - content = { + json.errors({ explanation: "#{pluralize(@<%= singular_table_name %>.errors.count, "error")} prohibited this post from being saved:", messages: @<%= singular_table_name %>.errors.full_messages.map{|msg| {body: msg}} - } - - flash.now[:form_error] = content + }) end json.form(partial: 'form') do diff --git a/superglue_rails/lib/generators/rails/templates/web/edit.js b/superglue_rails/lib/generators/rails/templates/web/edit.js index 048a114c..f06e944a 100644 --- a/superglue_rails/lib/generators/rails/templates/web/edit.js +++ b/superglue_rails/lib/generators/rails/templates/web/edit.js @@ -1,17 +1,14 @@ import React from 'react' -// import * as actionCreators from 'javascript/packs/action_creators' -// import {useDispatch} from 'react-redux' +// import { useSelector } from 'react-redux' export default function <%= plural_table_name.camelize %>Edit ({ // visit, // remote, form, - flash, + error, <%= singular_table_name.camelize(:lower) %>Path, <%= plural_table_name.camelize(:lower) %>Path, }) { - const error = flash.form_error - const messagesEl = error && (

{ error.explanation }

diff --git a/superglue_rails/lib/generators/rails/templates/web/index.js b/superglue_rails/lib/generators/rails/templates/web/index.js index 4962171a..ddf5762f 100644 --- a/superglue_rails/lib/generators/rails/templates/web/index.js +++ b/superglue_rails/lib/generators/rails/templates/web/index.js @@ -1,14 +1,14 @@ import React from 'react' -// import * as actionCreators from 'javascript/packs/action_creators' -// import {useDispatch} from 'react-redux' +import { useSelector } from 'react-redux' export default function <%= plural_table_name.camelize %>Index({ // visit, // remote, - flash, new<%= singular_table_name.camelize %>Path, <%= plural_table_name.camelize(:lower) %> = [], }) { + const flash = useSelector((state) => state.flash) + const <%= singular_table_name.camelize(:lower) %>Items = <%= plural_table_name.camelize(:lower) %>.map((<%= singular_table_name.camelize(:lower) %>, key) => { const deleteForm = <%=singular_table_name.camelize(:lower)%>.deleteForm; @@ -31,7 +31,7 @@ export default function <%= plural_table_name.camelize %>Index({ return (
-

{flash.notice}

+

{flash && flash.notice}

<%= plural_table_name.capitalize %>

diff --git a/superglue_rails/lib/generators/rails/templates/web/new.js b/superglue_rails/lib/generators/rails/templates/web/new.js index d0cbb3c4..0356c713 100644 --- a/superglue_rails/lib/generators/rails/templates/web/new.js +++ b/superglue_rails/lib/generators/rails/templates/web/new.js @@ -1,16 +1,13 @@ import React from 'react' -// import * as actionCreators from 'javascript/packs/action_creators' -// import { useDispatch } from 'react-redux' +// import { useSelector } from 'react-redux' export default function <%= plural_table_name.camelize %>New({ // visit, // remote form, - flash, + error, <%= plural_table_name.camelize(:lower) %>Path, }) { - const error = flash.form_error - const messagesEl = error && (

{ error.explanation }

diff --git a/superglue_rails/lib/generators/rails/templates/web/show.js b/superglue_rails/lib/generators/rails/templates/web/show.js index 2f3061cd..9226da86 100644 --- a/superglue_rails/lib/generators/rails/templates/web/show.js +++ b/superglue_rails/lib/generators/rails/templates/web/show.js @@ -1,17 +1,17 @@ import React from 'react' -// import * as actionCreators from 'javascript/packs/action_creators' -// import {useDispatch} from 'react-redux' +import { useSelector } from 'react-redux' export default function <%= plural_table_name.camelize %>Show({ // visit, // remote, - flash, <%- attributes_list_with_timestamps.select{|attr| attr != :id }.each do |attr| -%> <%=attr.camelize(:lower)%>, <%- end -%> edit<%= singular_table_name.camelize %>Path, <%= plural_table_name.camelize(:lower) %>Path }) { + const flash = useSelector((state) => state.flash) + return (

{flash && flash.notice}

diff --git a/superglue_rails/lib/install/templates/web/action_creators.js b/superglue_rails/lib/install/templates/web/action_creators.js deleted file mode 100644 index 323a233a..00000000 --- a/superglue_rails/lib/install/templates/web/action_creators.js +++ /dev/null @@ -1,14 +0,0 @@ -// Example: -// -// import { -// CLEAR_FORM_ERRORS -// } from './actions' -// -// export function clearFormErrors(pageKey) { -// return { -// type: CLEAR_FORM_ERRORS, -// payload: { -// pageKey, -// } -// } -// } diff --git a/superglue_rails/lib/install/templates/web/actions.js b/superglue_rails/lib/install/templates/web/actions.js index 422e6a90..aff6aa1d 100644 --- a/superglue_rails/lib/install/templates/web/actions.js +++ b/superglue_rails/lib/install/templates/web/actions.js @@ -1,3 +1,6 @@ -// Example: -// -// export const CLEAR_FORM_ERRORS = 'CLEAR_FORM_ERRORS' +import { createAction } from '@reduxjs/toolkit' +import { SAVE_RESPONSE, BEFORE_VISIT, UPDATE_FRAGMENTS } from '@thoughtbot/superglue' + +export const saveResponse = createAction(SAVE_RESPONSE) +export const beforeVisit = createAction(BEFORE_VISIT) +export const updateFragments = createAction(UPDATE_FRAGMENTS) diff --git a/superglue_rails/lib/install/templates/web/application.js b/superglue_rails/lib/install/templates/web/application.js index 6ee07bb2..fbfb90ae 100644 --- a/superglue_rails/lib/install/templates/web/application.js +++ b/superglue_rails/lib/install/templates/web/application.js @@ -1,13 +1,11 @@ import React from 'react'; -import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; -import reduceReducers from 'reduce-reducers'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; -import { render } from 'react-dom'; -import { ApplicationBase, fragmentMiddleware } from '@thoughtbot/superglue'; -import { applicationRootReducer, applicationPagesReducer } from './reducer'; +import { createRoot } from 'react-dom/client'; +import { ApplicationBase } from '@thoughtbot/superglue'; import { buildVisitAndRemote } from './application_visit'; import { pageIdentifierToPageComponent } from './page_to_page_mapping'; +import { buildStore } from './store' class Application extends ApplicationBase { mapping() { @@ -18,27 +16,8 @@ class Application extends ApplicationBase { return buildVisitAndRemote(navRef, store); } - buildStore(initialState, { superglue: superglueReducer, pages: pagesReducer }) { - // Create the store - // See `./reducer.js` for an explaination of the two included reducers - const composeEnhancers = - (this.hasWindow && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || - compose; - const reducer = reduceReducers( - combineReducers({ - superglue: superglueReducer, - pages: reduceReducers(pagesReducer, applicationPagesReducer), - }), - applicationRootReducer - ); - - const store = createStore( - reducer, - initialState, - composeEnhancers(applyMiddleware(thunk, fragmentMiddleware)) - ); - - return store; + buildStore(initialState, { superglue, pages}) { + return buildStore(initialState, superglue, pages); } } @@ -48,7 +27,8 @@ if (typeof window !== "undefined") { const location = window.location; if (appEl) { - render( + const root = createRoot(appEl); + root.render( , - appEl + /> ); } }); } + diff --git a/superglue_rails/lib/install/templates/web/application.json.props b/superglue_rails/lib/install/templates/web/application.json.props index 45ef105e..22442dc1 100644 --- a/superglue_rails/lib/install/templates/web/application.json.props +++ b/superglue_rails/lib/install/templates/web/application.json.props @@ -21,4 +21,7 @@ end json.restore_strategy 'fromCacheAndRevisitInBackground' json.rendered_at Time.now.to_i -json.flash flash.to_h + +json.slices do + json.flash flash.to_h +end diff --git a/superglue_rails/lib/install/templates/web/flash.js b/superglue_rails/lib/install/templates/web/flash.js new file mode 100644 index 00000000..465d388b --- /dev/null +++ b/superglue_rails/lib/install/templates/web/flash.js @@ -0,0 +1,19 @@ +import { createSlice } from '@reduxjs/toolkit' +import { saveResponse, beforeVisit } from '../actions' + +export const flashSlice = createSlice({ + name: 'flash', + initialState: {}, + extraReducers: (builder) => { + builder.addCase(beforeVisit, (state, action) => { + return {} + }) + builder.addCase(saveResponse, (state, action) => { + const { page } = action.payload; + + return { + ...state, ...page.slices.flash + } + }) + } +}) diff --git a/superglue_rails/lib/install/templates/web/pages.js b/superglue_rails/lib/install/templates/web/pages.js new file mode 100644 index 00000000..81a092fe --- /dev/null +++ b/superglue_rails/lib/install/templates/web/pages.js @@ -0,0 +1,15 @@ +import { createSlice } from '@reduxjs/toolkit' +import { saveResponse, beforeVisit } from '../actions' + +export const pagesSlice = createSlice({ + name: 'pages', + // extraReducers: (builder) => { + // builder.addCase(beforeVisit, (state, action) => { + // const {currentPageKey} = action.payload + // + // const currentPage = draft[currentPageKey] + // delete currentPage.error + // }) + // } +}) + diff --git a/superglue_rails/lib/install/templates/web/reducer.js b/superglue_rails/lib/install/templates/web/reducer.js deleted file mode 100644 index 092e1698..00000000 --- a/superglue_rails/lib/install/templates/web/reducer.js +++ /dev/null @@ -1,44 +0,0 @@ -// Example: -// -// import { -// CLEAR_FORM_ERRORS -// } from './actions' -// import produce from "immer" -// -// export const applicationPagesReducer = (state = {}, action) => { -// switch(action.type) { -// case CLEAR_FORM_ERRORS: { -// const {pageKey} = action.payload -// -// return produce(state, draft => { -// const currentPage = draft[pageKey] -// delete currentPage.errors -// }) -// } -// default: -// return state -// } -// } - - -// The applicationPageReducer is for cross page reducers -// Its common to add to this. You'll typically have to pass a pageKey to the -// action payload to distinguish the current page -// -// The pageKey is passed through the props in your component. Access it like -// this: `this.props.pageKey` then dispatch it in an action -export const applicationPagesReducer = (state = {}, action) => { - switch(action.type) { - default: - return state - } -} - -// The applicationRootReducer is for app wide reducers -// Its rare to be adding to this. -export const applicationRootReducer = (state = {}, action) => { - switch(action.type) { - default: - return state - } -} diff --git a/superglue_rails/lib/install/templates/web/store.js b/superglue_rails/lib/install/templates/web/store.js new file mode 100644 index 00000000..91ea8f72 --- /dev/null +++ b/superglue_rails/lib/install/templates/web/store.js @@ -0,0 +1,32 @@ +import { configureStore } from '@reduxjs/toolkit' +import { pagesSlice } from "./slices/pages" +import { flashSlice } from "./slices/flash" +import { + BEFORE_VISIT, + BEFORE_FETCH, + BEFORE_REMOTE, + fragmentMiddleware +} from '@thoughtbot/superglue' + +export const buildStore = (initialState, superglueReducer, supergluePagesReducer) => { + + return configureStore({ + preloadedState: initialState, + devTools: process.env.NODE_ENV !== 'production', + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [BEFORE_VISIT, BEFORE_FETCH, BEFORE_REMOTE], + }, + }).concat(fragmentMiddleware), + reducer: { + superglue: superglueReducer, + pages: (state, action) => { + const nextState = supergluePagesReducer(state, action) + return pagesSlice.reducer(nextState, action) + }, + flash: flashSlice.reducer + }, + }); +}; + diff --git a/superglue_rails/lib/install/web.rb b/superglue_rails/lib/install/web.rb index b13ffecf..f6325542 100644 --- a/superglue_rails/lib/install/web.rb +++ b/superglue_rails/lib/install/web.rb @@ -22,11 +22,14 @@ def app_js_path say "Copying page_to_page_mapping.js file to #{app_js_path}" copy_file "#{__dir__}/templates/web/page_to_page_mapping.js", "#{app_js_path}/page_to_page_mapping.js" -say "Copying reducer.js file to #{app_js_path}" -copy_file "#{__dir__}/templates/web/reducer.js", "#{app_js_path}/reducer.js" +say "Copying flash.js file to #{app_js_path}" +copy_file "#{__dir__}/templates/web/flash.js", "#{app_js_path}/slices/flash.js" -say "Copying action_creators.js file to #{app_js_path}" -copy_file "#{__dir__}/templates/web/action_creators.js", "#{app_js_path}/action_creators.js" +say "Copying pages.js file to #{app_js_path}" +copy_file "#{__dir__}/templates/web/pages.js", "#{app_js_path}/slices/pages.js" + +say "Copying store.js file to #{app_js_path}" +copy_file "#{__dir__}/templates/web/store.js", "#{app_js_path}/store.js" say "Copying actions.js file to #{app_js_path}" copy_file "#{__dir__}/templates/web/actions.js", "#{app_js_path}/actions.js" @@ -46,7 +49,7 @@ def app_js_path say "Installing FormProps" run "bundle add form_props" -say "Installing React, Redux, and Superglue" -run "yarn add history react-redux redux-thunk redux reduce-reducers immer @thoughtbot/superglue --save" +say "Installing Superglue and friends" +run "yarn add history react react-dom @reduxjs/toolkit react-redux @thoughtbot/superglue --save" say "Superglue is Installed! 🎉", :green diff --git a/superglue_rails/lib/tasks/install.rake b/superglue_rails/lib/tasks/install.rake index 591df58a..089c94c6 100644 --- a/superglue_rails/lib/tasks/install.rake +++ b/superglue_rails/lib/tasks/install.rake @@ -1,17 +1,7 @@ namespace :superglue do - desc "Verifies if any version of react is in package.json" - task :verify_react do - package_json = JSON.parse(File.read(Rails.root.join("package.json"))) - - if package_json["dependencies"]["react"].nil? - warn "React not installed. Did you install React? https://github.com/rails/webpacker#react" - warn "Exiting!" && exit! - end - end - namespace :install do desc "Install everything needed for superglue web" - task "web" => ["superglue:verify_react"] do + task "web" do template = File.expand_path("../install/web.rb", __dir__) exec "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{template}" end diff --git a/superglue_rails/test/acceptance/superglue_installation_acceptance.rb b/superglue_rails/test/acceptance/superglue_installation_acceptance.rb index f80a11e7..abbbffcf 100644 --- a/superglue_rails/test/acceptance/superglue_installation_acceptance.rb +++ b/superglue_rails/test/acceptance/superglue_installation_acceptance.rb @@ -9,6 +9,9 @@ SUPERGLUE_RAILS_PATH = File.join(ROOT_DIR, "superglue_rails") SUPERGLUE_SUPERGLUE_PATH = File.join(ROOT_DIR, "superglue") VERSION = File.read(File.expand_path("../../../VERSION", __dir__)).strip +SUPERGLUE_JS_VERSION = JSON.parse( + File.read(File.expand_path("../../../superglue/package.json", __dir__)).strip +)["version"] SERVER_PORT = "3000" @@ -52,7 +55,7 @@ def successfully(command, silent = false) def update_package_json content = File.read("package.json").gsub( /"@thoughtbot\/superglue.*$/, - "\"@thoughtbot/superglue\":\"file:#{SUPERGLUE_SUPERGLUE_PATH}/thoughtbot-superglue-#{VERSION}.tgz\"," + "\"@thoughtbot/superglue\":\"file:#{SUPERGLUE_SUPERGLUE_PATH}/thoughtbot-superglue-#{SUPERGLUE_JS_VERSION}.tgz\"," ) File.open("package.json", "w") { |file| file.puts content } end