From 63d4cd9aef3dba08f8336608fb6b6f90355a9f9a Mon Sep 17 00:00:00 2001 From: ddibened Date: Thu, 1 Nov 2018 19:11:50 -0400 Subject: [PATCH 1/2] updating README.md --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76501b07..014e74d6 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -This is our README. \ No newline at end of file +/---------------------------- GENERAL INFORMATION ----------------------/ + +THE TREE... (structure of the html elements) + +(Electron Window) + --> App (this is where the reference to the model obj gets made and lives) + - + --> Frame (this is where the visible layout begins) + - + --> Task List (the list of tasks along the left side) + - + --> TaskDisplay (this loads the information about the currently selected task) + - + --> TabList (the list of tabs associated with that task) + - + --> TabDisplay (this is the display for whichever tab has been clicked) + - + -> (whatever tab gets clicked. Ex. , , etc.) + +My understanding of the basic datastructures for the app (this can be changed however makes sense).... + + Model: + The model contains the single source of data for the app. Anything that wants to access some part of the + data calls "model.subscribeTo(, "tasktitles")" and passes it a reference to itself (generally + componentDidMount() or or the constructor is a good place to put this) with a string indicating which data source + it wants to know about (TaskList wants to know about the "task_titles"). The subscribing component needs to implement + an "onChange(newData)" method that describes what it should do with the new data (generally involving a call to + this.setState() ). + + When a tab wants to change some part of the data, it calls "model.request(action)", and passes in an 'action' object, + which looks (something) like this, // action = {code: "task_analytics_update", data: { }} // + + note - I think it would be easiest to track component specific changes within your component, and then send the resulting + composite object (probably something like // action.data = {analytics: {all the data}}) // ) as a whole,rather than send actions to the model for each minor change as it occurs. + + To render your component for debuggging/testing (see ExTab for an example component) you can go into TabDisplay (the component +that renders whichever tab gets clicked) and add inside the ".displayContainer" div. If you want to pass it test +arguments, you can call it with and then in your constructor, you can access +this data with 'props.testData'. + + +/------------------------------- OTHER STUFF? -------------------------------/ + From 0ebeb66ee7d05ed699e4ffe0cde0b10c530d951c Mon Sep 17 00:00:00 2001 From: ddibened Date: Sat, 3 Nov 2018 02:00:43 -0400 Subject: [PATCH 2/2] More succinct Model. ExTab example tracks its state and has a working button, as well as corresponding test. Components are passed copy of stored state, and they pass registerFinalState() to Model on unmount. All tests passing, though I rolled back some tests because they failed on refactor. Added some functionality to ExTab as an example of updating state and a button. Also updated the README. --- README.md | 53 ++++++---- src/App/App.js | 6 +- src/Model/Model.js | 125 +++++++++++++---------- src/Model/Model.test.js | 26 +++-- src/Model/ObservableData.js | 32 ++++-- src/Model/ObservableData.test.js | 46 ++++++++- src/Model/Resources.js | 35 +++++++ src/Model/Resources.test.js | 12 +++ src/TestResources/testutils.js | 22 ++-- src/Utilities/GeneralContent.js | 15 +++ src/components/ExTab/ExTab.test.js | 23 +++-- src/components/ExTab/View.js | 22 ++-- src/components/ExTab/index.js | 58 +++++------ src/components/Frame/View.js | 2 +- src/components/Frame/index.js | 16 +-- src/components/TabDisplay/index.js | 10 ++ src/components/TabList/index.js | 6 +- src/components/TaskDisplay/View.js | 60 +++-------- src/components/TaskDisplay/index.js | 52 +++++++++- src/components/TaskList/TaskList.test.js | 38 +------ src/components/TaskList/index.js | 39 ++++--- 21 files changed, 429 insertions(+), 269 deletions(-) create mode 100644 src/Model/Resources.js create mode 100644 src/Model/Resources.test.js create mode 100644 src/Utilities/GeneralContent.js diff --git a/README.md b/README.md index 014e74d6..72d96c2a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -/---------------------------- GENERAL INFORMATION ----------------------/ +/--------------------------------- THE TREE -----------------------------------/ THE TREE... (structure of the html elements) @@ -17,27 +17,44 @@ THE TREE... (structure of the html elements) - -> (whatever tab gets clicked. Ex. , , etc.) -My understanding of the basic datastructures for the app (this can be changed however makes sense).... - - Model: - The model contains the single source of data for the app. Anything that wants to access some part of the - data calls "model.subscribeTo(, "tasktitles")" and passes it a reference to itself (generally - componentDidMount() or or the constructor is a good place to put this) with a string indicating which data source - it wants to know about (TaskList wants to know about the "task_titles"). The subscribing component needs to implement - an "onChange(newData)" method that describes what it should do with the new data (generally involving a call to - this.setState() ). - - When a tab wants to change some part of the data, it calls "model.request(action)", and passes in an 'action' object, - which looks (something) like this, // action = {code: "task_analytics_update", data: { }} // - - note - I think it would be easiest to track component specific changes within your component, and then send the resulting - composite object (probably something like // action.data = {analytics: {all the data}}) // ) as a whole,rather than send actions to the model for each minor change as it occurs. - - To render your component for debuggging/testing (see ExTab for an example component) you can go into TabDisplay (the component + RENDERING FOR DEBUGGING: + To render your component for debuggging/testing (see ExTab for an example component) you can go into TabDisplay (the component that renders whichever tab gets clicked) and add inside the ".displayContainer" div. If you want to pass it test arguments, you can call it with and then in your constructor, you can access this data with 'props.testData'. +/---------------------------------- MODEL ------------------------------------/ + + Model: + The Model manages the interactions between components (mostly tabs) and all the data for the app (list of all tasks ('taskList' + list of all task titles, paired with their 'key' ('titlesKeyPairs')), and the most recently clicked task ('currentTask')). It handles updates to the data (add, edit, remove) in response to actions (clicks, form entries, etc.) that occur from anywhere in the app. + + What I think 'tasks' are: + task = { + title: "", + key: , + attrs: { < general information, ie. notification level, settings, etc. > }, + tabs: { < all information that the tabs need or have saved goes here > } + } + + Components and the model: + Basically, I don't think they should interact directly with the model, I think it is easier to test and manage if + the components (tabs) get passed the single function that they need from the Model ('recordFinalState()'), rather than a reference to the entire thing. + + -> The TabDisplay component (who's role is rendering tabs based on tab clicks) that renders your component will pass + in all the data from the currently selected task that has to do with your component, along with that task's 'key' property (a unique numeric identifier). + + -> I think you should handle actions locally, meaning if a user wants to edit a journal entry, then record those changes in + your local copy of the Journal info for the currently selected task in your component's 'state'. Then in your component's 'componentWillUnmount()' method, pass the final version of that state, with all of its accumulated changes (or no changes if nothing changed) back to the Model, and it will update the appropriate task (via the Model's recordFinalState("", this.state, key), which will be passed as a function into your component (again, through the 'props' argument in the + constructor)). (the functional version of the this.setState() method will come in handy, as if you use it, React will automatically re-render your component with the appropriately updated state). + + *** I'll update ExTab to try and illustrate what I mean *** + + *** if you think this is bonkers, slack me, I am happy to make changes to try and make it easier :) *** + + *** For now, just pretend that you have access to all of the data from the currentTask that has to do with your individual + component, passed in through the 'props' argument in the constructor. *** + /------------------------------- OTHER STUFF? -------------------------------/ diff --git a/src/App/App.js b/src/App/App.js index af7bc3f9..2583862f 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -2,11 +2,14 @@ import React, { Component } from 'react'; import Frame from '../components/Frame'; import Model from '../Model/Model'; import './App.css'; +import { getTestTaskListSmall } from '../TestResources/testutils' class App extends Component { constructor(props) { super(props); - this.model = new Model(); + // this reference to the model gets passed down through the tree + // this is just for testing.. + this.model = new Model(true, getTestTaskListSmall()); } render() { @@ -15,7 +18,6 @@ class App extends Component { ); - } } diff --git a/src/Model/Model.js b/src/Model/Model.js index 9054ed5d..e4f420db 100644 --- a/src/Model/Model.js +++ b/src/Model/Model.js @@ -1,13 +1,7 @@ -/* - * This tracks state changes. - */ - -import ObservableData from './ObservableData'; -// just for testing :) -import { getTestTaskListSmall } from '../TestResources/testutils' +import Resources from './Resources'; /* - * This the single source of data for the app. + * This manages connections between components, data, and actions (updates, edits, deletion, etc.). * * Any components that need information can subscribe to the datasource that they need (and they * will be updated (through their 'onChange()' method) when that data is altered). @@ -16,62 +10,87 @@ import { getTestTaskListSmall } from '../TestResources/testutils' */ class Model { - constructor() { - this.currentTask = new ObservableData(); - // 'allTasks' - holds the task objects (title, key, {tabs}) - this.allTasks = new ObservableData(); + // There is only an initTaskList passed in when 'testing' == true; + constructor(testing, initTaskList) { + // TODO -> read from DB on construction + + // Load some testing data in lieu of loading from DB + if (testing) { + this.resources = new Resources(initTaskList); + } else { + this.resources = new Resources([]); + } + // Lol you better be testing because otherwise resources is always empty... - // TODO -> read from DB on initial load - // THIS IS TEMPORARY - - this.allTasks.updateData(getTestTaskListSmall()); + // binding the 'this' of that function to be the Model reference. + this.registerFinalState = this.registerFinalState.bind(this); } // TODO -> Destructor (save changes to the file) - // returns all the tasks that exist. - getAllTasks() { - // return this.allTasks; - // dummy data for testing purposes - return this.allTasks.data; - } - // TODO -> maybe the TaskList should just subscribe, and circumvent this little thing? + /* + * 'desired_resource' - a string (ie. "task_title_list") that indicates which resource the subscriber wants to + * know about. + */ + subscribeTo(obs, desired_resource) { - getAllTaskTitles() { - return this.allTasks.data.map((task) => { - return task.title; - }); + switch (desired_resource) { + case "title_key_list": + this.resources.titleKeyList.subscribe(obs); + break; + case "current_task": + this.resources.currentTask.subscribe(obs); + break; + default: + throw Error("hmmm, couldn't find that resource: ${desired_resource}"); + } + } /* - * This is the function that applies changes to the data. - * - * all 'action' is an object with {code: 'what_to_do', args: { } }. + * Unsubscribes the 'obs' from the 'subbed_resource'. */ - applyChange(action) { - //remove task - //add task - //task completed - //edit task (meaning edit tabs) - } -} + unsubscribeFrom(obs, subbed_resource) { -export default Model; + switch (subbed_resource) { + case "title_key_list": + this.resources.titleKeyList.unsubscribe(obs); + break; + case "current_task": + this.resources.currentTask.unsubscribe(obs); + break; + } -/* - * Maybe the components should only recieve the necessary subscribe function, as opposed to the entire model? - * - * Would it be cleaner to parse the general task[] in the model itself (as various OD's), or have the components just clean - * up whatever they need? - */ + } -/* - * ALTERNATIVE DESIGN: as things are constructed, they are added (similar to the subscriber approach) into an array, - * that lives inside the model, and button clicks / interactions correspond to various actions that the model coordinates - * entirely (components are just conduits, they no longer contain logic). - * - * -- or maybe this is just how clicks should be handled? YES THIS - * - * Maybe something more functional in design? - but I don't really want to pass state all over the place... - */ + /* + * This is the function that updates tasks based on the final state of the tab.... + */ + registerFinalState(componentName, finalState, key) { + // Filter for the task that got changed by matching 'key's. + this.resources.allTasks.apply((obsTask) => { + const task = obsTask.getData(); + if (task.key === key) { + // finding th tab to update + var foundIt = false; + task.tabs.apply((tab) => { + if (tab.title === componentName) { + tab.info = finalState; + // mark that we found the tab we were looking for + foundIt = true; + } + }); + // handling the case when we don't find it + if (!foundIt) { + throw Error( + "You tried to update a component that isn't associated with that task: \n\tcom -> ${componentName}\n\ttask -> ${task})" + ); + } else { + obsTask.updateData(task); + } + } + }); + } +} +export default Model; \ No newline at end of file diff --git a/src/Model/Model.test.js b/src/Model/Model.test.js index 9962d1df..90b70604 100644 --- a/src/Model/Model.test.js +++ b/src/Model/Model.test.js @@ -1,20 +1,26 @@ -import Model from './Model' -import { getTestTaskListSmall, getTestTaskListTitlesSmall } from '../TestResources/testutils.js' +import Model from './Model'; +import { getTestTaskListSmall, getTestTitleKeyListSmall } from '../TestResources/testutils.js'; describe('Model', () => { it('constructs a new Model', () => { - const model = new Model(); - expect(model !== null); + const model = new Model(true, getTestTaskListSmall()); + expect(model.resources !== null); }); - it('returns list of titles on \'getAllTaskTitles()\'', () => { - const model = new Model(); - model.allTasks.updateData(getTestTaskListSmall()); + it('allows subscription and updates on task title list', () => { + const testing = true; + const model = new Model(testing, getTestTaskListSmall()); - const titles = model.getAllTaskTitles(); - const correctTitles = getTestTaskListTitlesSmall(); + var obs = { + taskTitles: [], + onChange: (newTitles) => { + obs.taskTitles = newTitles; + } + }; - expect(titles).toEqual(correctTitles) + model.subscribeTo(obs, "title_key_list"); + + expect(obs.taskTitles).toEqual(getTestTitleKeyListSmall()); }); }); \ No newline at end of file diff --git a/src/Model/ObservableData.js b/src/Model/ObservableData.js index 2ceacb2e..1bc2550a 100644 --- a/src/Model/ObservableData.js +++ b/src/Model/ObservableData.js @@ -3,37 +3,57 @@ * Objects can 'subscribe' to the data, and are notified (through calls to their 'onChange' method) * of any updates to the data. * - * In this application the TaskList will subscribe to the list of task titles as well as the current task, - * and the TaskDisplay will subscribe to the 'currentTask.tabs' property. + * In this application the TaskList will subscribe to the list of task titles as well as the current task + * (in case it needs to color it), and the TaskDisplay will subscribe to the 'currentTask.tabs' property, and pass + * the relevant data into whichever task is loaded. */ + class ObservableData { constructor() { - // 'data' - the currently selected task this.data = null; // 'subscribers' the objects that want to know about changes to that data this.subscribers = []; } + // Add the 'observer' to the subscriber list subscribe(observer) { if (typeof(observer.onChange) == "function") { this.subscribers.push(observer); - observer.onChange(this.data); + observer.onChange(this.copyData()); } else { // This string template ${thing} isn't working like I think it does... - throw TypeError("${observer} doesn't implement 'onChange() method...'"); + throw TypeError("{observer.toString()} doesn't implement 'onChange() method...'"); } } + // Remove the 'observer' from the subscriber list unsubscribe(observer) { this.subscribers = this.subscribers.filter((value) => { return value !== observer; }); } + // this returns a shallow copy of the original data + getData() { + return this.copyData(); + } + copyData() { + if (this.data === null) { + throw Error("Data has not been set yet"); + } else { + if (this.data === 'object') { + return Object.assign({}, this.data); + } else { + return this.data; + } + } + } + + // This is the only function that should ever actually change data updateData(newData) { this.data = newData; - this.subscribers.forEach((obs) => obs.onChange(newData)); + this.subscribers.forEach((obs) => obs.onChange(this.copyData())); } } diff --git a/src/Model/ObservableData.test.js b/src/Model/ObservableData.test.js index 178b44d9..f0efe9fe 100644 --- a/src/Model/ObservableData.test.js +++ b/src/Model/ObservableData.test.js @@ -1,4 +1,4 @@ -import ObservableData from './ObservableData' +import ObservableData from './ObservableData'; describe('ObservableData', () => { it('constructor inits correctly', () => { @@ -12,6 +12,8 @@ describe('ObservableData', () => { const od = new ObservableData(); const obs = new MockObserver(); + od.updateData(2); + od.subscribe(obs);  od.updateData(4); @@ -38,6 +40,8 @@ describe('ObservableData', () => { const obs2 = new MockObserver(); const obs3 = new MockObserver(); + od.updateData(11); + od.subscribe(obs1); od.subscribe(obs2); od.subscribe(obs3); @@ -53,6 +57,8 @@ describe('ObservableData', () => { const od = new ObservableData(); const obs = new MockObserver(); const obs2 = new MockObserver(); + + od.updateData(2); od.subscribe(obs); od.subscribe(obs2); @@ -66,14 +72,50 @@ describe('ObservableData', () => { expect(od.subscribers.length).toEqual(1); expect(obs.internalState).toEqual(2); }); + + it('calls \'obs.onChange(newData)\' with a shallow copy of data when data is a primitive', () => { + const od = new ObservableData(); + var originalTitle = "tomatoes"; + var data = originalTitle; + + od.updateData(data); + + var obs = new MockObserver(); + + od.subscribe(obs); + + expect(obs.data).toEqual(originalTitle); + obs.data = "new title"; + expect(od.data).toEqual(originalTitle); + }); + + it('call \'obs.onChange(newData)\' with a shallow copy of data when data is an object', () => { + const od = new ObservableData(); + var originalData = { + title: "tomatoes", + list: [1, 2, 3, 4] + } + + od.updateData(originalData); + + var obs = new MockObserver(); + od.subscribe(obs); + + expect(obs.data).toEqual(originalData); + obs.data.title = "different data!"; + obs.data.list[2] = 6; + expect(od.data).toEqual(originalData); + }); });    class MockObserver { constructor() { this.internalState = -1; + this.data= "init title"; } - onChange(ignoredData) { + onChange(newData) { this.internalState += 1; + this.data = newData; } } \ No newline at end of file diff --git a/src/Model/Resources.js b/src/Model/Resources.js new file mode 100644 index 00000000..add53918 --- /dev/null +++ b/src/Model/Resources.js @@ -0,0 +1,35 @@ +import ObservableData from './ObservableData'; +import { InitialTask } from '../Utilities/GeneralContent'; +/* + * This is where the data is parsed and stored throughout its life. It only ever returns + * copies of what it is holding, not actual references to their values. + */ + export default class Resources { + // Initializing the things we will want to track + constructor(allTasks) { + // Each task is wrapped in an ObservableData object... + this.taskList = allTasks.map((task, index) => { + const obs = new ObservableData(); + task.key = index; + obs.updateData(task) + return obs; + }); + this.titleKeyList = new ObservableData(); + this.currentTask = new ObservableData(); + + this.currentTask.updateData(new InitialTask()); + this.titleKeyList.updateData(this.parseTasksToTitles()); + } + + parseTasksToTitles() { + return ( + this.taskList.map((obsTask) => { + const task = obsTask.getData() + return { + title: task.title, + key: task.key, + } + }) + ); + } + } \ No newline at end of file diff --git a/src/Model/Resources.test.js b/src/Model/Resources.test.js new file mode 100644 index 00000000..243b06ab --- /dev/null +++ b/src/Model/Resources.test.js @@ -0,0 +1,12 @@ +import Resources from './Resources' +import { getTestTaskListSmall, getTestTitleKeyListSmall } from '../TestResources/testutils' + +describe('Resources', () => { + + // This also verifies that it parses the titles correctly. + it('properly inits resources in constructor', () => { + const res = new Resources(getTestTaskListSmall()); + + expect(res.titleKeyList.getData()).toEqual(getTestTitleKeyListSmall()); + }); +}); \ No newline at end of file diff --git a/src/TestResources/testutils.js b/src/TestResources/testutils.js index 5fbf8cc8..7f37a898 100644 --- a/src/TestResources/testutils.js +++ b/src/TestResources/testutils.js @@ -9,16 +9,16 @@ // returns a (small) list of tasks export function getTestTaskListSmall() { return [ - { title: "get pizza", key: 0, tabs: [ { analytics: "ooh buddy, that's a lot of pepperoni" }, { journal: "pinapple, nice!" }]}, - {title: "go swimming", key: 1, tabs: [ { journal: "the pool was lovely this evening" }, { journal: "I look good in my new goggles" }]}, - {title: "pick up kid", key: 2, tabs: [ { journal: "that was fun" }, { analytics: "time to put them back down" } ]}, - {title: "practice snorkeling", key: 3, tabs: [ { analytics: "20 minutes"}, {calendar: "some dates"}]} + { title: "get pizza", key: 0, tabs: [ { title: "analytics", info: "ooh buddy, that's a lot of pepperoni" }, { title: "journal", info: "pinapple, nice!" }]}, + {title: "go swimming", key: 1, tabs: [ { title: "journal", info: "the pool was lovely this evening" }, { title: "journal", info: "I look good in my new goggles" }]}, + {title: "pick up kid", key: 2, tabs: [ { title: "journal", info: "that was fun" }, { title: "journal", info: "time to put them back down" } ]}, + {title: "practice snorkeling", key: 3, tabs: [ { title: "analytics", info: "20 minutes"}, {calendar: "some dates"}]} ]; } -export function getTestTaskListTitlesSmall() { - return [ - "get pizza", - "go swimming", - "pick up kid", - "practice snorkeling"]; -} + +// returns the map of titles and keys from the other small list. +export function getTestTitleKeyListSmall() { + return getTestTaskListSmall().map((task, index) => { + return {key: index, title: task.title}; + });; +} \ No newline at end of file diff --git a/src/Utilities/GeneralContent.js b/src/Utilities/GeneralContent.js new file mode 100644 index 00000000..0f6809da --- /dev/null +++ b/src/Utilities/GeneralContent.js @@ -0,0 +1,15 @@ +/* + * These are large definitions that are reused throughout the application. + */ + +/* + * This is the task that is loaded when the app first opens, possibly it could be expanded into a sort of home screen, + * kind of like what we used to call the "today" view. + */ +export class InitialTask { + constructor() { + this.title = "initialTask"; + this.tabs = [ {title: "welcome tab", info: {welcomeMessage: "Welcome, select a task, or create a new one :) "}}]; + this.key = -1; + } +} \ No newline at end of file diff --git a/src/components/ExTab/ExTab.test.js b/src/components/ExTab/ExTab.test.js index 9dc1ef7c..1baa26fd 100644 --- a/src/components/ExTab/ExTab.test.js +++ b/src/components/ExTab/ExTab.test.js @@ -1,6 +1,3 @@ -/* - * This is where the tests go - */ import React from 'react'; // 'enzyme' is a testing tool that renders a fake DOM, // 'shallow()' will render only the component itself, @@ -8,25 +5,37 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import ExTab from './index.js'; +/* + * This is where the tests go + */ + // what are you testing describe('ExTab', () => { - // this is the test of an individual component + // this is the test of an individual method it('should render on render()', () => { - const extab = shallow().render(); + const extab = shallow(); // search the wrapper (which shallow() returns) for elements with the class // 'ExTab' (it borrows css syntax) expect(extab.find('.ExTab')).not.toEqual(null); }); + // verify rendered data (which in this case requires mount() rather than shallow()) it('should have the right title', () => { const extab = mount(); - console.log(extab.find('ExTab').text()); - expect(extab.find('.ExTab').text()).toEqual("tomatoes, apples, big difference."); }); + + it('should increment \'state.data\' on button clicks', () => { + const extab = mount(); + + extab.find('.exbutton').simulate('click'); + extab.find('.exbutton').simulate('click'); + + expect(extab.state().data).toEqual(2); + }); }); // To run only these tests, use " npm test 'ExTab' " diff --git a/src/components/ExTab/View.js b/src/components/ExTab/View.js index 3be1feaf..264ff5f3 100644 --- a/src/components/ExTab/View.js +++ b/src/components/ExTab/View.js @@ -1,17 +1,19 @@ -/* - * This is where you define how the component will render - */ import React from 'react'; // import a css file if you need it // import './ExTab.css' -// you can use argument you passed in to View.... (but you have to call them the same thing as you called -// them when you passed them in from index.js's return ); -const View = ({title}) => ( -
- {title} +/* + * This is where you define how the component will render + */ + +// you can use argument(s) you passed in to View.... (but you have to call them the same thing you called +// them when you passed them in from index.js's // return ) //; +const View = ({title, data, onClick}) => ( +
+
{title}
+
{data}
+
); -export default View; - +export default View; \ No newline at end of file diff --git a/src/components/ExTab/index.js b/src/components/ExTab/index.js index 06d90c30..663d1abe 100644 --- a/src/components/ExTab/index.js +++ b/src/components/ExTab/index.js @@ -1,23 +1,25 @@ +import React, { Component } from 'react'; +import View from './View'; + /* * This is the file where you write all your business logic, and it defines the class * that will extend React.Component, and be rendered into the DOM. * - * I would advise making a fake task in the constructor, which will later be replaced by - * a subscripting to the state of data in the model. You can assume that any information - * you need will be included in the task-tab you create, and we'll figure out how to - * get it there when the rest of it render the way you want it to :) (it won't be hard, + * I would advise making a fake copy of all the info you need from the task in the constructor, which will later be + * replaced be supplied by TabDisplay. You can assume that any information + * you need will be included in the tab/component you create, and we'll figure out how to + * get it there when the rest of it renders the way you want it to :) (it won't be hard, * just more complicated than I can describe here). * * You can test this by adding into the TabDisplay/View.js file (between the
's) */ -import React, { Component } from 'react'; -import View from './View'; // this 'export default' is how we get it in other files, but it can also go at the end of the file (see 1.) export default class ExTab extends Component { constructor(props) { - // have to call this so we don't overwrite the general React.Component constructor + // have to call this so we don't overwrite the general React.Component constructor, + // and it you don't, you will get an error that 'this' is not defined. super(props); /* * 'props' contains fields that correspond to the arguments passed into this object, for example, @@ -25,50 +27,40 @@ export default class ExTab extends Component { * then ExTab's 'props' would have a property called 'props.title', with whatever value 'mytitle' was. */ - // You have to provide an initial value for 'state' if you are going to use it. + // This is a good spot to initialize the 'state' of this component. + // You have to provide an initial value for 'state' if you are going to use it, it will probably be passed in via + // the 'props'. this.state = { - data: [] + data: 0 } this.mytitle = "tomatoes, apples, big difference."; - // If you leave this blank, you will get linter warnings about 'useless constructors'. - - // it's also a good spot to initialize the 'state' of this component. + // You have to bind functions you are going to pass so that when they get called, they know what 'this' is + this.onClick = this.onClick.bind(this); } /* - * This is a lifecycle method that gets called after this element gets loaded into the DOM. - */ - componentDidMount() { - // it's where you want to subscribe to whatever data you need in the model - // this.model.subscribe() THIS PROTOCOL IS PROBABLY GOING TO CHANGE - } - - /* - * Another lifecycle method that gets called + * lifecycle method that gets called when the component is being removed from the DOM */ componentWillUnmount() { - // this is where you should '.unsubscribe(this)' from the model. + // this is where you record your final state to the model + // ex --> this.recordFinalState("ex_tab", this.state); + // this is commented out because I'm not passing in the register function yet :) } - /* - * This function is what the model will call if the data it's subscribed to changes. - */ - onChange(updatedData) { - // Set takes as an argument a function that takes the 'state' (and optionally the props) - // and returns the newState. + // defining the click method to pass to the button + onClick() { this.setState((state) => { - // how do you want to change this state based on the old state? - // 'state' is the old state, and you return the new state. - return state.data = updatedData; + state.data += 1; + return state; }); } - // This is the function that will actually return the html element that show up in the DOM. + // This is the function that will actually return the html element that shows up in the DOM. render() { // pass this anything that it will need (for instance, I am going to pass a string) - return ; + return ; } } diff --git a/src/components/Frame/View.js b/src/components/Frame/View.js index cfd0b5c1..cff392ff 100644 --- a/src/components/Frame/View.js +++ b/src/components/Frame/View.js @@ -7,7 +7,7 @@ import './Frame.css'; const View = ({model}) => (
- +
); diff --git a/src/components/Frame/index.js b/src/components/Frame/index.js index 688c90b2..94c700ff 100644 --- a/src/components/Frame/index.js +++ b/src/components/Frame/index.js @@ -1,12 +1,12 @@ -// the definition of the Frame objects import React, { Component } from 'react'; import View from './View'; -export default class Frame extends Component { -// why is there a view file/component on top of the index.js for the frame component itself? - render() { - return ( - - ); - } +/* + * The definition of the Frame component. + */ +export default function Frame(props) { + + return ( + + ); } diff --git a/src/components/TabDisplay/index.js b/src/components/TabDisplay/index.js index 68da0186..8e9df591 100644 --- a/src/components/TabDisplay/index.js +++ b/src/components/TabDisplay/index.js @@ -3,7 +3,17 @@ import View from './View'; class TabDisplay extends Component { + constructor(props) { + super(props); + + this.tabInfo = props.tabInfo; + this.tabToDisplay = props.tabToDisplay; + // this is being passed the registerFinalState function, it just isn't passing it yet. + + } + render() { + // TODO -> parse tabinfo to decide what to render... return ; } diff --git a/src/components/TabList/index.js b/src/components/TabList/index.js index 889bf4ec..e411a493 100644 --- a/src/components/TabList/index.js +++ b/src/components/TabList/index.js @@ -1,9 +1,9 @@ -/* - * This component is all about the tab list. - */ import React, { Component } from 'react'; import View from "./View"; +/* + * This component renders the list of tabs for the 'current_task'. + */ class TabList extends Component { render() { diff --git a/src/components/TaskDisplay/View.js b/src/components/TaskDisplay/View.js index 20f6e071..8cfc3979 100644 --- a/src/components/TaskDisplay/View.js +++ b/src/components/TaskDisplay/View.js @@ -2,58 +2,22 @@ import React from 'react'; import TabList from '../TabList'; import TabDisplay from '../TabDisplay'; import './TaskDisplay.css'; -import BigCalendar from 'react-big-calendar-like-google'; -import 'react-big-calendar/lib/css/react-big-calendar.css' -// https://github.com/intljusticemission/react-big-calendar/issues/234 -/* The current version of big calendar implemented here is the most basic, the package also allows: -- event creation -- Localization -- show more via popup -- drag and drop -*see more here http://intljusticemission.github.io/react-big-calendar/examples/index.html -Q&A about big-calendar availale on Discord https://discordapp.com/channels/102860784329052160/424364360731852800 -*/ -import moment from 'moment' -/* - * TODO -> move Calendar into its own component +/* + * Render the TaskDisplay component, which renders the TabList and TabDisplay components. + * + * This passes down the list of tabs, the title of the tab to display, and the info it will need from the + * current task. */ +const View = ({tabList, tabToDisplay, tabInfo, registerFinalState}) => { -const localizer = BigCalendar.momentLocalizer(moment) -let allViews = Object.keys(BigCalendar.Views).map(k => BigCalendar.Views[k]) - -const View = props => { - /*const myEventsList = [ - { - allDay: false, - end: new Date('December 10, 2017 11:13:00'), - start: new Date('December 09, 2017 11:13:00'), - title: 'hi', - }, - { - allDay: true, - end: new Date('December 09, 2017 11:13:00'), - start: new Date('December 09, 2017 11:13:00'), - title: 'All Day Event', - }, - ]; */ return (
- - - + +
)} -export default View; - -/* -
- -
-*/ \ No newline at end of file +export default View; \ No newline at end of file diff --git a/src/components/TaskDisplay/index.js b/src/components/TaskDisplay/index.js index 5dcf8d9c..25b7d20e 100644 --- a/src/components/TaskDisplay/index.js +++ b/src/components/TaskDisplay/index.js @@ -1,15 +1,63 @@ import React, { Component } from 'react'; import View from './View'; +import { InitialTask } from '../../Utilities/GeneralContent'; class TaskDisplay extends Component { constructor(props) { super(props); - // we are going to pass things in through here (like all the tabs, updating task, etc.) + // This is the farthest down the tree the Model itself needs to go... + this.model = props.model; + + this.state = { + currentTask: new InitialTask(), + currentTabTitle: "welcome tab", + }; + } + + componentDidMount() { + // subscribe to the "current_task" + this.model.subscribeTo(this, "current_task"); + } + + componentWillUnmount() { + this.model.unsubscribeFrom(this, "current_task"); + } + + onChange(newCurrentTask) { + this.setState((state) => { + state.currentTask = newCurrentTask; + return state; + }); + } + + /* + * Clicks to tabList should cause a different tab to be loaded... + * + * 'tabClicked' - a string that represents which tab got clicked. + */ + onClick(tabClicked) { + this.setState((state) => { + state.currentTabTitle = tabClicked; + return state; + }) } render() { - return ; + const tabList = this.state.currentTask.tabs.map((tab) => { + return tab.name; + }); + const tabInfo = this.state.currentTask.tabs.filter((tab) => { + if (tab.title === this.state.currentTabTitle) { + return tab.info; + } + }); + + return < View + tabList={tabList} + tabToDisplay={this.state.currentTabTitle} + tabInfo={tabInfo} + registerFinalState={this.model.registerFinalState} />; } } diff --git a/src/components/TaskList/TaskList.test.js b/src/components/TaskList/TaskList.test.js index d461af84..322be561 100644 --- a/src/components/TaskList/TaskList.test.js +++ b/src/components/TaskList/TaskList.test.js @@ -6,8 +6,7 @@ import Model from '../../Model/Model' describe('TaskList', () => { it('should display all tasks', () => { - const model = new Model(); - model.allTasks.data = getTestTaskListSmall(); + const model = new Model(true, getTestTaskListSmall()); const tl = mount(); const allRenderedTasks = tl.find('.task'); @@ -15,41 +14,10 @@ describe('TaskList', () => { expect(allRenderedTasks.length).toEqual(getTestTaskListSmall().length); allRenderedTasks.forEach((el) => { - expect(model.getAllTaskTitles().includes(el.text())).toBe(true); + expect(model.resources.taskList.map( (task) => { return task.getData().title }).includes(el.text())).toBe(true); }); }); - it('should update display when the model\'s task list changes', () => { - const model = new Model(); - model.allTasks.updateData(getTestTaskListSmall()); - const tl = mount(); - - // removing one of the tasks - model.allTasks.updateData(getTestTaskListSmall().filter((task) => { - if (task.title != "pick up kid") { - return task; - } - })); - - tl.update(); - const allRenderedTasks = tl.find('.task'); - expect(allRenderedTasks.length).toEqual(getTestTaskListSmall().length - 1); - - allRenderedTasks.forEach((el) => { - expect(model.getAllTaskTitles().includes(el.text())).toBe(true); - }); - - }); - it('should unsubscribe when unmounting', () => { - const model = new Model(); - model.allTasks.data = getTestTaskListSmall(); - const tl = mount(); - - expect(model.allTasks.subscribers.length).toEqual(1); - tl.unmount(); + // FINISH THESE - - - expect(model.allTasks.subscribers.length).toEqual(0); - }); }); \ No newline at end of file diff --git a/src/components/TaskList/index.js b/src/components/TaskList/index.js index d5c11dcd..88470b5f 100644 --- a/src/components/TaskList/index.js +++ b/src/components/TaskList/index.js @@ -1,51 +1,50 @@ -// creating the list of tasks import React, { Component } from 'react'; import View from "./View" -import Model from '../../Model/Model' +/* + * Renders the list of tasks, and also creates the button for adding new tasks. + */ class TaskList extends Component { constructor(props) { super(props); - // this.model = props.model; - // not sure where model was coming from and it was creating errors so I'm just gonna make a new one here - this.model = new Model(); + + this.model = props.model; // setting the initial state this.state = { - taskTitles: ["no tasks to display..."] - } - - this.onChange.bind(this); + taskTitles: [ { key:-2, title: "no tasks to display..." } ] + }; } /* * This is a lifecycle method from React.Component that gets called after this - * component it loaded into the DOM. + * component is loaded into the DOM. */ componentDidMount() { // The TaskList wants to know when the list of tasks changes. - this.model.allTasks.subscribe(this); - // Setting the component's initial state - this.onChange(); + this.model.subscribeTo(this, "title_key_list"); } // Another lifecycle method, that gets called before it is removed from the DOM. componentWillUnmount() { - this.model.allTasks.unsubscribe(this); + this.model.unsubscribeFrom(this, ""); } - // I think I will change this to subscribe exlusively to the title list. - onChange(newData) { + // When the list of titles changes, this component should update its state to reflect the new title list + onChange(newTitles) { this.setState((state) => { - state.taskTitles = this.model.getAllTaskTitles(); + state.taskTitles = newTitles; return state; }); } + // TODO -> Create the button that adds new tasks, pass it an on click method + render() { - // convert task objects to task elements (should this be its own function?) - const taskTitleElementList = this.state.taskTitles.map((title) => { - return
{title}
; + // converting task objects to task elements + const taskTitleElementList = this.state.taskTitles.map((titleKeyPair) => { + + return
{titleKeyPair.title}
; }); return ;