1 | import ensureFile from './ensure-file.js'
|
2 | import getViewRelativeToView from './get-view-relative-to-view.js'
|
3 | import path from 'path'
|
4 |
|
5 | function ensureFirstStoryIsOn(flow, key, stories) {
|
6 | if (!stories.has(key)) return
|
7 |
|
8 | let story = flow.get(key)
|
9 | if (story && story.stories.size > 0) {
|
10 | let index = 0
|
11 | for (let id of story.stories) {
|
12 | if (index === 0 || !story.isSeparate) {
|
13 | stories.add(id)
|
14 | }
|
15 | index++
|
16 | ensureFirstStoryIsOn(flow, id, stories)
|
17 | }
|
18 | }
|
19 | }
|
20 |
|
21 | let makeFlow = ({ tools, viewsById, viewsToFiles }) => {
|
22 | let flowMap = new Map()
|
23 | let flowMapStr = []
|
24 |
|
25 | for (let view of viewsToFiles.values()) {
|
26 | if (!view || view.custom || !view.parsed.view.isStory) continue
|
27 |
|
28 | let states = []
|
29 | for (let id of view.parsed.view.views) {
|
30 | let viewInView = getViewRelativeToView({
|
31 | id,
|
32 | view,
|
33 | viewsById,
|
34 | viewsToFiles,
|
35 | })
|
36 |
|
37 | if (viewInView && !viewInView.custom && viewInView.parsed.view.isStory) {
|
38 | states.push(viewInView.parsed.view.viewPath)
|
39 | }
|
40 | }
|
41 |
|
42 | let isSeparate = view.parsed.view.flow === 'separate'
|
43 | let parent = view.parsed.view.viewPathParent
|
44 |
|
45 | flowMapStr.push(
|
46 | `["${view.parsed.view.viewPath}", {
|
47 | parent: "${parent === '/' ? '' : parent}",
|
48 | isSeparate: ${isSeparate},
|
49 | stories: new Set(${states.length > 0 ? JSON.stringify(states) : ''})
|
50 | }]`
|
51 | )
|
52 | flowMap.set(view.parsed.view.viewPath, {
|
53 | parent,
|
54 | isSeparate,
|
55 | stories: new Set(states),
|
56 | })
|
57 | }
|
58 |
|
59 | let topStory = '/App'
|
60 | let initialState = new Set([topStory])
|
61 | ensureFirstStoryIsOn(flowMap, topStory, initialState)
|
62 |
|
63 | return `// This file is automatically generated by Views and will be overwritten
|
64 | // when the morpher runs. If you want to contribute to how it's generated, eg,
|
65 | // improving the algorithms inside, etc, see this:
|
66 | // https://github.com/viewstools/morph/blob/master/ensure-flow.js
|
67 |
|
68 | import React, { useCallback, useContext, useEffect, useReducer } from 'react'
|
69 | ${tools ? "import ViewsTools from './ViewsTools.js'" : ''}
|
70 |
|
71 | export let flow = new Map([${flowMapStr.join(', ')}])
|
72 |
|
73 | let TOP_STORY = "${topStory}"
|
74 |
|
75 | function ensureFirstStoryIsOn(key, stories) {
|
76 | if (!stories.has(key)) return
|
77 |
|
78 | let story = flow.get(key)
|
79 | if (story.stories.size > 0) {
|
80 | let index = 0
|
81 | let canAdd = intersection(stories, story.stories).size === 0
|
82 | for (let id of story.stories) {
|
83 | if ((canAdd && index === 0) || !story.isSeparate) {
|
84 | stories.add(id)
|
85 | }
|
86 | index++
|
87 | ensureFirstStoryIsOn(id, stories)
|
88 | }
|
89 | }
|
90 | }
|
91 |
|
92 | function ensureParents(key, stories) {
|
93 | let story = flow.get(key)
|
94 | if (!story) {
|
95 | console.error(\`View "\${key}" is missing its parent\`)
|
96 | return
|
97 | }
|
98 | if (!story.parent) {
|
99 | return
|
100 | }
|
101 |
|
102 | stories.add(story.parent)
|
103 | ensureParents(story.parent, stories)
|
104 | }
|
105 |
|
106 | function getAllChildrenOf(key, children) {
|
107 | if (!flow.has(key)) return
|
108 |
|
109 | let story = flow.get(key)
|
110 | for (let id of story.stories) {
|
111 | children.add(id)
|
112 | getAllChildrenOf(id, children)
|
113 | }
|
114 | }
|
115 |
|
116 |
|
117 | let intersection = (a, b) => new Set([...a].filter(ai => b.has(ai)))
|
118 | let difference = (a, b) => new Set([...a].filter(ai => !b.has(ai)))
|
119 |
|
120 | function getNextFlow(key, state) {
|
121 | if (state.has(key)) return state
|
122 |
|
123 | let next = new Set([key])
|
124 |
|
125 | ensureFirstStoryIsOn(key, next)
|
126 | ensureParents(key, next)
|
127 |
|
128 | let diffIn = difference(next, state)
|
129 | let diffOut = new Set()
|
130 |
|
131 | difference(state, next).forEach(id => {
|
132 | let story = flow.get(id)
|
133 | if (!story) {
|
134 | console.debug({ type: 'views/flow/missing-story', id })
|
135 | diffOut.add(id)
|
136 | return
|
137 | }
|
138 |
|
139 | if (state.has(story.parent)) {
|
140 | let parent = flow.get(story.parent)
|
141 | if (intersection(parent.stories, diffIn).size > 0) {
|
142 | diffOut.add(id)
|
143 | let children = new Set()
|
144 | getAllChildrenOf(id, children)
|
145 | children.forEach(cid => diffOut.add(cid))
|
146 | }
|
147 | }
|
148 | })
|
149 |
|
150 | let nextState = new Set([...difference(state, diffOut), ...diffIn])
|
151 | ensureFirstStoryIsOn(TOP_STORY, nextState)
|
152 | return new Set([...nextState].sort())
|
153 | }
|
154 |
|
155 | let MAX_ACTIONS = 10000
|
156 | let SYNC = 'flow/SYNC'
|
157 | let SET = 'flow/SET'
|
158 |
|
159 | let Context = React.createContext([{ actions: [], flow: new Set() }, () => {}])
|
160 | export let useFlowState = () => useContext(Context)[0]
|
161 | export let useFlow = () => useFlowState().flow
|
162 | export let useSetFlowTo = () => {
|
163 | let [, dispatch] = useContext(Context)
|
164 | return useCallback(id => dispatch({ type: SET, id }), []) // eslint-disable-line
|
165 | // ignore dispatch
|
166 | }
|
167 |
|
168 | function getNextActions(state, id) {
|
169 | return [id, ...state.actions].slice(0, MAX_ACTIONS)
|
170 | }
|
171 |
|
172 | function reducer(state, action) {
|
173 | switch (action.type) {
|
174 | ${
|
175 | tools
|
176 | ? `case SYNC: {
|
177 | return {
|
178 | flow: new Set(action.flow),
|
179 | actions: getNextActions(state, action.id)
|
180 | }
|
181 | }`
|
182 | : ''
|
183 | }
|
184 |
|
185 | case SET: {
|
186 | if (process.env.NODE_ENV === 'development') {
|
187 | console.debug({ type: 'views/flow/set', id: action.id })
|
188 |
|
189 | if (!flow.has(action.id)) {
|
190 | console.debug({ type: 'views/flow/invalid-story', id: action.id, availableStories: flow })
|
191 | throw new Error(
|
192 | \`Story "$\{action.id}" doesn't exist. See the valid stories logged above this error.\`
|
193 | )
|
194 | }
|
195 | }
|
196 |
|
197 | if (state.actions[0] === action.id) {
|
198 | if (process.env.NODE_ENV === 'development') {
|
199 | console.debug({ type: 'views/flow/already-set-as-last-action-ignoring', id: action.id, actions: state.actions })
|
200 | }
|
201 | return state
|
202 | }
|
203 |
|
204 | return {
|
205 | flow: getNextFlow(action.id, state.flow),
|
206 | actions: getNextActions(state, action.id)
|
207 | }
|
208 | }
|
209 |
|
210 | default: {
|
211 | throw new Error(\`Unknown action "\${action.type}" in Flow\`)
|
212 | }
|
213 | }
|
214 | }
|
215 |
|
216 | export function ViewsFlow(props) {
|
217 | let context = useReducer(reducer, { actions: [], flow: props.initialState })
|
218 | let [state] = context
|
219 |
|
220 | useEffect(() => {
|
221 | if (typeof props.onChange === 'function') {
|
222 | props.onChange(state)
|
223 | }
|
224 | }, [state]) // eslint-disable-line
|
225 | // ignore props.onChange
|
226 |
|
227 | return (
|
228 | <Context.Provider value={context}>
|
229 | ${
|
230 | tools
|
231 | ? '<ViewsTools flow={context}>{props.children}</ViewsTools>'
|
232 | : '{props.children}'
|
233 | }
|
234 | </Context.Provider>
|
235 | )
|
236 | }
|
237 |
|
238 | ViewsFlow.defaultProps = {
|
239 | initialState: new Set(${JSON.stringify([...initialState], null, ' ')})
|
240 | }
|
241 |
|
242 | export function normalizePath(viewPath, relativePath) {
|
243 | let url = new URL(\`file://\${viewPath}/../\${relativePath}\`)
|
244 | return url.pathname
|
245 | }`
|
246 | }
|
247 |
|
248 | export default function ensureFlow({ src, tools, viewsById, viewsToFiles }) {
|
249 | return ensureFile({
|
250 | file: path.join(src, 'Logic', 'ViewsFlow.js'),
|
251 | content: makeFlow({ tools, viewsById, viewsToFiles }),
|
252 | })
|
253 | }
|