mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Web app update
This commit is contained in:
parent
6c3c9d8a83
commit
d47bcdff64
15
.gitignore
vendored
15
.gitignore
vendored
@ -15,12 +15,13 @@
|
||||
/vendor/
|
||||
/web/bundles/
|
||||
*.sublime-workspace
|
||||
database.sqlite
|
||||
QtClient/build-*-Debug/
|
||||
*.pro.user
|
||||
notes*.sqlite
|
||||
Makefile.Debug
|
||||
Makefile.Release
|
||||
Makefile
|
||||
Makefile.*
|
||||
QtClient/build-*
|
||||
TODO.md
|
||||
tests/generated
|
||||
QtClient/JoplinQtClient/make.bat
|
||||
*.pro.user
|
||||
QtClient/data/
|
||||
app/data/uploads/
|
||||
!app/data/uploads/.gitkeep
|
||||
sparse_test.php
|
17
QtClient/JoplinQtClient/make.bat
Executable file
17
QtClient/JoplinQtClient/make.bat
Executable file
@ -0,0 +1,17 @@
|
||||
SET PATH=%PATH%;"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin"
|
||||
|
||||
cd "D:\Web\www\joplin\QtClient\build-evernote-import-qt-Visual_C_32_bites-Debug"
|
||||
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
|
||||
"C:\Qt\5.7\msvc2015\bin\qmake.exe" D:\Web\www\joplin\QtClient\evernote-import\evernote-import-qt.pro -spec win32-msvc2015 "CONFIG+=debug" "CONFIG+=qml_debug"
|
||||
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
|
||||
"C:\Qt\Tools\QtCreator\bin\jom.exe" qmake_all
|
||||
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
|
||||
"C:\Qt\Tools\QtCreator\bin\jom.exe"
|
||||
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
|
||||
|
||||
|
||||
/cygdrive/c/Qt/Tools/QtCreator/bin/jom.exe
|
@ -58,4 +58,15 @@ CREATE TABLE version (
|
||||
version INT
|
||||
);
|
||||
|
||||
--CREATE TABLE mimetypes (
|
||||
-- id INT,
|
||||
-- mime TEXT
|
||||
--);
|
||||
|
||||
--CREATE TABLE mimetype_extensions (
|
||||
-- id INTEGER PRIMARY KEY,
|
||||
-- mimetype_id,
|
||||
-- extension TEXT
|
||||
--);
|
||||
|
||||
INSERT INTO version (version) VALUES (1);
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include <QSqlRecord>
|
||||
#include <QCryptographicHash>
|
||||
#include <QTextCodec>
|
||||
#include <QDataStream>
|
||||
|
||||
#include "xmltomd.h"
|
||||
|
||||
@ -164,7 +165,18 @@ xmltomd::Resource parseResource(QXmlStreamReader& reader) {
|
||||
return xmltomd::Resource();
|
||||
}
|
||||
|
||||
output.data = QByteArray::fromBase64(reader.readElementText().toUtf8());
|
||||
|
||||
//qApp->exit(0);
|
||||
|
||||
QByteArray ba;
|
||||
// qDebug() << reader.text();
|
||||
// qApp->exit(0);
|
||||
QString s = reader.readElementText();
|
||||
s = s.replace("\n", "");
|
||||
ba.append(s);
|
||||
output.data = QByteArray::fromBase64(ba);
|
||||
// qDebug() << output.data.toBase64();
|
||||
// exit(0);
|
||||
} else if (reader.name() == "mime") {
|
||||
output.mime = reader.readElementText();
|
||||
} else if (reader.name() == "resource-attributes") {
|
||||
@ -282,7 +294,7 @@ Note parseNote(QXmlStreamReader& reader) {
|
||||
// </en-export>
|
||||
|
||||
int mediaHashIndex = 0;
|
||||
for (int i = 0; i < note.resources.size(); i++) {
|
||||
for (size_t i = 0; i < note.resources.size(); i++) {
|
||||
xmltomd::Resource& r = note.resources[i];
|
||||
if (r.id == "") {
|
||||
if (note.enMediaElements.size() <= mediaHashIndex) {
|
||||
@ -339,6 +351,13 @@ void filePutContents(const QString& filePath, const QString& content) {
|
||||
}
|
||||
}
|
||||
|
||||
QString extensionFromMimeType(const QString& mimeType) {
|
||||
if (mimeType == "image/jpg" || mimeType == "image/jpeg") return ".jpg";
|
||||
if (mimeType == "image/png") return ".png";
|
||||
if (mimeType == "image/gif") return ".gif";
|
||||
return "";
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QCoreApplication a(argc, argv);
|
||||
|
||||
@ -387,9 +406,9 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
std::vector<Note> notes = parseXmlFile(fileInfo.absoluteFilePath());
|
||||
|
||||
for (int noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||
for (size_t noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||
Note n = notes[noteIndex];
|
||||
for (int resourceIndex = 0; resourceIndex < n.resources.size(); resourceIndex++) {
|
||||
for (size_t resourceIndex = 0; resourceIndex < n.resources.size(); resourceIndex++) {
|
||||
xmltomd::Resource resource = n.resources[resourceIndex];
|
||||
QSqlQuery query(db);
|
||||
query.prepare("INSERT INTO resources (id, title, mime, filename, created_time, updated_time) VALUES (?,?,?,?,?,?)");
|
||||
@ -406,10 +425,19 @@ int main(int argc, char *argv[]) {
|
||||
query.addBindValue(resource.id);
|
||||
query.addBindValue(n.id);
|
||||
query.exec();
|
||||
|
||||
QString resourceFilePath = resourceDir + "/" + resource.id; //+ extensionFromMimeType(resource.mime);
|
||||
QFile resourceFile(resourceFilePath);
|
||||
if (resourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
QDataStream stream(&resourceFile);
|
||||
stream << resource.data;
|
||||
} else {
|
||||
qWarning() << "Cannot write to" << resourceFilePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||
for (size_t noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||
Note n = notes[noteIndex];
|
||||
|
||||
// if (i != 8 || noteIndex != 3090) continue;
|
||||
|
@ -7,11 +7,15 @@ services:
|
||||
|
||||
app.eloquent:
|
||||
class: AppBundle\Eloquent
|
||||
arguments: []
|
||||
|
||||
# app.fine_diff:
|
||||
# class: AppBundle\FineDiff
|
||||
# arguments: []
|
||||
arguments: ['@app.mime_types', '@app.paths']
|
||||
|
||||
twig.exception_listener:
|
||||
class: stdObject
|
||||
class: stdObject
|
||||
|
||||
app.paths:
|
||||
class: AppBundle\Paths
|
||||
arguments: [%kernel.root_dir%]
|
||||
|
||||
app.mime_types:
|
||||
class: AppBundle\MimeTypes
|
||||
arguments: ['@app.paths']
|
||||
|
1853
app/data/mime.types
Executable file
1853
app/data/mime.types
Executable file
File diff suppressed because it is too large
Load Diff
1
app/data/mime_types.php
Executable file
1
app/data/mime_types.php
Executable file
File diff suppressed because one or more lines are too long
0
app/data/uploads/.gitkeep
Normal file
0
app/data/uploads/.gitkeep
Normal file
@ -140,7 +140,7 @@ class Config {
|
||||
|
||||
}
|
||||
|
||||
class FolderItem {
|
||||
class BaseItem {
|
||||
|
||||
private $title = '';
|
||||
private $body = '';
|
||||
@ -183,17 +183,17 @@ class FolderItem {
|
||||
|
||||
}
|
||||
|
||||
class FolderItems {
|
||||
class BaseItems {
|
||||
|
||||
private $items = array();
|
||||
|
||||
private function getFolderItems($dir, $parentId, &$output) {
|
||||
private function getBaseItems($dir, $parentId, &$output) {
|
||||
$paths = glob($dir . '/*');
|
||||
foreach ($paths as $path) {
|
||||
$isFolder = is_dir($path);
|
||||
$modTime = filemtime($path);
|
||||
|
||||
$o = new FolderItem();
|
||||
$o = new BaseItem();
|
||||
$o->setTitle(basename($path));
|
||||
$o->setId(Api::createId($parentId . '_' . $o->title()));
|
||||
$o->setParentId($parentId);
|
||||
@ -202,13 +202,13 @@ class FolderItems {
|
||||
|
||||
if (!$isFolder) $o->setBody(file_get_contents($path));
|
||||
$output[] = $o;
|
||||
if ($isFolder) $this->getFolderItems($path, $o->id(), $output);
|
||||
if ($isFolder) $this->getBaseItems($path, $o->id(), $output);
|
||||
}
|
||||
}
|
||||
|
||||
public function fromPath($path) {
|
||||
$this->items = array();
|
||||
$this->getFolderItems($path, null, $this->items);
|
||||
$this->getBaseItems($path, null, $this->items);
|
||||
}
|
||||
|
||||
public function all() {
|
||||
@ -272,8 +272,8 @@ $api->setSessionId($session['id']);
|
||||
if (array_key_exists('sync', $flags)) {
|
||||
$syncStartTime = time();
|
||||
$lastSyncTime = $config->get('last_sync_time');
|
||||
$folderItems = new FolderItems();
|
||||
$folderItems->fromPath($dataPath);
|
||||
$BaseItems = new BaseItems();
|
||||
$BaseItems->fromPath($dataPath);
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Get latest changes from API
|
||||
@ -287,7 +287,7 @@ if (array_key_exists('sync', $flags)) {
|
||||
$notes = array();
|
||||
$maxId = null;
|
||||
foreach ($response['items'] as $item) {
|
||||
$folderItem = new FolderItem();
|
||||
$BaseItem = new BaseItem();
|
||||
|
||||
switch ($item['type']) {
|
||||
|
||||
@ -295,7 +295,7 @@ if (array_key_exists('sync', $flags)) {
|
||||
case 'update':
|
||||
|
||||
$resource = $api->exec('GET', $item['item_type'] . 's/' . $item['item_id']);
|
||||
$folderItem->fromApiArray($item['item_type'], $resource);
|
||||
$BaseItem->fromApiArray($item['item_type'], $resource);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -304,13 +304,13 @@ if (array_key_exists('sync', $flags)) {
|
||||
|
||||
}
|
||||
|
||||
$folderItems->setById($folderItem->id(), $folderItem);
|
||||
$BaseItems->setById($BaseItem->id(), $BaseItem);
|
||||
|
||||
$maxId = max($item['id'], $maxId);
|
||||
}
|
||||
|
||||
foreach ($folderItems->all() as $item) {
|
||||
$relativePath = $folderItems->itemFullPath($item);
|
||||
foreach ($BaseItems->all() as $item) {
|
||||
$relativePath = $BaseItems->itemFullPath($item);
|
||||
$path = $dataPath . '/' . $relativePath;
|
||||
|
||||
foreach (array('folder', 'note') as $itemType) {
|
||||
@ -333,7 +333,7 @@ if (array_key_exists('sync', $flags)) {
|
||||
// Send changed notes and folders to API
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
foreach ($folderItems->all() as $item) {
|
||||
foreach ($BaseItems->all() as $item) {
|
||||
if ($item->modTime() < $lastSyncTime) continue;
|
||||
|
||||
if ($item->isFolder()) {
|
||||
|
12
spa_client/.babelrc
Executable file
12
spa_client/.babelrc
Executable file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react",
|
||||
"stage-0"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"transform-decorators-legacy"
|
||||
]
|
||||
]
|
||||
}
|
39
spa_client/.gitignore
vendored
Executable file
39
spa_client/.gitignore
vendored
Executable file
@ -0,0 +1,39 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Ignore files for distribution
|
||||
dist/*
|
||||
|
||||
# OSX .DS_Store files
|
||||
.DS_Store
|
19
spa_client/README.md
Executable file
19
spa_client/README.md
Executable file
@ -0,0 +1,19 @@
|
||||
React/Redux Example Todo Application
|
||||
====================================
|
||||
|
||||
An example application demonstrating how to build a Todo with React and Redux.
|
||||
|
||||
# How to run
|
||||
|
||||
Open up a terminal and execute:
|
||||
|
||||
```term
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Then open up your browser and navigate to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
# License
|
||||
|
||||
Apache 2.0
|
42
spa_client/package.json
Executable file
42
spa_client/package.json
Executable file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "stormpath-react-redux-todo-example-application",
|
||||
"version": "0.0.1",
|
||||
"description": "React/Redux example todo application.",
|
||||
"homepage": "https://github.com/typerandom/stormpath-react-redux-todo-example-application",
|
||||
"author": "Stormpath, 2016",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "node ./server ./src/ ./webpack.dev.config",
|
||||
"build": "rm -rf ./dist/ && mkdir ./dist/ && cp -r ./src/* ./dist && rm -rf ./dist/js/* && webpack",
|
||||
"start": "npm run build && node ./server"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/typerandom/stormpath-react-redux-todo-example-application.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/typerandom/stormpath-react-redux-todo-example-application/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.3.26",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-plugin-react-transform": "^2.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-0": "^6.3.13",
|
||||
"babel-runtime": "^6.3.19",
|
||||
"express": "^4.13.4",
|
||||
"morgan": "^1.7.0",
|
||||
"open": "0.0.5",
|
||||
"react": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-redux": "^4.4.6",
|
||||
"redux": "^3.4.0",
|
||||
"webpack": "^1.12.13",
|
||||
"webpack-dev-middleware": "^1.5.1",
|
||||
"object-path-immutable": "^0.5.1",
|
||||
"deepcopy": "^0.6.3"
|
||||
}
|
||||
}
|
32
spa_client/server.js
Executable file
32
spa_client/server.js
Executable file
@ -0,0 +1,32 @@
|
||||
var open = require('open');
|
||||
var path = require('path');
|
||||
var morgan = require('morgan');
|
||||
var express = require('express');
|
||||
var webpack = require('webpack');
|
||||
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||
|
||||
var config = require('./webpack.config');
|
||||
var compiler = webpack(config);
|
||||
|
||||
var app = express();
|
||||
|
||||
app.use(morgan('dev'));
|
||||
|
||||
app.use(webpackDevMiddleware(compiler, {
|
||||
noInfo: true,
|
||||
publicPath: config.output.publicPath
|
||||
}));
|
||||
|
||||
app.use(express.static('./dist/'));
|
||||
|
||||
app.get('*', function (req, res){
|
||||
res.sendFile(path.resolve(__dirname, './dist/', 'index.html'))
|
||||
});
|
||||
|
||||
app.listen(3000, function (err) {
|
||||
if (err) {
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
console.log('Web listening at http://localhost:3000/.');
|
||||
});
|
39
spa_client/src/index.html
Executable file
39
spa_client/src/index.html
Executable file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
a, a:active, a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
.selected {
|
||||
border: 1px solid black;
|
||||
}
|
||||
.level-1 {
|
||||
margin-left: 0;
|
||||
}
|
||||
.level-2 {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.level-3 {
|
||||
margin-left: 40px;
|
||||
}
|
||||
.folder-list {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 15px;
|
||||
}
|
||||
.note-list {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
420
spa_client/src/js/app.jsx
Executable file
420
spa_client/src/js/app.jsx
Executable file
@ -0,0 +1,420 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom'
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux'
|
||||
import RootFolderList from 'components/root-folder-list.jsx';
|
||||
import NoteList from 'components/note-list.jsx';
|
||||
import { reducer } from './reducer.jsx'
|
||||
|
||||
let defaultState = {
|
||||
'myButtonLabel': 'click',
|
||||
items: [
|
||||
{ id: 101, title: 'folder 1', type: 1, parent_id: 0 },
|
||||
{ id: 102, title: 'folder 2', type: 1, parent_id: 0 },
|
||||
{ id: 103, title: 'folder 3', type: 1, parent_id: 101 },
|
||||
{ id: 104, title: 'folder 4', type: 1, parent_id: 101 },
|
||||
{ id: 105, title: 'folder 5', type: 1, parent_id: 0 },
|
||||
{ id: 106, title: 'folder 6', type: 1, parent_id: 105 },
|
||||
{ id: 1, type: 2, parent_id: 101, title: 'one', body: '111 dsqfdsmlk mqkfkdq sfkl qlmskfqm' },
|
||||
{ id: 2, type: 2, parent_id: 101, title: 'two', body: '222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||
{ id: 3, type: 2, parent_id: 103, title: 'three', body: '33 dsqfdsmlk mqkfkdq sfkl 33 qlmskfqm' },
|
||||
{ id: 4, type: 2, parent_id: 103, title: 'four', body: '4222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||
{ id: 5, type: 2, parent_id: 103, title: 'five', body: '5222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||
{ id: 6, type: 2, parent_id: 104, title: 'six', body: '6222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||
{ id: 7, type: 2, parent_id: 104, title: 'seven', body: '7222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||
],
|
||||
selectedFolderId: null,
|
||||
selectedNoteId: null,
|
||||
expandedFolderIds: [],
|
||||
}
|
||||
|
||||
let store = createStore(reducer, defaultState)
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<RootFolderList />
|
||||
<NoteList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('container')
|
||||
)
|
||||
|
||||
|
||||
// var defaultState = {
|
||||
// folders: [],
|
||||
// todo: {
|
||||
// items: []
|
||||
// },
|
||||
// selectedFolderId: null
|
||||
// };
|
||||
|
||||
// function addTodo(message) {
|
||||
// return {
|
||||
// type: 'ADD_TODO',
|
||||
// message: message,
|
||||
// completed: false
|
||||
// };
|
||||
// }
|
||||
|
||||
// function completeTodo(index) {
|
||||
// return {
|
||||
// type: 'COMPLETE_TODO',
|
||||
// index: index
|
||||
// };
|
||||
// }
|
||||
|
||||
// function deleteTodo(index) {
|
||||
// return {
|
||||
// type: 'DELETE_TODO',
|
||||
// index: index
|
||||
// };
|
||||
// }
|
||||
|
||||
// function clearTodo() {
|
||||
// return {
|
||||
// type: 'CLEAR_TODO'
|
||||
// };
|
||||
// }
|
||||
|
||||
// function createId() {
|
||||
// return Math.round(Math.random() * 99999);
|
||||
// }
|
||||
|
||||
// function folderIndex(state, id) {
|
||||
// for (var i = 0; i < state.folders.length; i++) {
|
||||
// if (state.folders[i].id == id) return i;
|
||||
// }
|
||||
// return -1;
|
||||
// }
|
||||
|
||||
// function folderById(state, id) {
|
||||
// var i = folderIndex(state, id);
|
||||
// return i >= 0 ? state.folders[i] : null;
|
||||
// }
|
||||
|
||||
// function todoApp(state, action) {
|
||||
|
||||
// switch (action.type) {
|
||||
|
||||
// case 'ADD_FOLDER':
|
||||
|
||||
// var folder = {
|
||||
// name: action.name,
|
||||
// id: createId(),
|
||||
// selected: false
|
||||
// };
|
||||
|
||||
// state = immutable.push(state, 'folders', folder);
|
||||
// state = immutable.set(state, 'selectedFolderId', folder.id);
|
||||
// return state;
|
||||
|
||||
// case 'DELETE_FOLDER':
|
||||
|
||||
// var folders = deepcopy(state.folders);
|
||||
// var index = folderIndex(state, action.id);
|
||||
// if (index < 0) return state;
|
||||
// folders.splice(index, 1);
|
||||
// return immutable.set(state, 'folders', folders);
|
||||
|
||||
// case 'SET_SELECTED_FOLDER':
|
||||
|
||||
// return immutable.set(state, 'selectedFolderId', action.id);
|
||||
|
||||
// case 'SET_FOLDER_NAME':
|
||||
|
||||
// var idx = folderIndex(state, action.id);
|
||||
// return immutable.set(state, 'folders.' + idx + '.name', action.name);
|
||||
|
||||
// case 'ADD_TODO':
|
||||
// var items = [].concat(state.todo.items);
|
||||
// return Object.assign({}, state, {
|
||||
// todo: {
|
||||
// items: items.concat([{
|
||||
// message: action.message,
|
||||
// completed: false
|
||||
// }])
|
||||
// }
|
||||
// });
|
||||
|
||||
// case 'COMPLETE_TODO':
|
||||
// var items = [].concat(state.todo.items);
|
||||
|
||||
// items[action.index].completed = true;
|
||||
|
||||
// return Object.assign({}, state, {
|
||||
// todo: {
|
||||
// items: items
|
||||
// }
|
||||
// });
|
||||
|
||||
// case 'DELETE_TODO':
|
||||
// var items = [].concat(state.todo.items);
|
||||
|
||||
// items.splice(action.index, 1);
|
||||
|
||||
// return Object.assign({}, state, {
|
||||
// todo: {
|
||||
// items: items
|
||||
// }
|
||||
// });
|
||||
|
||||
// case 'CLEAR_TODO':
|
||||
// return Object.assign({}, state, {
|
||||
// todo: {
|
||||
// items: []
|
||||
// }
|
||||
// });
|
||||
|
||||
// default:
|
||||
// return state;
|
||||
// }
|
||||
// }
|
||||
|
||||
// var store = createStore(todoApp, defaultState);
|
||||
|
||||
// class AddTodoForm extends React.Component {
|
||||
// state = {
|
||||
// message: ''
|
||||
// };
|
||||
|
||||
// onFormSubmit(e) {
|
||||
// e.preventDefault();
|
||||
// store.dispatch(addTodo(this.state.message));
|
||||
// this.setState({ message: '' });
|
||||
// }
|
||||
|
||||
// onMessageChanged(e) {
|
||||
// var message = e.target.value;
|
||||
// this.setState({ message: message });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <form onSubmit={this.onFormSubmit.bind(this)}>
|
||||
// <input type="text" placeholder="Todo..." onChange={this.onMessageChanged.bind(this)} value={this.state.message} />
|
||||
// <input type="submit" value="Add" />
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class AddFolderForm extends React.Component {
|
||||
// state = {
|
||||
// name: ''
|
||||
// };
|
||||
|
||||
// onFormSubmit(e) {
|
||||
// e.preventDefault();
|
||||
// store.dispatch({
|
||||
// type: 'ADD_FOLDER',
|
||||
// name: this.state.name
|
||||
// });
|
||||
// this.setState({ name: '' });
|
||||
// }
|
||||
|
||||
// onInputChange(e) {
|
||||
// this.setState({ name: e.target.value });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <form onSubmit={this.onFormSubmit.bind(this)}>
|
||||
// <input type="text" placeholder="Folder..." onChange={this.onInputChange.bind(this)} value={this.state.name} />
|
||||
// <input type="submit" value="Add" />
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class TodoItem extends React.Component {
|
||||
// onDeleteClick() {
|
||||
// store.dispatch(deleteTodo(this.props.index));
|
||||
// }
|
||||
|
||||
// onCompletedClick() {
|
||||
// store.dispatch(completeTodo(this.props.index));
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <li>
|
||||
// <a href="#" onClick={this.onCompletedClick.bind(this)} style={{textDecoration: this.props.completed ? 'line-through' : 'none'}}>{this.props.message.trim()}</a>
|
||||
// <a href="#" onClick={this.onDeleteClick.bind(this)} style={{textDecoration: 'none'}}>[x]</a>
|
||||
// </li>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class FolderItem extends React.Component {
|
||||
// onDeleteClick() {
|
||||
// store.dispatch({
|
||||
// type: 'DELETE_FOLDER',
|
||||
// id: this.props.item.id
|
||||
// });
|
||||
// }
|
||||
|
||||
// onSelected() {
|
||||
// store.dispatch({
|
||||
// type: 'SET_SELECTED_FOLDER',
|
||||
// id: this.props.item.id
|
||||
// });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// let selectedClass = this.props.selected ? 'selected' : '';
|
||||
// return (
|
||||
// <li>
|
||||
// <a href="#" className={selectedClass} onClick={this.onSelected.bind(this)}>{this.props.item.name} ({this.props.item.id})</a> <a href="#" onClick={this.onDeleteClick.bind(this)}>[x]</a>
|
||||
// </li>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class FolderList extends React.Component {
|
||||
// state = {
|
||||
// folders: [],
|
||||
// selectedFolderId: null,
|
||||
// folderName: '',
|
||||
// lastSelectedFolderId: null
|
||||
// };
|
||||
|
||||
// componentWillMount() {
|
||||
// store.subscribe(() => {
|
||||
// var state = store.getState();
|
||||
// this.setState({
|
||||
// folders: state.folders,
|
||||
// selectedFolderId: state.selectedFolderId
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// folderNameInput_keyPress(e) {
|
||||
// if (e.key == 'Enter') {
|
||||
// console.info(this.state.folderName);
|
||||
// store.dispatch({
|
||||
// type: 'SET_FOLDER_NAME',
|
||||
// name: this.state.folderName,
|
||||
// id: this.state.selectedFolderId
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// folderNameInput_onChange(e) {
|
||||
// this.setState({ folderName: e.target.value });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// var items = [];
|
||||
|
||||
// this.state.folders.forEach((item, index) => {
|
||||
// let isSelected = this.state.selectedFolderId == item.id;
|
||||
// items.push(
|
||||
// <FolderItem
|
||||
// key={index}
|
||||
// index={index}
|
||||
// item={item}
|
||||
// selected={isSelected} />
|
||||
// );
|
||||
// });
|
||||
|
||||
// if (!items.length) {
|
||||
// return (
|
||||
// <p>
|
||||
// <i>No folder.</i>
|
||||
// </p>
|
||||
// );
|
||||
// }
|
||||
|
||||
// var selectedFolder = folderById(this.state, this.state.selectedFolderId);
|
||||
// var selectedFolderId = selectedFolder ? selectedFolder.id : null;
|
||||
// if (selectedFolderId !== this.state.lastSelectedFolderId) {
|
||||
// this.state.folderName = selectedFolder ? selectedFolder.name : '';
|
||||
// this.state.lastSelectedFolderId = selectedFolderId;
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <ol>{ items }</ol>
|
||||
// <input type="text" onKeyPress={this.folderNameInput_keyPress.bind(this)} onChange={this.folderNameInput_onChange.bind(this)} value={this.state.folderName} />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// class TodoList extends React.Component {
|
||||
// state = {
|
||||
// items: []
|
||||
// };
|
||||
|
||||
// componentWillMount() {
|
||||
// store.subscribe(() => {
|
||||
// var state = store.getState();
|
||||
// this.setState({
|
||||
// items: state.todo.items
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// var items = [];
|
||||
|
||||
// this.state.items.forEach((item, index) => {
|
||||
// items.push(<TodoItem
|
||||
// key={index}
|
||||
// index={index}
|
||||
// message={item.message}
|
||||
// completed={item.completed}
|
||||
// />);
|
||||
// });
|
||||
|
||||
// if (!items.length) {
|
||||
// return (
|
||||
// <p>
|
||||
// <i>Please add something to do.</i>
|
||||
// </p>
|
||||
// );
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <ol>{ items }</ol>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// ReactDOM.render(
|
||||
// <div>
|
||||
// <h1>Todo</h1>
|
||||
// <AddTodoForm />
|
||||
// <AddFolderForm />
|
||||
// <FolderList />
|
||||
// <TodoList />
|
||||
// </div>,
|
||||
// document.getElementById('container')
|
||||
// );
|
||||
|
||||
// store.dispatch({
|
||||
// type: 'ADD_FOLDER',
|
||||
// name: 'aaaa'
|
||||
// });
|
||||
|
||||
// store.dispatch({
|
||||
// type: 'ADD_FOLDER',
|
||||
// name: 'bbbb'
|
||||
// });
|
||||
|
||||
// store.dispatch({
|
||||
// type: 'ADD_FOLDER',
|
||||
// name: 'cccc'
|
||||
// });
|
39
spa_client/src/js/components/folder-list.jsx
Executable file
39
spa_client/src/js/components/folder-list.jsx
Executable file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Folder from './folder.jsx';
|
||||
import { connect } from 'react-redux';
|
||||
import * as fi from 'models/folder-item.jsx';
|
||||
|
||||
class FolderListComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
let elements = [];
|
||||
let level = Number(this.props.level) + 1;
|
||||
let className = 'level-' + level;
|
||||
className += ' folder-list';
|
||||
|
||||
this.props.items.forEach((item, index) => {
|
||||
if (this.props.parentId != item.parent_id) return;
|
||||
let selected = this.props.selectedFolderId == item.id;
|
||||
let children = fi.children(this.props.items, item.id);
|
||||
elements.push(<Folder level={level} title={item.title} key={item.id} id={item.id} expandedFolderIds={this.props.expandedFolderIds} selectedFolderId={this.props.selectedFolderId} children={children} />);
|
||||
});
|
||||
|
||||
return <div className={className}>{elements}</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = function(state) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = function(dispatch) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const FolderList = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FolderListComponent)
|
||||
|
||||
export default FolderList
|
50
spa_client/src/js/components/folder.jsx
Executable file
50
spa_client/src/js/components/folder.jsx
Executable file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux'
|
||||
import FolderList from './folder-list.jsx';
|
||||
|
||||
class FolderComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
let selectedClass = this.props.selectedFolderId == this.props.id ? 'selected' : '';
|
||||
let elements = [];
|
||||
let key = 'note-name-' + this.props.id;
|
||||
|
||||
elements.push(
|
||||
<div key={key} onClick={this.props.onClick.bind(this)} className={selectedClass} id="{this.props.id}">{this.props.title}</div>
|
||||
);
|
||||
|
||||
var showChildren = this.props.children.length && this.props.expandedFolderIds.indexOf(this.props.id) >= 0;
|
||||
|
||||
if (showChildren) {
|
||||
key = 'folder-list-' + this.props.id;
|
||||
elements.push(
|
||||
<FolderList key={key} level={this.props.level} parentId={this.props.id} items={this.props.children} selectedFolderId={this.props.selectedFolderId} />
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{elements}</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Folder = connect(
|
||||
function(state) { return {} },
|
||||
|
||||
function(dispatch) {
|
||||
return {
|
||||
onClick: function(event) {
|
||||
dispatch({
|
||||
type: 'SELECT_FOLDER',
|
||||
id: this.props.id,
|
||||
});
|
||||
dispatch({
|
||||
type: 'TOGGLE_FOLDER',
|
||||
id: this.props.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
)(FolderComponent)
|
||||
|
||||
export default Folder
|
11
spa_client/src/js/components/my-button.js
Executable file
11
spa_client/src/js/components/my-button.js
Executable file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
class MyButton extends React.Component {
|
||||
|
||||
render() {
|
||||
return <button onClick={this.props.onClick}>{this.props.label}</button>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MyButton
|
28
spa_client/src/js/components/note-list-item.jsx
Executable file
28
spa_client/src/js/components/note-list-item.jsx
Executable file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
class NoteListItemComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
let className = this.props.selected ? 'selected' : '';
|
||||
return <div onClick={this.props.onClick.bind(this)} className={className}>{this.props.title}</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const NoteListItem = connect(
|
||||
function(state) { return {} },
|
||||
function(dispatch) {
|
||||
return {
|
||||
onClick: function(event) {
|
||||
console.info(this.props.id);
|
||||
dispatch({
|
||||
type: 'SELECT_NOTE',
|
||||
id: this.props.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
)(NoteListItemComponent)
|
||||
|
||||
export default NoteListItem
|
37
spa_client/src/js/components/note-list.jsx
Executable file
37
spa_client/src/js/components/note-list.jsx
Executable file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import NoteListItem from './note-list-item.jsx';
|
||||
import { connect } from 'react-redux'
|
||||
import * as fi from 'models/folder-item.jsx';
|
||||
|
||||
class NoteListComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
let elements = [];
|
||||
|
||||
this.props.items.forEach((item, index) => {
|
||||
let selected = this.props.selectedNoteId == item.id;
|
||||
elements.push(<NoteListItem selected={selected} key={item.id} title={item.title} body={item.body} id={item.id} />);
|
||||
});
|
||||
|
||||
return <div className="note-list">{elements}</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = function(state) {
|
||||
return {
|
||||
items: fi.notes(state.items, state.selectedFolderId),
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = function(dispatch) {
|
||||
return { }
|
||||
}
|
||||
|
||||
const NoteList = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(NoteListComponent)
|
||||
|
||||
export default NoteList
|
31
spa_client/src/js/components/root-folder-list.jsx
Executable file
31
spa_client/src/js/components/root-folder-list.jsx
Executable file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import FolderList from './folder-list.jsx';
|
||||
import { connect } from 'react-redux';
|
||||
import * as fi from 'models/folder-item.jsx';
|
||||
|
||||
class RootFolderListComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
return <FolderList expandedFolderIds={this.props.expandedFolderIds} level="0" parentId="0" items={this.props.items} selectedFolderId={this.props.selectedFolderId} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = function(state) {
|
||||
return {
|
||||
items: fi.folders(state.items),
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
expandedFolderIds: state.expandedFolderIds,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = function(dispatch) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const RootFolderList = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(RootFolderListComponent)
|
||||
|
||||
export default RootFolderList
|
42
spa_client/src/js/models/folder-item.jsx
Executable file
42
spa_client/src/js/models/folder-item.jsx
Executable file
@ -0,0 +1,42 @@
|
||||
export function rootItems(items) {
|
||||
var output = [];
|
||||
items.forEach((item, index) => {
|
||||
if (!item.parent_id) return;
|
||||
output.push(item);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
export function folders(items) {
|
||||
var output = [];
|
||||
items.forEach((item, index) => {
|
||||
if (item.type != 1) return;
|
||||
output.push(item);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
export function notes(items, folderId) {
|
||||
var output = [];
|
||||
items.forEach((item, index) => {
|
||||
if (item.type != 2) return;
|
||||
if (item.parent_id != folderId) return;
|
||||
output.push(item);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
export function byId(items, id) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id == id) return items[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function children(items, id) {
|
||||
var output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].parent_id == id) output.push(items[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
32
spa_client/src/js/reducer.jsx
Executable file
32
spa_client/src/js/reducer.jsx
Executable file
@ -0,0 +1,32 @@
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
export function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'SELECT_FOLDER':
|
||||
|
||||
var state = deepcopy(state);
|
||||
state.selectedFolderId = action.id;
|
||||
return state;
|
||||
|
||||
case 'SELECT_NOTE':
|
||||
|
||||
var state = deepcopy(state);
|
||||
state.selectedNoteId = action.id;
|
||||
return state;
|
||||
|
||||
case 'TOGGLE_FOLDER':
|
||||
|
||||
var state = deepcopy(state);
|
||||
var idx = state.expandedFolderIds.indexOf(action.id);
|
||||
if (idx < 0) {
|
||||
state.expandedFolderIds.push(action.id);
|
||||
} else {
|
||||
state.expandedFolderIds.splice(idx, 1);
|
||||
}
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
128
spa_client/src/js/simple-example.js
Executable file
128
spa_client/src/js/simple-example.js
Executable file
@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { render } from 'react-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { createStore } from 'redux';
|
||||
import immutable from 'object-path-immutable';
|
||||
import { Provider } from 'react-redux'
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
// This is the default state of the application. Each
|
||||
// application has only one state, which is an object
|
||||
// with one or more properties.
|
||||
|
||||
let defaultState = {
|
||||
'myButtonLabel': 'click'
|
||||
}
|
||||
|
||||
// The reducer is what processes the actions of the application, such as
|
||||
// button clicks, text changes, etc. It takes a state and an action as
|
||||
// input and must return a state. Important: the state must not be modified
|
||||
// directly. Create a copy first (see `deepcopy`), modify it and return it.
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'SET_BUTTON_NAME':
|
||||
|
||||
var state = deepcopy(state);
|
||||
state.myButtonLabel = action.name;
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// The store is what essentially links the reducer to the state.
|
||||
|
||||
let store = createStore(reducer, defaultState)
|
||||
|
||||
// Create the button and input components. Those are regular React components.
|
||||
|
||||
class MyButton extends React.Component {
|
||||
|
||||
render() {
|
||||
return <button onClick={this.props.onClick}>{this.props.label}</button>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MyInput extends React.Component {
|
||||
|
||||
render() {
|
||||
return <input onKeyPress={this.props.onKeyPress} type="text" />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create the connected components. A connected component (often called "container component")
|
||||
// is a react component that has been connected to the application state. The connection
|
||||
// happens by mapping the state properties to the component properties, and by mapping the
|
||||
// dispatches to the component handlers. This allows better separating the view (React
|
||||
// component) from the model/controller (state and actions).
|
||||
|
||||
const mapStateToButtonProps = function(state) {
|
||||
return { label: state.myButtonLabel };
|
||||
}
|
||||
|
||||
const mapDispatchToButtonProps = function(dispatch) {
|
||||
return {
|
||||
onClick: function() {
|
||||
alert('click');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MyConnectedButton = connect(
|
||||
mapStateToButtonProps,
|
||||
mapDispatchToButtonProps
|
||||
)(MyButton)
|
||||
|
||||
const mapStateToInputProps = function(state) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const mapDispatchToInputProps = function(dispatch) {
|
||||
return {
|
||||
onKeyPress(e) {
|
||||
if (e.key == 'Enter') {
|
||||
dispatch({
|
||||
type: 'SET_BUTTON_NAME',
|
||||
name: e.target.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MyConnectionInput = connect(
|
||||
mapStateToInputProps,
|
||||
mapDispatchToInputProps
|
||||
)(MyInput)
|
||||
|
||||
// Create the application. Note that we display the Connected components,
|
||||
// which in turn include the Rect components.
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<MyConnectedButton />
|
||||
<MyConnectionInput />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Render the application via the <Provider> tag. This is a special React-Redux
|
||||
// component that "magically" links the store to the application and components.
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('container')
|
||||
)
|
38
spa_client/webpack.config.js
Executable file
38
spa_client/webpack.config.js
Executable file
@ -0,0 +1,38 @@
|
||||
var path = require('path');
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
quiet: true,
|
||||
entry: [
|
||||
'./src/js/app.jsx'
|
||||
],
|
||||
output: {
|
||||
path: __dirname + '/dist/js/',
|
||||
filename: 'app.js',
|
||||
publicPath: '/js/'
|
||||
},
|
||||
module: {
|
||||
loaders: [{
|
||||
test: /\.(jsx|js)$/,
|
||||
loaders: ['babel'],
|
||||
include: path.join(__dirname, './src/js/')
|
||||
}]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
components: path.resolve(__dirname, './src/js/components'),
|
||||
models: path.resolve(__dirname, './src/js/models'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// new webpack.optimize.UglifyJsPlugin({
|
||||
// minimize: true,
|
||||
// compress: {
|
||||
// warnings: false
|
||||
// }
|
||||
// }),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"'
|
||||
})
|
||||
]
|
||||
};
|
32
spa_client/webpack.dev.config.js
Executable file
32
spa_client/webpack.dev.config.js
Executable file
@ -0,0 +1,32 @@
|
||||
// NOT USED
|
||||
|
||||
var path = require('path');
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
'./src/js/app'
|
||||
],
|
||||
target: 'web',
|
||||
devtool: 'eval-source-map',
|
||||
output: {
|
||||
path: __dirname + '/src/js/',
|
||||
filename: 'app.min.js',
|
||||
publicPath: '/js/'
|
||||
},
|
||||
module: {
|
||||
loaders: [{
|
||||
test: /\.js$/,
|
||||
loaders: ['babel'],
|
||||
include: path.join(__dirname, './src/js/')
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
'Promise': 'es6-promise'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"'
|
||||
})
|
||||
]
|
||||
};
|
9
src/AppBundle/ApiSerializer.php
Executable file
9
src/AppBundle/ApiSerializer.php
Executable file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle;
|
||||
|
||||
class ApiSerializer {
|
||||
|
||||
|
||||
|
||||
}
|
84
src/AppBundle/Command/BuildMimeTypeArrayCommand.php
Executable file
84
src/AppBundle/Command/BuildMimeTypeArrayCommand.php
Executable file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
|
||||
use AppBundle\Paths;
|
||||
|
||||
class BuildMimeTypeArrayCommand extends ContainerAwareCommand {
|
||||
|
||||
protected function configure() {
|
||||
$this->setName('app:build-mime-type-array');
|
||||
$this->setDescription('Build MIME type PHP array from Apache mime.types file.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$paths = $this->getContainer()->get('app.paths');
|
||||
|
||||
$sourcePath = $paths->dataDir() . '/mime.types';
|
||||
$targetPath = $paths->dataDir() . '/mime_types.php';
|
||||
$sqlitePath = $paths->dataDir() . '/mime_types.sqlite.sql';
|
||||
|
||||
if (file_exists($targetPath)) {
|
||||
// TODO: (or not)
|
||||
// To update the existing PHP file, it needs to be loaded first and the IDs need to remain in place
|
||||
// since they might be use in the database, etc., then any new type can be added.
|
||||
// This is easy to implement, but most likely YAGNI.
|
||||
throw new \Exception(sprintf('MimeType PHP file already exists at %s and the code to update it as not been implemented.', $targetPath));
|
||||
}
|
||||
|
||||
$lines = file_get_contents($sourcePath);
|
||||
if ($lines === false) throw new \Exception('Cannot read ' . $sourcePath);
|
||||
$lines = explode("\n", $lines);
|
||||
|
||||
$mimeTypes = array();
|
||||
$id = 1;
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (!$line) continue;
|
||||
if ($line[0] == '#') continue;
|
||||
$tokens = explode("\t", $line);
|
||||
if (count($tokens) < 2) continue;
|
||||
|
||||
$mimeType = trim($tokens[0]);
|
||||
if (!$mimeType) continue; // Shouldn't happen
|
||||
$extensions = explode(' ', $tokens[count($tokens) - 1]);
|
||||
|
||||
$mimeTypes[$id] = array(
|
||||
'id' => $id,
|
||||
't' => $mimeType,
|
||||
'e' => $extensions,
|
||||
);
|
||||
|
||||
$id++;
|
||||
}
|
||||
|
||||
// CREATE TABLE mimetypes (
|
||||
// id INT,
|
||||
// mime TEXT
|
||||
// );
|
||||
|
||||
// CREATE TABLE mimetype_extensions (
|
||||
// id INTEGER PRIMARY KEY,
|
||||
// mimetype_id,
|
||||
// extension TEXT
|
||||
// );
|
||||
|
||||
// foreach ($mimeTypes[$id]
|
||||
|
||||
$content = var_export($mimeTypes, true);
|
||||
$content = str_replace(' ', '', $content);
|
||||
$content = str_replace("\n", '', $content);
|
||||
$content = str_replace(',)', ')', $content);
|
||||
$content = '<?php return ' . $content . ';';
|
||||
|
||||
$ok = file_put_contents($targetPath, $content);
|
||||
if ($ok === false) throw new \Exception('Could not write to ' . $targetPath);
|
||||
|
||||
$output->writeln(sprintf('File created at "%s"', $targetPath));
|
||||
}
|
||||
|
||||
}
|
@ -103,6 +103,11 @@ abstract class ApiController extends Controller {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
protected function userId() {
|
||||
$u = $this->user();
|
||||
return $u ? $u->id : 0;
|
||||
}
|
||||
|
||||
protected function aclCheck($resource) {
|
||||
if (!is_array($resource)) $resource = array($resource);
|
||||
$user = $this->user();
|
||||
|
61
src/AppBundle/Controller/FilesController.php
Executable file
61
src/AppBundle/Controller/FilesController.php
Executable file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Controller;
|
||||
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use AppBundle\Controller\ApiController;
|
||||
use AppBundle\Model\BaseModel;
|
||||
use AppBundle\Model\File;
|
||||
use AppBundle\Exception\ValidationException;
|
||||
|
||||
class FilesController extends ApiController {
|
||||
|
||||
/**
|
||||
* @Route("/files")
|
||||
*/
|
||||
public function allAction(Request $request) {
|
||||
if ($request->isMethod('POST')) {
|
||||
if (!isset($_FILES['file'])) throw new ValidationException('Missing "file" parameter');
|
||||
|
||||
$file = new File();
|
||||
$file->moveUploadedFile($_FILES['file']);
|
||||
$file->owner_id = $this->userId();
|
||||
$file->save();
|
||||
|
||||
return static::successResponse($file);
|
||||
}
|
||||
|
||||
return static::errorResponse('Invalid method');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/files/{id}")
|
||||
*/
|
||||
public function oneAction($id, Request $request) {
|
||||
$file = File::find(File::unhex($id));
|
||||
if (!$file) return static::errorResponse('Not found', 0, 404);
|
||||
|
||||
if ($request->isMethod('GET')) {
|
||||
return static::successResponse($file);
|
||||
}
|
||||
|
||||
if ($request->isMethod('PATCH')) {
|
||||
$data = $this->patchParameters();
|
||||
foreach ($data as $n => $v) {
|
||||
$file->{$n} = $v;
|
||||
}
|
||||
$file->save();
|
||||
return static::successResponse($file);
|
||||
}
|
||||
|
||||
if ($request->isMethod('DELETE')) {
|
||||
$file->delete();
|
||||
return static::successResponse();
|
||||
}
|
||||
|
||||
return static::errorResponse('Invalid method');
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ use AppBundle\Controller\ApiController;
|
||||
use AppBundle\Model\User;
|
||||
use AppBundle\Model\Session;
|
||||
use AppBundle\Model\Change;
|
||||
use AppBundle\Model\FolderItem;
|
||||
use AppBundle\Model\BaseItem;
|
||||
use AppBundle\Exception\ValidationException;
|
||||
|
||||
|
||||
@ -127,45 +127,45 @@ class UsersController extends ApiController {
|
||||
// var_dump($diff2);
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->delta = 'salut ca va';
|
||||
// $change->save();
|
||||
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut, ça va ? oui très bien');
|
||||
// $change->save();
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut - oui très bien');
|
||||
// $change->save();
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut, ça va ? oui bien');
|
||||
// $change->save();
|
||||
|
||||
|
||||
|
||||
$d = Change::fullFieldText(FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), FolderItem::enumId('field', 'title'));
|
||||
$d = Change::fullFieldText(BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), BaseItem::enumId('field', 'title'));
|
||||
var_dump($d);die();
|
||||
|
||||
|
||||
|
@ -6,21 +6,26 @@ class Eloquent {
|
||||
|
||||
private $capsule_ = null;
|
||||
|
||||
public function __construct() {
|
||||
public function __construct($mimeTypes, $paths) {
|
||||
$this->capsule_ = new \Illuminate\Database\Capsule\Manager();
|
||||
|
||||
$this->capsule_->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => '127.0.0.1',
|
||||
'host' => 'localhost',
|
||||
'database' => 'notes',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'password' => 'pass',
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$this->capsule_->bootEloquent();
|
||||
|
||||
// In order to keep things lightweight, the models aren't part of Symfony dependency
|
||||
// injection framework, so any service required by a model needs to be injected here.
|
||||
Model\File::$mimeTypes = $mimeTypes;
|
||||
Model\File::$paths = $paths;
|
||||
}
|
||||
|
||||
public function connection() {
|
||||
|
84
src/AppBundle/MimeTypes.php
Executable file
84
src/AppBundle/MimeTypes.php
Executable file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle;
|
||||
|
||||
class MimeTypes {
|
||||
|
||||
private $mimeTypes_ = null;
|
||||
private $paths_ = null;
|
||||
private $defaultMimeType_ = 'application/octet-stream';
|
||||
private $defaultMimeTypeId_ = null;
|
||||
|
||||
public function __construct($paths) {
|
||||
$this->paths_ = $paths;
|
||||
}
|
||||
|
||||
// Get the mime type from the file extension, if any, or from
|
||||
// the file content. The second check will be skipped if the
|
||||
// file doesn't exists.
|
||||
public function idFromPath($filePath) {
|
||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
$r = $this->extensionToMimeTypeId($ext);
|
||||
if ($r !== $this->defaultMimeTypeId()) return $r;
|
||||
|
||||
// Else try to get the mime type from the binary content
|
||||
if (!file_exists($filePath)) return $this->defaultMimeTypeId();
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$r = finfo_file($finfo, $filePath);
|
||||
finfo_close($finfo);
|
||||
return $r === false ? $this->defaultMimeTypeId() : $this->stringToId($r);
|
||||
}
|
||||
|
||||
private function loadMimeTypes() {
|
||||
if ($this->mimeTypes_) return;
|
||||
$path = $this->paths_->dataDir() . '/mime_types.php';
|
||||
if (!file_exists($path)) throw new \Exception(sprintf('File not found: "%s"', $path));
|
||||
$this->mimeTypes_ = require $path;
|
||||
}
|
||||
|
||||
public function defaultMimeTypeId() {
|
||||
if ($this->defaultMimeTypeId_) return $this->defaultMimeTypeId_;
|
||||
$this->loadMimeTypes();
|
||||
$this->defaultMimeTypeId_ = $this->stringToId($this->defaultMimeType_);
|
||||
return $this->defaultMimeTypeId_;
|
||||
}
|
||||
|
||||
public function defaultMimeType() {
|
||||
return $this->defaultMimeType_;
|
||||
}
|
||||
|
||||
public function idToString($id) {
|
||||
$this->loadMimeTypes();
|
||||
$id = (int)$id;
|
||||
if (!isset($this->mimeTypes_[$id])) throw new \Exception(sprintf('Invalid MIME type ID: %s', $id));
|
||||
return $this->mimeTypes_[$id]['t'];
|
||||
}
|
||||
|
||||
public function stringToId($mimeType) {
|
||||
$this->loadMimeTypes();
|
||||
$mimeType = strtolower(trim($mimeType));
|
||||
if (!$mimeType) return $this->defaultMimeTypeId();
|
||||
|
||||
foreach ($this->mimeTypes_ as $id => $o) {
|
||||
if ($o['t'] == $mimeType) return $id;
|
||||
}
|
||||
|
||||
return $this->defaultMimeTypeId();
|
||||
}
|
||||
|
||||
private function extensionToMimeTypeId($ext) {
|
||||
$this->loadMimeTypes();
|
||||
$ext = strtolower(trim($ext));
|
||||
if (!$ext) return $this->defaultMimeTypeId();
|
||||
|
||||
foreach ($this->mimeTypes_ as $id => $o) {
|
||||
foreach ($o['e'] as $e) {
|
||||
if ($ext == $e) return $id;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->defaultMimeTypeId();
|
||||
}
|
||||
|
||||
}
|
45
src/AppBundle/Model/BaseItem.php
Executable file
45
src/AppBundle/Model/BaseItem.php
Executable file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class BaseItem extends BaseModel {
|
||||
|
||||
public $useUuid = true;
|
||||
public $incrementing = false;
|
||||
|
||||
static protected $enums = array(
|
||||
'type' => array('folder', 'note', 'todo', 'tag'),
|
||||
);
|
||||
|
||||
public function itemTypeId() {
|
||||
$typeName = null;
|
||||
switch (get_called_class()) {
|
||||
case 'AppBundle\Model\Folder':
|
||||
$typeName = 'folder';
|
||||
break;
|
||||
case 'AppBundle\Model\Note':
|
||||
$typeName = 'note';
|
||||
break;
|
||||
case 'AppBundle\Model\Tag':
|
||||
$typeName = 'tag';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$typeName) throw new \Exception('Unknown item class: ' . get_called_class());
|
||||
|
||||
return self::enumId('type', $typeName);
|
||||
}
|
||||
|
||||
static public function byId($itemTypeId, $itemId) {
|
||||
if ($itemTypeId == BaseItem::enumId('type', 'folder')) {
|
||||
return Folder::where('id', '=', $itemId)->first();
|
||||
} else if ($itemTypeId == BaseItem::enumId('type', 'note')) {
|
||||
return Note::where('id', '=', $itemId)->first();
|
||||
} else if ($itemTypeId == BaseItem::enumId('type', 'tag')) {
|
||||
return Tag::where('id', '=', $itemId)->first();
|
||||
}
|
||||
|
||||
throw new \Exception('Unsupported item type: ' . $itemTypeId);
|
||||
}
|
||||
|
||||
}
|
@ -157,7 +157,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
}
|
||||
|
||||
if (isset($output['item_type'])) {
|
||||
$output['item_type'] = FolderItem::enumName('type', $output['item_type'], true);
|
||||
$output['item_type'] = BaseItem::enumName('type', $output['item_type'], true);
|
||||
}
|
||||
|
||||
if (isset($output['item_field'])) {
|
||||
@ -311,8 +311,12 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
$this->isNew = $v;
|
||||
}
|
||||
|
||||
public function isNew() {
|
||||
return !$this->id || $this->isNew === true;
|
||||
}
|
||||
|
||||
public function save(Array $options = array()) {
|
||||
$isNew = !$this->id || $this->isNew === true;
|
||||
$isNew = $this->isNew();
|
||||
|
||||
if ($this->useUuid && $isNew && !$this->id) $this->id = self::createId();
|
||||
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
|
||||
@ -320,6 +324,8 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
|
||||
parent::save($options);
|
||||
|
||||
$this->isNew = null;
|
||||
|
||||
if (count($this->versionedFields)) {
|
||||
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
|
||||
}
|
||||
@ -364,7 +370,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->owner_id;
|
||||
$change->client_id = static::clientId();
|
||||
$change->item_type = FolderItem::enumId('type', $this->classItemTypeName());
|
||||
$change->item_type = BaseItem::enumId('type', $this->classItemTypeName());
|
||||
$change->type = Change::enumId('type', $type);
|
||||
$change->item_id = $this->id;
|
||||
return $change;
|
||||
|
52
src/AppBundle/Model/File.php
Executable file
52
src/AppBundle/Model/File.php
Executable file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class File extends BaseModel {
|
||||
|
||||
static public $mimeTypes = null;
|
||||
static public $paths = null;
|
||||
|
||||
public $useUuid = true;
|
||||
public $incrementing = false;
|
||||
|
||||
public function toPublicArray() {
|
||||
$output = parent::toPublicArray();
|
||||
$output['mime_type'] = self::$mimeTypes->idToString($this->mime_type);
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
@unlink($this->path());
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
public function path() {
|
||||
return self::$paths->uploadsDir() . '/' . BaseModel::hex($this->id);
|
||||
}
|
||||
|
||||
static public function pathForId($id) {
|
||||
return self::$paths->uploadsDir() . '/' . $id;
|
||||
}
|
||||
|
||||
public function moveUploadedFile($file) {
|
||||
if (isset($file['error']) && $file['error']) throw new \Exception('Cannot upload file: ' . $file['error']);
|
||||
|
||||
$filePath = $file['tmp_name'];
|
||||
$originalName = $file['name'];
|
||||
$id = BaseModel::createId();
|
||||
|
||||
$targetPath = self::$paths->uploadsDir() . '/' . BaseModel::hex($id);
|
||||
if (file_exists($targetPath)) throw new \Exception('Hash collision'); // Shouldn't happen
|
||||
|
||||
if (!@move_uploaded_file($filePath, $targetPath)) throw new \Exception('Unknown error - file could not be uploaded.');
|
||||
|
||||
$this->id = $id;
|
||||
$this->title = $originalName;
|
||||
$this->original_name = $originalName;
|
||||
$this->mime_type = self::$mimeTypes->idFromPath($targetPath);
|
||||
|
||||
$this->setIsNew(true);
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class Folder extends FolderItem {
|
||||
class Folder extends BaseItem {
|
||||
|
||||
protected $versionedFields = array('title');
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class Note extends FolderItem {
|
||||
class Note extends BaseItem {
|
||||
|
||||
protected $versionedFields = array('title', 'body');
|
||||
|
||||
|
78
src/AppBundle/Model/Tag.php
Executable file
78
src/AppBundle/Model/Tag.php
Executable file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class Tag extends BaseItem {
|
||||
|
||||
// A star has the exact same behaviour as a tag, except
|
||||
// it might be diplayed differently.
|
||||
static private $starTag_ = null;
|
||||
|
||||
static public function starTag($ownerId) {
|
||||
if (self::$starTag_) return self::$starTag_;
|
||||
$t = Tag::where('internal', '=', 1)
|
||||
->where('title', '=', 'star')
|
||||
->first();
|
||||
|
||||
if (!$t) {
|
||||
$t = new Tag();
|
||||
$t->title = 'star';
|
||||
$t->internal = 1;
|
||||
$t->owner_id = $ownerId;
|
||||
$t->save();
|
||||
}
|
||||
|
||||
self::$starTag_ = $t;
|
||||
return self::$starTag_;
|
||||
}
|
||||
|
||||
static public function star($item) {
|
||||
self::starTag($item->owner_id)->add($item);
|
||||
}
|
||||
|
||||
static public function unstar($item) {
|
||||
self::starTag($item->owner_id)->remove($item);
|
||||
}
|
||||
|
||||
static public function isStarred($item) {
|
||||
return self::starTag($item->owner_id)->includes($item);
|
||||
}
|
||||
|
||||
static public function starredItems($ownerId) {
|
||||
return self::starTag($ownerId)->items();
|
||||
}
|
||||
|
||||
public function add($item) {
|
||||
if ($this->includes($item)) return;
|
||||
|
||||
$t = new Tagged_item();
|
||||
$t->tag_id = $this->id;
|
||||
$t->item_id = $item->id;
|
||||
$t->item_type = $item->itemTypeId();
|
||||
$t->save();
|
||||
}
|
||||
|
||||
public function includes($item) {
|
||||
return !!Tagged_item::where('item_type', '=', $item->itemTypeId())
|
||||
->where('item_id', '=', $item->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function remove($item) {
|
||||
Tagged_item::where('item_type', '=', $item->itemTypeId())
|
||||
->where('item_id', '=', $item->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// TODO: retrieve items in one SQL query
|
||||
public function items() {
|
||||
$output = array();
|
||||
$taggedItems = Tagged_item::where('tag_id', '=', $this->id)->get();
|
||||
foreach ($taggedItems as $taggedItem) {
|
||||
$item = BaseItem::byId($taggedItem->item_type, $taggedItem->item_id);
|
||||
$output[] = $item;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
7
src/AppBundle/Model/Tagged_item.php
Executable file
7
src/AppBundle/Model/Tagged_item.php
Executable file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Model;
|
||||
|
||||
class Tagged_item extends BaseModel {
|
||||
|
||||
}
|
25
src/AppBundle/Paths.php
Executable file
25
src/AppBundle/Paths.php
Executable file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle;
|
||||
|
||||
class Paths {
|
||||
|
||||
private $rootDir_ = null;
|
||||
|
||||
public function __construct($rootDir) {
|
||||
$this->rootDir_ = $rootDir;
|
||||
}
|
||||
|
||||
public function rootDir() {
|
||||
return $this->rootDir_;
|
||||
}
|
||||
|
||||
public function dataDir() {
|
||||
return $this->rootDir() . '/data';
|
||||
}
|
||||
|
||||
public function uploadsDir() {
|
||||
return $this->dataDir() . '/uploads';
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ CREATE TABLE `folders` (
|
||||
`owner_id` binary(16) NULL default NULL,
|
||||
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||
`encryption_method` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `notes` (
|
||||
@ -14,11 +14,40 @@ CREATE TABLE `notes` (
|
||||
`completed` tinyint(1) NOT NULL default '0',
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
`latitude` DECIMAL(10, 8) NOT NULL default '0',
|
||||
`longitude` DECIMAL(11, 8) NOT NULL default '0',
|
||||
`altitude` DECIMAL(9, 4) NOT NULL default '0',
|
||||
`parent_id` binary(16) NULL default NULL,
|
||||
`owner_id` binary(16),
|
||||
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||
`encryption_method` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
`order` int(11) NOT NULL default '0',
|
||||
`is_todo` tinyint(1) NOT NULL default '0',
|
||||
`todo_due` int(11) NOT NULL default '0',
|
||||
`todo_completed` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `tags` (
|
||||
`id` binary(16) NOT NULL,
|
||||
`title` varchar(1024) NOT NULL default '',
|
||||
`owner_id` binary(16) NOT NULL,
|
||||
`internal` tinyint(1) NOT NULL default '0',
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||
`encryption_method` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `tagged_items` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tag_id` binary(16) NOT NULL,
|
||||
`item_id` binary(16) NOT NULL,
|
||||
`item_type` int(11) NOT NULL,
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `users` (
|
||||
@ -29,7 +58,7 @@ CREATE TABLE `users` (
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
`owner_id` binary(16),
|
||||
PRIMARY KEY (`id`)
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `sessions` (
|
||||
@ -38,7 +67,7 @@ CREATE TABLE `sessions` (
|
||||
`client_id` binary(16),
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `changes` (
|
||||
@ -53,5 +82,18 @@ CREATE TABLE `changes` (
|
||||
`item_field` int(11) NOT NULL default '0',
|
||||
`delta` MEDIUMTEXT,
|
||||
`previous_id` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
||||
CREATE TABLE `files` (
|
||||
`id` binary(16) NOT NULL,
|
||||
`title` varchar(256) NOT NULL default '',
|
||||
`mime_type` int(11) NOT NULL default '0',
|
||||
`original_name` varchar(256) NOT NULL default '',
|
||||
`created_time` int(11) NOT NULL default '0',
|
||||
`updated_time` int(11) NOT NULL default '0',
|
||||
`owner_id` binary(16) NULL default NULL,
|
||||
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||
`encryption_method` int(11) NOT NULL default '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) CHARACTER SET=utf8;
|
||||
|
18
tests/AppBundle/Controller/DefaultControllerTest.php
Executable file
18
tests/AppBundle/Controller/DefaultControllerTest.php
Executable file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\AppBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
public function testIndex()
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
|
||||
$this->assertEquals(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
|
||||
}
|
||||
}
|
25
tests/Model/BaseItemTest.php
Executable file
25
tests/Model/BaseItemTest.php
Executable file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||
|
||||
use AppBundle\Model\BaseItem;
|
||||
use AppBundle\Model\Note;
|
||||
use AppBundle\Model\Folder;
|
||||
use AppBundle\Model\Tag;
|
||||
|
||||
class BaseItemTest extends BaseTestCase {
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testItemType() {
|
||||
$n = new Note();
|
||||
$this->assertEquals(BaseItem::enumId('type', 'note'), $n->itemTypeId());
|
||||
$n = new Folder();
|
||||
$this->assertEquals(BaseItem::enumId('type', 'folder'), $n->itemTypeId());
|
||||
$n = new Tag();
|
||||
$this->assertEquals(BaseItem::enumId('type', 'tag'), $n->itemTypeId());
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||
|
||||
use AppBundle\Model\BaseModel;
|
||||
use AppBundle\Model\FolderItem;
|
||||
use AppBundle\Model\BaseItem;
|
||||
use AppBundle\Model\Note;
|
||||
use AppBundle\Model\Change;
|
||||
|
||||
@ -24,7 +24,7 @@ class ChangeTest extends BaseTestCase {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->user()->id;
|
||||
$change->client_id = $this->clientId();
|
||||
$change->item_type = FolderItem::enumId('type', 'note');
|
||||
$change->item_type = BaseItem::enumId('type', 'note');
|
||||
$change->item_field = BaseModel::enumId('field', 'body');
|
||||
$change->type = Change::enumId('type', 'create');
|
||||
$change->item_id = $itemId;
|
||||
@ -36,7 +36,7 @@ class ChangeTest extends BaseTestCase {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->user()->id;
|
||||
$change->client_id = $this->clientId();
|
||||
$change->item_type = FolderItem::enumId('type', 'note');
|
||||
$change->item_type = BaseItem::enumId('type', 'note');
|
||||
$change->item_field = BaseModel::enumId('field', 'body');
|
||||
$change->type = Change::enumId('type', 'update');
|
||||
$change->item_id = $itemId;
|
||||
@ -62,7 +62,7 @@ class ChangeTest extends BaseTestCase {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->user()->id;
|
||||
$change->client_id = $this->clientId(1);
|
||||
$change->item_type = FolderItem::enumId('type', 'note');
|
||||
$change->item_type = BaseItem::enumId('type', 'note');
|
||||
$change->item_field = BaseModel::enumId('field', 'body');
|
||||
$change->type = Change::enumId('type', 'create');
|
||||
$change->item_id = $itemId;
|
||||
@ -76,7 +76,7 @@ class ChangeTest extends BaseTestCase {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->user()->id;
|
||||
$change->client_id = $this->clientId(2);
|
||||
$change->item_type = FolderItem::enumId('type', 'note');
|
||||
$change->item_type = BaseItem::enumId('type', 'note');
|
||||
$change->item_field = BaseModel::enumId('field', 'body');
|
||||
$change->type = Change::enumId('type', 'update');
|
||||
$change->item_id = $itemId;
|
||||
@ -91,7 +91,7 @@ class ChangeTest extends BaseTestCase {
|
||||
$change = new Change();
|
||||
$change->user_id = $this->user()->id;
|
||||
$change->client_id = $this->clientId(1);
|
||||
$change->item_type = FolderItem::enumId('type', 'note');
|
||||
$change->item_type = BaseItem::enumId('type', 'note');
|
||||
$change->item_field = BaseModel::enumId('field', 'body');
|
||||
$change->type = Change::enumId('type', 'update');
|
||||
$change->item_id = $itemId;
|
||||
|
71
tests/Model/TagTest.php
Executable file
71
tests/Model/TagTest.php
Executable file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||
|
||||
use AppBundle\Model\Note;
|
||||
use AppBundle\Model\Tag;
|
||||
|
||||
class TagTest extends BaseTestCase {
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testTagging() {
|
||||
$note = new Note();
|
||||
$note->save();
|
||||
|
||||
$tag = new Tag();
|
||||
$tag->owner_id = $this->userId();
|
||||
$tag->save();
|
||||
|
||||
$this->assertFalse($tag->includes($note));
|
||||
|
||||
$tag->add($note);
|
||||
$items = $tag->items();
|
||||
$this->assertEquals(1, count($items));
|
||||
|
||||
$tag->add($note);
|
||||
$items = $tag->items();
|
||||
$this->assertEquals(1, count($items));
|
||||
|
||||
$note2 = new Note();
|
||||
$note2->save();
|
||||
|
||||
$tag->add($note2);
|
||||
$items = $tag->items();
|
||||
$this->assertEquals(2, count($items));
|
||||
|
||||
$this->assertEquals($note->id, $items[0]->id);
|
||||
$this->assertEquals($note2->id, $items[1]->id);
|
||||
$this->assertTrue($tag->includes($note));
|
||||
$this->assertTrue($tag->includes($note2));
|
||||
|
||||
$tag->remove($note);
|
||||
$items = $tag->items();
|
||||
$this->assertEquals(1, count($items));
|
||||
|
||||
$tag->remove($note2);
|
||||
$items = $tag->items();
|
||||
$this->assertEquals(0, count($items));
|
||||
}
|
||||
|
||||
public function testStarring() {
|
||||
$note = new Note();
|
||||
$note->owner_id = $this->userId();
|
||||
$note->save();
|
||||
|
||||
$this->assertFalse(Tag::isStarred($note));
|
||||
|
||||
Tag::star($note);
|
||||
$this->assertTrue(Tag::isStarred($note));
|
||||
$this->assertEquals(1, count(Tag::starredItems($this->userId())));
|
||||
|
||||
Tag::star($note);
|
||||
$this->assertEquals(1, count(Tag::starredItems($this->userId())));
|
||||
|
||||
Tag::unstar($note);
|
||||
$this->assertFalse(Tag::isStarred($note));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user