You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Web app update
This commit is contained in:
		
							
								
								
									
										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)); | ||||
| 	} | ||||
| 	 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user