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