Skip to content

Commit

Permalink
feat(demo): save tasks in demo and Collection (#511)
Browse files Browse the repository at this point in the history
* WIP: refactor collections

* WIP: fix project bugs

* WIP: save tasks to firebase

* WIP: change tasks implementation

* WIP: saves running task and updates ui with elapsed

* WIP: moving some computed to Collection, path changes

* WIP: prevent updateNow from retrigger

* WIP: add contributors

* WIP: fix saving running task

* WIP: fixed bugs, better UI

* WIP: keep header visible during login

* WIP: move firebase settings in its own file, add README

* WIP: should auto start running task

* WIP: remove unused images, change favicon

* WIP: allow anonymous login

* WIP: fix bug when project is not saved

* WIP: use visibility api to stop time passing when hidden
  • Loading branch information
gaspard authored and Guria committed Nov 30, 2016
1 parent 1d4e21f commit 2d565fc
Show file tree
Hide file tree
Showing 122 changed files with 1,469 additions and 880 deletions.
52 changes: 52 additions & 0 deletions packages/demo/README.md
@@ -0,0 +1,52 @@
# Cerebral-Demo

This application is a simple time-tracker built with `cerebral`, `cerebral-provider-firebase`, `cerebral-forms` and `cerebral-router`. It serves as an example on how all these modules work together to create a complete application with relational data, login, time based events and so on.

This project is an ongoing community effort so if you feel like you could add some features, please join in !


## Firebase setup

In case you want to run this application on your own firebase project, please follow the following steps:

### Config

Copy the `web app` config data from the firebase console into `src/firebaseConfig.js`.

### Authentification

Enable `email/password` authentification in the firebase console.

### Security rules

Set these security rules in the database section of the firebase console:

```js
{
"rules": {
"$uid": {
"clients": {
".indexOn": "updated_at"
},
"projects": {
".indexOn": "updated_at"
},
"tasks": {
".indexOn": "updated_at"
},
".read": "auth != null && $uid == auth.uid",
".write": "auth != null && $uid == auth.uid"
},
".read": false,
".write": false
}
}
```

## Start application

Once the dependencies are installed, start the application with:

```
npm run start
```
12 changes: 10 additions & 2 deletions packages/demo/TODO.md
@@ -1,5 +1,13 @@
* Up/down arrows in project selector should select projects in current selector list.
* user info in menu (with logout link)
* enable Report page with url query for filtering by client/project/date, etc
* showSaveDraftModal for project
* on update of client website url, add missing 'http://' if needed and in Client/index.js and Client/form.js, remove adding http://
* refuse to delete project with tasks or move tasks to no-project.
* refuse to delete client with projects or move projects to no-client.
* Create (dark) footer with links to cerebral stuff and language selector.
* Tasks component to search for tasks.., not sure what should go there.

* @julio ? implement sendEmailVerification
and applyActionCode

* @gaspard edit/delete tasks...
* @gaspard fix editing name of running task (it is saved and the `updated` call changes the field the user is currently typing in). Should prevent updates to forms field when it is focussed.
4 changes: 3 additions & 1 deletion packages/demo/package.json
Expand Up @@ -6,7 +6,9 @@
"author": "Gaspard Bucher <gaspard@lucidogen.io>",
"contributors": [
"Aleksey Guryanov <gurianov@gmail.com>",
"Christian Alfoni <christianalfoni@gmail.com>"
"Christian Alfoni <christianalfoni@gmail.com>",
"Henri Hulski <henri.hulski@gazeta.pl>",
"Julio Saito <saitodisse@gmail.com>"
],
"devDependencies": {
"node-sass": "^3.11.2",
Expand Down
Binary file removed packages/demo/public/img/cerebral-logo.png
Binary file not shown.
Binary file removed packages/demo/public/img/cerebral-mini.png
Binary file not shown.
Binary file removed packages/demo/public/img/client-mini.png
Binary file not shown.
Binary file modified packages/demo/public/img/favicon.ico
Binary file not shown.
26 changes: 26 additions & 0 deletions packages/demo/src/common/Collection/computed/visibleKeys.js
@@ -0,0 +1,26 @@
import {Computed} from 'cerebral'
import paths from '../paths'
import sort from '../sort'

export default function (moduleName) {
const {collectionPath, draftPath, filterPath} = paths(moduleName)

return Computed(
{
items: `${collectionPath}.**`,
afilter: filterPath,
selectedKey: `${draftPath}.key`
},
({items, afilter, selectedKey}) => {
const filter = afilter && afilter.toLowerCase()
const list = Object.keys(items).filter(key => (
!filter || items[key].name.toLowerCase().indexOf(filter) >= 0
))
if (selectedKey && list.indexOf(selectedKey) < 0) {
// Always show edited item (also if it is not saved yet)
list.unshift(selectedKey)
}
return list.sort(sort)
}
)
}
27 changes: 27 additions & 0 deletions packages/demo/src/common/Collection/index.js
@@ -0,0 +1,27 @@
import init from './signals/init'
import create from './signals/create'
import discardDraft from './signals/discardDraft'
import edit from './signals/edit'
import newItem from './signals/newItem'
import update from './signals/update'
import updated from './signals/updated'
import updateDraft from './signals/updateDraft'
import updateFilter from './signals/updateFilter'
import remove from './signals/remove'
import removed from './signals/removed'

export default function collection (moduleName, initState) {
return {
create: create(moduleName),
discardDraft: discardDraft(moduleName),
edit: edit(moduleName),
init: init(moduleName, initState),
newItem: newItem(moduleName),
update: update(moduleName),
updated: updated(moduleName),
remove: remove(moduleName),
removed: removed(moduleName),
updateFilter: updateFilter(moduleName),
updateDraft: updateDraft(moduleName)
}
}
5 changes: 5 additions & 0 deletions packages/demo/src/common/Collection/operators/makeRef.js
@@ -0,0 +1,5 @@
import {v4} from 'uuid'

export default function (context) {
return {value: v4()}
}
27 changes: 27 additions & 0 deletions packages/demo/src/common/Collection/paths.js
@@ -0,0 +1,27 @@
import {input, set, state, string, when} from 'cerebral/operators'

export default function paths (moduleName) {
return {
collectionPath: `${moduleName}.all`,
draftPath: `${moduleName}.$draft`,
filterPath: `${moduleName}.$filter`,
errorPath: `app.$error`,
dynamicPaths: [
set(
input`remoteCollectionPath`, string`${state`user.$currentUser.uid`}.${moduleName}`
),
when(input`key`), {
true: [
set(
input`itemPath`, string`${moduleName}.all.${input`key`}`
),
set(
input`remoteItemPath`,
string`${input`remoteCollectionPath`}.${input`key`}`
)
],
false: []
}
]
}
}
11 changes: 11 additions & 0 deletions packages/demo/src/common/Collection/props/list.js
@@ -0,0 +1,11 @@
import visibleKeys from '../computed/visibleKeys'
import paths from '../paths'

export default function connectProps (moduleName) {
const {draftPath, filterPath} = paths(moduleName)
return {
visibleKeys: visibleKeys(moduleName),
filter: filterPath,
selectedKey: `${draftPath}.key`
}
}
9 changes: 9 additions & 0 deletions packages/demo/src/common/Collection/signals/create.js
@@ -0,0 +1,9 @@
import newItem from './newItem'
import update from './update'

export default function (moduleName) {
return [
...newItem(moduleName),
...update(moduleName)
]
}
10 changes: 10 additions & 0 deletions packages/demo/src/common/Collection/signals/discardDraft.js
@@ -0,0 +1,10 @@
import {state, unset} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {draftPath} = paths(moduleName)
return [
unset(state`${draftPath}.key`),
unset(state`${draftPath}`)
]
}
13 changes: 13 additions & 0 deletions packages/demo/src/common/Collection/signals/edit.js
@@ -0,0 +1,13 @@
import {input, set, state} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {draftPath, dynamicPaths} = paths(moduleName)
return [
dynamicPaths,
set(state`${draftPath}`, state`${input`itemPath`}`),
// To trigger change on collection list listening for
// draft.key
set(state`${draftPath}.key`, input`key`)
]
}
45 changes: 45 additions & 0 deletions packages/demo/src/common/Collection/signals/init.js
@@ -0,0 +1,45 @@
import {input, merge, set, state, when} from 'cerebral/operators'
import {onChildAdded, onChildChanged, onChildRemoved, value} from 'cerebral-provider-firebase'
import paths from '../paths'

let timestamp

export default function init (moduleName, initState = {}) {
const {collectionPath, dynamicPaths, errorPath} = paths(moduleName)
return [
// prepare remote and local path
...dynamicPaths,
() => {
// timestamp before calling value
timestamp = (new Date()).getTime()
},
value(input`remoteCollectionPath`), {
success: [
// need to use 'merge' here to notify changes on default item keys
merge(state`${collectionPath}`, initState),
when(input`value`), {
true: [
merge(state`${collectionPath}`, input`value`)
],
false: []
},
// start listening
onChildAdded(
input`remoteCollectionPath`, `${moduleName}.updated`,
{
orderByChild: 'updated_at',
// Use the timestamp set before getting value
startAt: () => ({value: timestamp})
}
),
onChildChanged(
input`remoteCollectionPath`, `${moduleName}.updated`),
onChildRemoved(
input`remoteCollectionPath`, `${moduleName}.removed`)
],
error: [
set(state`${errorPath}`, input`error`)
]
}
]
}
15 changes: 15 additions & 0 deletions packages/demo/src/common/Collection/signals/newItem.js
@@ -0,0 +1,15 @@
import {merge, set, state} from 'cerebral/operators'
import makeRef from '../operators/makeRef'
import paths from '../paths'

export default function (moduleName) {
const {draftPath, filterPath} = paths(moduleName)
return [
// Prepare initial item state
set(state`${draftPath}`, {}),
merge(state`${draftPath}`, {
key: makeRef,
name: state`${filterPath}`
})
]
}
14 changes: 14 additions & 0 deletions packages/demo/src/common/Collection/signals/remove.js
@@ -0,0 +1,14 @@
import {input} from 'cerebral/operators'
import {remove} from 'cerebral-provider-firebase'
import paths from '../paths'

export default function (moduleName) {
const {dynamicPaths} = paths(moduleName)
return [
...dynamicPaths,
remove(input`remoteItemPath`), {
success: [],
error: []
}
]
}
10 changes: 10 additions & 0 deletions packages/demo/src/common/Collection/signals/removed.js
@@ -0,0 +1,10 @@
import {input, state, unset} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {dynamicPaths} = paths(moduleName)
return [
...dynamicPaths,
unset(state`${input`itemPath`}`)
]
}
17 changes: 17 additions & 0 deletions packages/demo/src/common/Collection/signals/save.js
@@ -0,0 +1,17 @@
import {input, set} from 'cerebral/operators'
import {set as setRemote} from 'cerebral-provider-firebase'
import paths from '../paths'
import timestampValue from './timestampValue'

export default function (moduleName) {
const {dynamicPaths} = paths(moduleName)
return [
// Expects input.key and input.value
// Ensure value.key is properly set.
set(input`value.key`, input`key`),
...timestampValue,
...dynamicPaths,
setRemote(input`remoteItemPath`, input`value`)
// This chain must be followed by {success: [], error: []}
]
}
11 changes: 11 additions & 0 deletions packages/demo/src/common/Collection/signals/timestampValue.js
@@ -0,0 +1,11 @@
export default [
function timestampValue ({input: {value}}) {
return {value: Object.assign(
{},
{created_at: {'.sv': 'timestamp'}},
value,
{updated_at: {'.sv': 'timestamp'}}
)
}
}
]
21 changes: 21 additions & 0 deletions packages/demo/src/common/Collection/signals/update.js
@@ -0,0 +1,21 @@
import {input, set, state, unset} from 'cerebral/operators'
import paths from '../paths'
import save from './save'

export default function (moduleName) {
const {draftPath, errorPath} = paths(moduleName)
const p = [
set(input`key`, state`${draftPath}.key`),
set(input`value`, state`${draftPath}`),
...save(moduleName), {
success: [
// Clear form.
unset(state`${draftPath}.key`)
],
error: [
set(state`${errorPath}`, input`error`)
]
}
]
return p
}
9 changes: 9 additions & 0 deletions packages/demo/src/common/Collection/signals/updateDraft.js
@@ -0,0 +1,9 @@
import {input, set, state} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {draftPath} = paths(moduleName)
return [
set(state`${draftPath}.${input`key`}`, input`value`)
]
}
9 changes: 9 additions & 0 deletions packages/demo/src/common/Collection/signals/updateFilter.js
@@ -0,0 +1,9 @@
import {input, set, state} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {filterPath} = paths(moduleName)
return [
set(state`${filterPath}`, input`value`)
]
}
21 changes: 21 additions & 0 deletions packages/demo/src/common/Collection/signals/updated.js
@@ -0,0 +1,21 @@
import {input, set, state, when} from 'cerebral/operators'
import paths from '../paths'

export default function (moduleName) {
const {draftPath, dynamicPaths} = paths(moduleName)

return [
...dynamicPaths,

set(state`${input`itemPath`}`, input`value`),

when(state`${draftPath}.key`, input`key`,
(draftKey, updatedKey) => draftKey === updatedKey
), {
true: [
set(state`${draftPath}`, input`value`)
],
false: []
}
]
}

0 comments on commit 2d565fc

Please sign in to comment.