From 2f248a6370bd0b9305e5e9be299d39b03bc6ebe8 Mon Sep 17 00:00:00 2001 From: RemiZOffAlex Date: Tue, 16 Jul 2024 20:57:38 +0300 Subject: [PATCH] Update note, model, page --- src/myapp/acl/README.md | 47 +++ src/myapp/acl/__init__.py | 6 + src/myapp/acl/acl.py | 83 ++++ src/myapp/models/__init__.py | 14 +- src/myapp/models/note/common.py | 15 + src/myapp/models/note/trash.py | 42 +++ src/myapp/models/note/tree.py | 49 +++ src/myapp/models/page/common.py | 15 + src/myapp/models/page/trash.py | 42 +++ src/myapp/models/page/tree.py | 49 +++ src/myapp/ns_api/__init__.py | 1 + src/myapp/ns_api/favorite/__init__.py | 7 + src/myapp/ns_api/favorite/note.py | 110 ++++++ src/myapp/ns_api/favorite/page.py | 110 ++++++ src/myapp/ns_api/note.py | 40 +- src/myapp/templates/components/inc.j2 | 1 + src/myapp/templates/components/info.js | 39 ++ src/myapp/templates/private/components/inc.j2 | 2 + .../private/components/menu-general.js | 6 +- .../templates/private/components/notes.js | 57 +++ .../templates/private/components/tags.js | 356 ++++++++++++++++++ .../private/domains/favorite/favorite.js | 110 ++++++ .../templates/private/domains/favorite/inc.j2 | 15 + .../private/domains/favorite/menu.js | 45 +++ .../private/domains/favorite/notes.js | 120 ++++++ .../private/domains/favorite/pages.js | 120 ++++++ src/myapp/templates/private/domains/inc.j2 | 2 + .../templates/private/domains/note/add.js | 103 +++++ .../templates/private/domains/note/edit.js | 170 +++++++++ .../templates/private/domains/note/inc.j2 | 17 + .../templates/private/domains/note/note.js | 220 +++++++++++ .../templates/private/domains/note/notes.js | 112 ++++++ .../templates/private/domains/note/source.js | 130 +++++++ 33 files changed, 2234 insertions(+), 21 deletions(-) create mode 100644 src/myapp/acl/README.md create mode 100644 src/myapp/acl/__init__.py create mode 100644 src/myapp/acl/acl.py create mode 100644 src/myapp/models/note/trash.py create mode 100644 src/myapp/models/note/tree.py create mode 100644 src/myapp/models/page/trash.py create mode 100644 src/myapp/models/page/tree.py create mode 100644 src/myapp/ns_api/favorite/__init__.py create mode 100644 src/myapp/ns_api/favorite/note.py create mode 100644 src/myapp/ns_api/favorite/page.py create mode 100644 src/myapp/templates/components/info.js create mode 100644 src/myapp/templates/private/components/notes.js create mode 100644 src/myapp/templates/private/components/tags.js create mode 100644 src/myapp/templates/private/domains/favorite/favorite.js create mode 100644 src/myapp/templates/private/domains/favorite/inc.j2 create mode 100644 src/myapp/templates/private/domains/favorite/menu.js create mode 100644 src/myapp/templates/private/domains/favorite/notes.js create mode 100644 src/myapp/templates/private/domains/favorite/pages.js create mode 100644 src/myapp/templates/private/domains/note/add.js create mode 100644 src/myapp/templates/private/domains/note/edit.js create mode 100644 src/myapp/templates/private/domains/note/inc.j2 create mode 100644 src/myapp/templates/private/domains/note/note.js create mode 100644 src/myapp/templates/private/domains/note/notes.js create mode 100644 src/myapp/templates/private/domains/note/source.js diff --git a/src/myapp/acl/README.md b/src/myapp/acl/README.md new file mode 100644 index 0000000..ceb296a --- /dev/null +++ b/src/myapp/acl/README.md @@ -0,0 +1,47 @@ +Удаление неиспользуемых ACL + +В файле specialistoff/acl/acl.py + +``` +# import json +# from pathlib import Path + +... + + # data = {} + # acl_file = Path(app.config['DIR_DATA'] + "/acl.json") + # if acl_file.exists(): + # with open(acl_file, 'r') as fd: + # data = json.load(fd) + # if view_func.__module__ not in data: + # data[view_func.__module__] = [] + # if view_func.__name__ not in data[view_func.__module__]: + # data[view_func.__module__].append(view_func.__name__) + # with open(acl_file, 'w') as fd: + # json.dump(data, fd) +``` + +``` +import json +from pathlib import Path + +from specialistoff import app, lib, models + +data = {} +acl_file = Path(app.config['DIR_DATA'] + "/acl.json") +if acl_file.exists(): + with open(acl_file, 'r') as fd: + data = json.load(fd) + + +for item in data: + resources = models.db_session.query( + models.ACLResource + ).filter( + models.ACLResource.modulename == item + ).filter( + ~models.ACLResource.funcname.in_(data[item]) + ) + if resources.count() > 0: + print(resources.all()) +``` diff --git a/src/myapp/acl/__init__.py b/src/myapp/acl/__init__.py new file mode 100644 index 0000000..ebb13a9 --- /dev/null +++ b/src/myapp/acl/__init__.py @@ -0,0 +1,6 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +from .acl import ACL + +__all__ = ['ACL'] diff --git a/src/myapp/acl/acl.py b/src/myapp/acl/acl.py new file mode 100644 index 0000000..8bebf9f --- /dev/null +++ b/src/myapp/acl/acl.py @@ -0,0 +1,83 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +# import json +# from pathlib import Path + +from flask import abort +from functools import wraps +from .. import app, lib, models + + +class ACL: + """Декоратор для управления доступом + """ + def __init__(self, view_func): + # data = {} + # acl_file = Path(app.config['DIR_DATA'] + "/acl.json") + # if acl_file.exists(): + # with open(acl_file, 'r') as fd: + # data = json.load(fd) + # if view_func.__module__ not in data: + # data[view_func.__module__] = [] + # if view_func.__name__ not in data[view_func.__module__]: + # data[view_func.__module__].append(view_func.__name__) + # with open(acl_file, 'w') as fd: + # json.dump(data, fd) + funcname = view_func.__name__ + modulename = view_func.__module__ + resource = models.db_session.query( + models.ACLResource + ).filter( + models.ACLResource.funcname == funcname, + models.ACLResource.modulename == modulename + ).first() + if resource is None: + resource = models.ACLResource( + modulename=modulename, + funcname=funcname + ) + models.db_session.add(resource) + models.db_session.commit() + self.view_func = view_func + wraps(view_func)(self) + + def __call__(self, *args, **kwargs): + """Текущий объект, на который проверяем наличие прав + """ + resource = models.db_session.query( + models.ACLResource.id + ).filter( + models.ACLResource.funcname == self.view_func.__name__, + models.ACLResource.modulename == self.view_func.__module__ + ).first() + + rule = models.db_session.query( + models.ACLUserRule.permission + ).filter( + models.ACLUserRule.user_id == lib.get_user().id, + models.ACLUserRule.resource_id == resource.id + ).first() + if rule is None: + roles = models.db_session.query( + models.ACLUserRole.role_id + ).filter( + models.ACLUserRole.user_id == lib.get_user().id + ) + rolerule = models.db_session.query( + models.ACLRoleRule + ).filter( + models.ACLRoleRule.role_id.in_(roles), + models.ACLRoleRule.resource_id == resource.id + ).first() + if rolerule is None: + if app.config['DEFAULT_PERMISSION'] == 'deny': + abort(403) + elif rolerule.permission == 'deny': + abort(403) + elif rule.permission == 'deny': + abort(403) + # maybe do something before the view_func call + response = self.view_func(*args, **kwargs) + # maybe do something after the view_func call + return response diff --git a/src/myapp/models/__init__.py b/src/myapp/models/__init__.py index 28bc205..80b21de 100644 --- a/src/myapp/models/__init__.py +++ b/src/myapp/models/__init__.py @@ -32,15 +32,19 @@ from .users import User # noqa F401 # Метки from .tag import Tag # noqa F401 -# Статьи -from .page.common import Page # noqa F401 -from .page.favorite import PageFavorite # noqa F401 -from .page.tag import PageTag # noqa F401 - # Заметки from .note.common import Note # noqa F401 from .note.favorite import NoteFavorite # noqa F401 from .note.tag import NoteTag # noqa F401 +from .note.trash import NoteTrash # noqa F401 +from .note.tree import NoteTree # noqa F401 + +# Статьи +from .page.common import Page # noqa F401 +from .page.favorite import PageFavorite # noqa F401 +from .page.tag import PageTag # noqa F401 +from .page.trash import PageTrash # noqa F401 +from .page.tree import PageTree # noqa F401 Base.metadata.create_all(engine) diff --git a/src/myapp/models/note/common.py b/src/myapp/models/note/common.py index e1e8c5b..2739ad7 100644 --- a/src/myapp/models/note/common.py +++ b/src/myapp/models/note/common.py @@ -28,6 +28,21 @@ class Note(Base): uselist=False ) tags = relationship("NoteTag", primaryjoin="Note.id==NoteTag.note_id") + # Родитель + parents = relationship( + "NoteTree", + primaryjoin="Note.id == NoteTree.child_id", + ) + # Дочерние узлы + nodes = relationship( + "NoteTree", + primaryjoin="Note.id == NoteTree.parent_id", + ) + trash = relationship( + "NoteTrash", + primaryjoin="Note.id == NoteTrash.note_id", + uselist=False + ) def __init__(self, user, title): assert type(user).__name__ == 'User', 'Не передан объект User' diff --git a/src/myapp/models/note/trash.py b/src/myapp/models/note/trash.py new file mode 100644 index 0000000..afab883 --- /dev/null +++ b/src/myapp/models/note/trash.py @@ -0,0 +1,42 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +import datetime +from sqlalchemy import Column, Integer, ForeignKey, DateTime, String +from sqlalchemy.orm import relationship + +from .. import Base + + +class NoteTrash(Base): + """Корзина для страниц справки + """ + __tablename__ = "note_trash" + + id = Column(Integer, primary_key=True, index=True) + # ID пользователя + user_id = Column(Integer, ForeignKey('user.id'), index=True) + # ID страницы + note_id = Column(Integer, ForeignKey('note.id'), index=True) + # Дата удаления + created = Column(DateTime) + + # Связи + user = relationship( + "User", + primaryjoin="NoteTrash.user_id == User.id", + uselist=False + ) + note = relationship( + "Note", + primaryjoin="NoteTrash.note_id == Note.id", + back_populates="trash", + uselist=False + ) + + def __init__(self, user, note): + assert type(user).__name__ == 'User', 'Не передан объект User' + assert type(note).__name__ == 'Note', 'Не передан объект Note' + self.user_id = user.id + self.note_id = note.id + self.created = datetime.datetime.now() diff --git a/src/myapp/models/note/tree.py b/src/myapp/models/note/tree.py new file mode 100644 index 0000000..5518207 --- /dev/null +++ b/src/myapp/models/note/tree.py @@ -0,0 +1,49 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +import datetime +from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime +from sqlalchemy.orm import relationship + +from .. import Base + + +class NoteTree(Base): + """Справка по сервису + """ + __tablename__ = "note_tree" + + id = Column(Integer, primary_key=True, index=True) + parent_id = Column(Integer, ForeignKey('note.id'), index=True) # ссылку на предка (ancestor) + child_id = Column(Integer, ForeignKey('note.id'), index=True) # ссылку на потомка (descendant) + level = Column(Integer, default=0) # Уровень относительно родителя + + # Связи + # Родитель + parent = relationship( + "Note", + primaryjoin="Note.id == NoteTree.parent_id", + uselist=False + ) + # Дочерний узел + child = relationship( + "Note", + primaryjoin="Note.id == NoteTree.child_id", + uselist=False + ) + + def __init__(self, parent, child): + assert type(parent).__name__ == 'Note', 'Не передан объект Note' + assert type(child).__name__ == 'Note', 'Не передан объект Note' + self.parent_id = parent.id + self.child_id = child.id + + def __repr__(self): + return "".format( + id=self.id, + parent=self.parent, + child=self.child + ) + + def __json__(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/src/myapp/models/page/common.py b/src/myapp/models/page/common.py index 7730b4e..b1b33be 100644 --- a/src/myapp/models/page/common.py +++ b/src/myapp/models/page/common.py @@ -28,6 +28,21 @@ class Page(Base): uselist=False ) tags = relationship("PageTag", primaryjoin="Page.id==PageTag.page_id") + # Родитель + parents = relationship( + "PageTree", + primaryjoin="Page.id == PageTree.child_id", + ) + # Дочерние узлы + nodes = relationship( + "PageTree", + primaryjoin="Page.id == PageTree.parent_id", + ) + trash = relationship( + "PageTrash", + primaryjoin="Page.id == PageTrash.page_id", + uselist=False + ) def __init__(self, user, title): assert type(user).__name__ == 'User', 'Не передан объект User' diff --git a/src/myapp/models/page/trash.py b/src/myapp/models/page/trash.py new file mode 100644 index 0000000..6213d3c --- /dev/null +++ b/src/myapp/models/page/trash.py @@ -0,0 +1,42 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +import datetime +from sqlalchemy import Column, Integer, ForeignKey, DateTime, String +from sqlalchemy.orm import relationship + +from .. import Base + + +class PageTrash(Base): + """Корзина для страниц справки + """ + __tablename__ = "page_trash" + + id = Column(Integer, primary_key=True, index=True) + # ID пользователя + user_id = Column(Integer, ForeignKey('user.id'), index=True) + # ID страницы + page_id = Column(Integer, ForeignKey('page.id'), index=True) + # Дата удаления + created = Column(DateTime) + + # Связи + user = relationship( + "User", + primaryjoin="PageTrash.user_id == User.id", + uselist=False + ) + page = relationship( + "Page", + primaryjoin="PageTrash.page_id == Page.id", + back_populates="trash", + uselist=False + ) + + def __init__(self, user, page): + assert type(user).__name__ == 'User', 'Не передан объект User' + assert type(page).__name__ == 'Page', 'Не передан объект Page' + self.user_id = user.id + self.page_id = page.id + self.created = datetime.datetime.now() diff --git a/src/myapp/models/page/tree.py b/src/myapp/models/page/tree.py new file mode 100644 index 0000000..e704d7c --- /dev/null +++ b/src/myapp/models/page/tree.py @@ -0,0 +1,49 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +import datetime +from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime +from sqlalchemy.orm import relationship + +from .. import Base + + +class PageTree(Base): + """Справка по сервису + """ + __tablename__ = "page_tree" + + id = Column(Integer, primary_key=True, index=True) + parent_id = Column(Integer, ForeignKey('page.id'), index=True) # ссылку на предка (ancestor) + child_id = Column(Integer, ForeignKey('page.id'), index=True) # ссылку на потомка (descendant) + level = Column(Integer, default=0) # Уровень относительно родителя + + # Связи + # Родитель + parent = relationship( + "Page", + primaryjoin="Page.id == PageTree.parent_id", + uselist=False + ) + # Дочерний узел + child = relationship( + "Page", + primaryjoin="Page.id == PageTree.child_id", + uselist=False + ) + + def __init__(self, parent, child): + assert type(parent).__name__ == 'Page', 'Не передан объект Page' + assert type(child).__name__ == 'Page', 'Не передан объект Page' + self.parent_id = parent.id + self.child_id = child.id + + def __repr__(self): + return "".format( + id=self.id, + parent=self.parent, + child=self.child + ) + + def __json__(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/src/myapp/ns_api/__init__.py b/src/myapp/ns_api/__init__.py index aa02db6..447b02d 100644 --- a/src/myapp/ns_api/__init__.py +++ b/src/myapp/ns_api/__init__.py @@ -34,6 +34,7 @@ jsonrpc = JSONRPC() from . import ( # noqa F401 login, + favorite, note, profile, page, diff --git a/src/myapp/ns_api/favorite/__init__.py b/src/myapp/ns_api/favorite/__init__.py new file mode 100644 index 0000000..6561515 --- /dev/null +++ b/src/myapp/ns_api/favorite/__init__.py @@ -0,0 +1,7 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +from . import ( + note, + page +) diff --git a/src/myapp/ns_api/favorite/note.py b/src/myapp/ns_api/favorite/note.py new file mode 100644 index 0000000..22fa2a6 --- /dev/null +++ b/src/myapp/ns_api/favorite/note.py @@ -0,0 +1,110 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +from .. import jsonrpc, login_required +from ... import acl, app, lib, models +from ...mutations.note import note_as_dict + + +@jsonrpc.method('favorite.note.add') +@login_required +def favorite_note_add(id: int) -> bool: + """Добавление статьи в избранное + """ + note = models.db_session.query( + models.Note + ).filter( + models.Note.id == id + ).first() + if note is None: + raise ValueError + exist = models.db_session.query( + models.NoteFavorite + ).filter( + models.NoteFavorite.note_id == id, + models.NoteFavorite.user_id == lib.get_user().id + ).first() + if exist: + raise ValueError + + new_favorite_note = models.NoteFavorite( + lib.get_user(), + note + ) + models.db_session.add(new_favorite_note) + models.db_session.commit() + + return True + + +@jsonrpc.method('favorite.note.delete') +@login_required +def favorite_note_delete(id: int) -> bool: + """Удаление статьи из избранного + """ + exist = models.db_session.query( + models.NoteFavorite + ).filter( + models.NoteFavorite.note_id == id, + models.NoteFavorite.user_id == lib.get_user().id + ).first() + if exist is None: + raise ValueError + models.db_session.delete(exist) + models.db_session.commit() + return True + + +@jsonrpc.method('favorite.notes') +@login_required +def favorite_notes( + page: int = 1, + order_by: dict = {'field': 'title', 'order': 'asc'}, + fields: list = ['id', 'title'] +) -> list: + indexes = models.db_session.query( + models.NoteFavorite.note_id + ).filter( + models.NoteFavorite.user_id == lib.get_user().id + ) + notes = models.db_session.query( + models.Note + ).filter( + models.Note.id.in_(indexes) + ) + + # Сортировка + if order_by['field'] not in ['created', 'id', 'title', 'updated']: + raise ValueError + if order_by['order'] not in ['asc', 'desc']: + raise ValueError + field = getattr(models.Note, order_by['field']) + order = getattr(field, order_by['order']) + notes = notes.order_by( + order() + ) + + notes = lib.getpage( + notes, + page, + app.config['ITEMS_ON_PAGE'] + ).all() + + result = [] + for note in notes: + newRow = note_as_dict(note, fields) + result.append(newRow) + + return result + + +@jsonrpc.method('favorite.notes.count') +@login_required +def favorite_notes_count() -> int: + result = models.db_session.query( + models.NoteFavorite + ).filter( + models.NoteFavorite.user_id == lib.get_user().id + ).count() + + return result diff --git a/src/myapp/ns_api/favorite/page.py b/src/myapp/ns_api/favorite/page.py new file mode 100644 index 0000000..b96261f --- /dev/null +++ b/src/myapp/ns_api/favorite/page.py @@ -0,0 +1,110 @@ +__author__ = 'RemiZOffAlex' +__email__ = 'remizoffalex@mail.ru' + +from .. import jsonrpc, login_required +from ... import acl, app, lib, models +from ...mutations.page import page_as_dict + + +@jsonrpc.method('favorite.page.add') +@login_required +def favorite_page_add(id: int) -> str: + """Добавление статьи в избранное + """ + page = models.db_session.query( + models.Page + ).filter( + models.Page.id == id + ).first() + if page is None: + raise ValueError + exist = models.db_session.query( + models.PageFavorite + ).filter( + models.PageFavorite.page_id == id, + models.PageFavorite.user_id == lib.get_user().id + ).first() + if exist: + raise ValueError + + new_favorite_page = models.PageFavorite( + lib.get_user(), + page + ) + models.db_session.add(new_favorite_page) + models.db_session.commit() + + return 'ok' + + +@jsonrpc.method('favorite.page.delete') +@login_required +def favorite_page_delete(id: int) -> str: + """Удаление статьи из избранного + """ + exist = models.db_session.query( + models.PageFavorite + ).filter( + models.PageFavorite.page_id == id, + models.PageFavorite.user_id == lib.get_user().id + ).first() + if exist is None: + raise ValueError + models.db_session.delete(exist) + models.db_session.commit() + return 'ok' + + +@jsonrpc.method('favorite.pages') +@login_required +def favorite_pages( + page: int = 1, + order_by: dict = {'field': 'title', 'order': 'asc'}, + fields: list = ['id', 'title'] +) -> list: + indexes = models.db_session.query( + models.PageFavorite.page_id + ).filter( + models.PageFavorite.user_id == lib.get_user().id + ) + pages = models.db_session.query( + models.Page + ).filter( + models.Page.id.in_(indexes) + ) + + # Сортировка + if order_by['field'] not in ['created', 'id', 'title', 'updated']: + raise ValueError + if order_by['order'] not in ['asc', 'desc']: + raise ValueError + field = getattr(models.Page, order_by['field']) + order = getattr(field, order_by['order']) + pages = pages.order_by( + order() + ) + + pages = lib.getpage( + pages, + page, + app.config['ITEMS_ON_PAGE'] + ).all() + + result = [] + for page in pages: + newRow = page_as_dict(page, fields) + result.append(newRow) + + return result + + +@jsonrpc.method('favorite.pages.count') +@login_required +def favorite_pages_count() -> int: + result = models.db_session.query( + models.PageFavorite + ).filter( + models.PageFavorite.user_id == lib.get_user().id + ).count() + + return result diff --git a/src/myapp/ns_api/note.py b/src/myapp/ns_api/note.py index 4ec311f..553d0d1 100644 --- a/src/myapp/ns_api/note.py +++ b/src/myapp/ns_api/note.py @@ -3,10 +3,14 @@ __email__ = 'remizoffalex@mail.ru' from . import jsonrpc, login_required from .. import app, lib, models +from ..mutations.note import note_as_dict @jsonrpc.method('note') -def note_id(id: int) -> dict: +def note_id( + id: int, + fields: list[str] = ['id', 'title'] +) -> dict: """Заметка """ note = models.db_session.query( @@ -17,11 +21,7 @@ def note_id(id: int) -> dict: if note is None: raise ValueError - result = note.as_dict() - result['user'] = note.user.as_dict() - result['tags'] = [] - for tagLink in note.tags: - result['tags'].append(tagLink.tag.as_dict()) + result = note_as_dict(note, fields) return result @@ -91,14 +91,30 @@ def note_update(id: int, title: str, body: str) -> dict: @jsonrpc.method('notes') -def notes_list(page: int) -> list: +def notes_list( + page: int = 1, + order_by: dict = {'field': 'title', 'order': 'asc'}, + fields: list = ['id', 'title'] +) -> list: """Список заметок """ notes = models.db_session.query( models.Note - ).order_by( - models.Note.title.asc() + ).filter( + models.Note.user_id==lib.get_user().id ) + + # Сортировка + if order_by['field'] not in ['created', 'id', 'title', 'status', 'updated']: + raise ValueError + if order_by['order'] not in ['asc', 'desc']: + raise ValueError + field = getattr(models.Note, order_by['field']) + order = getattr(field, order_by['order']) + notes = notes.order_by( + order() + ) + notes = lib.getpage( notes, page, @@ -107,11 +123,7 @@ def notes_list(page: int) -> list: result = [] for note in notes: - newRow = note.as_dict() - newRow['user'] = note.user.as_dict() - newRow['tags'] = [] - for tagLink in note.tags: - newRow['tags'].append(tagLink.tag.as_dict()) + newRow = note_as_dict(note, fields) result.append(newRow) return result diff --git a/src/myapp/templates/components/inc.j2 b/src/myapp/templates/components/inc.j2 index f72d1f6..b8bfa86 100644 --- a/src/myapp/templates/components/inc.j2 +++ b/src/myapp/templates/components/inc.j2 @@ -1,5 +1,6 @@ {% include '/components/backtotop.js' %} {% include '/components/filter.js' %} {% include '/components/footer.js' %} +{% include '/components/info.js' %} {% include '/components/order_by.js' %} {% include '/components/pagination.js' %} diff --git a/src/myapp/templates/components/info.js b/src/myapp/templates/components/info.js new file mode 100644 index 0000000..0e5f31a --- /dev/null +++ b/src/myapp/templates/components/info.js @@ -0,0 +1,39 @@ +function ComponentInfo() { + let data = { + item: null, + } + return { + oninit: function(vnode) { + console.log('ComponentInfo.oninit'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + }, + onbeforeupdate: function(vnode) { + console.log('ComponentInfo.onbeforeupdate'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + }, + view: function(vnode) { + console.log('ComponentInfo.view'); + let result = []; + if (data.item!=null) { + result.push( + m('div', {class: 'row'}, + m('div', {class: 'col text-muted py-2'}, + m('small', [ + m('i', {class: 'fa fa-user me-1'}), + m(m.route.Link, {class: 'me-2', href: `/user/${data.item.user.id}`}, data.item.user.name), + {tag: "<", children: `Создано: ${data.item.created}`}, + {tag: "<", children: ' '}, + {tag: "<", children: `Обновлено: ${data.item.updated}`}, + ]) + ) + ) + ); + }; + return result; + } + } +}; diff --git a/src/myapp/templates/private/components/inc.j2 b/src/myapp/templates/private/components/inc.j2 index 8d7344e..e41d04d 100644 --- a/src/myapp/templates/private/components/inc.j2 +++ b/src/myapp/templates/private/components/inc.j2 @@ -1,4 +1,6 @@ {% include '/private/components/favorite.js' %} {% include '/private/components/menu-general.js' %} +{% include '/private/components/notes.js' %} {% include '/private/components/pages.js' %} +{% include '/private/components/tags.js' %} {% include '/private/components/users.js' %} diff --git a/src/myapp/templates/private/components/menu-general.js b/src/myapp/templates/private/components/menu-general.js index 423d4ef..7bf1f46 100644 --- a/src/myapp/templates/private/components/menu-general.js +++ b/src/myapp/templates/private/components/menu-general.js @@ -30,8 +30,10 @@ function MenuGeneral() { m(m.route.Link, { class: 'btn btn-outline-secondary btn-lg border-0', href: '/tags'}, 'Метки'), m(m.route.Link, { class: 'btn btn-outline-secondary btn-lg border-0', href: '/users'}, 'Пользователи'), m('div', { class: 'btn-group btn-group-lg float-end'}, - m(m.route.Link, {class: 'btn btn-outline-secondary', href: '/profile'}, m('i', {class: 'fa fa-user'})), - m('button', {class: "btn btn-outline-danger", onclick: logout}, m('i', {class: "fa fa-sign-out"})), + m(m.route.Link, { class: 'btn btn-outline-primary', href: '/notes' }, m('i', { class: 'fa fa-sticky-note-o' })), + m(m.route.Link, { class: 'btn btn-outline-warning', href: '/favorite' }, m('i', { class: 'fa fa-star' })), + m(m.route.Link, { class: 'btn btn-outline-secondary', href: '/profile'}, m('i', {class: 'fa fa-user'})), + m('button', {class: 'btn btn-outline-danger', onclick: logout}, m('i', {class: 'fa fa-sign-out'})), ) ]) ) diff --git a/src/myapp/templates/private/components/notes.js b/src/myapp/templates/private/components/notes.js new file mode 100644 index 0000000..81dc06b --- /dev/null +++ b/src/myapp/templates/private/components/notes.js @@ -0,0 +1,57 @@ +function ComponentNotes() { + let data = { + notes: null, + }; + function note_render(note, noteIdx) { + let odd = ''; + if (noteIdx % 2) { + odd = ' bg-light' + }; + let tags = note.tags.map( + function(tag, tagIdx) { + return [ + m('i', {class: "fa fa-tag"}), + {tag: '<', children: ' '}, + m(m.route.Link, {class: "font-monospace text-decoration-none", href: `/tag/${tag.id}`}, tag.name), + {tag: '<', children: ' '}, + ] + } + ); + + return m('div', {class: 'row'}, + m('div', {class: "col py-2" + odd}, [ + m(m.route.Link, {class: "text-decoration-none", href: `/note/${note.id}`}, m.trust(note.title)), + m('div', {class: 'row'}, + m('div', {class: 'col text-muted'}, + m('small', [...tags]) + ) + ) + ]) + ) + }; + function notes_render() { + return data.notes.map(note_render); + }; + return { + oninit: function(vnode) { + console.log('ComponentNotes.oninit'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + }, + onupdate: function(vnode) { + console.log('ComponentNotes.onupdate'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + }, + view: function() { + console.log('ComponentNotes.view'); + if (data.notes!=null) { + let result = []; + result.push(notes_render()); + return result; + }; + } + }; +}; diff --git a/src/myapp/templates/private/components/tags.js b/src/myapp/templates/private/components/tags.js new file mode 100644 index 0000000..68697e5 --- /dev/null +++ b/src/myapp/templates/private/components/tags.js @@ -0,0 +1,356 @@ +/* +Пример + +result.push(m(ComponentTags, {resource: data.file, typeOf: 'file'})); +*/ +function ComponentTags(arguments) { + let data = { + resource: null, + typeOf: null, + newtag: '', + groups: {}, + tags_all: [], + panels: { + standart: { + visible: false + }, + new: { + visible: false + } + }, + error: null, + }; + for (let key in arguments){ + data[key] = arguments[key]; + }; + function tag_add() { + /* Добавить тег к ресурсу */ + let newtag = data.newtag.trim().toLowerCase(); + data.newtag = newtag; + if (data.newtag.length<2) { + return; + } + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'tag.exist', + "params": { + "name": data.newtag + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + tag_add_to_node( response['result'] ); + } else if ('error' in response) { + console.log(response); + data.panels.new.visible = true; + } + } + ).catch( + function(error) { + console.log(error); + } + ); + }; + function tag_new_add() { + /* Добавление нового тега */ + if (data.newtag.length<2) { + return; + } + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'tag.add', + "params": { + "name": data.newtag + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + tags_get(); + tag_add_to_node( response['result'] ); + } + } + ); + }; + function tag_remove(tag) { + /* Удаление тега из ресурса */ + console.log(tag_includes(tag)); + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": `tag.${data.typeOf}.delete`, + "params": { + "tag": tag.id, + "id": data.resource.id + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.resource.tags = arrayRemove(data.resource.tags, tag); + } + } + ) + }; + function tags_get() { + /* Получить список тегов */ + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'tags.groups', + "params": {}, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'tags', + "params": {}, + "id": get_id() + } + ] + }).then( + function(response) { + if ('result' in response[0]) { + data.groups = response[0]['result']; + } + if ('result' in response[1]) { + data.tags_all = response[1]['result']; + } + } + ) + }; + function tags_filtered() { + let result = data.tags_all.filter(filterTag); + return result; + }; + function tag_includes(tag) { + let result = data.resource.tags.find(function(element, index, array) { + return element.id===tag.id; + }, tag); + return result; + }; + function filterTag(tag) { + let value = data.newtag; + if ( value.length<2 ) { + return false; + } + if ( tag.name.toLowerCase().includes(value.toLowerCase()) ) { + return true; + } + return false; + }; + function tag_add_to_node(tag) { + /* Добавление тега к ресурсу */ + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": `tag.${data.typeOf}.add`, + "params": { + "id": data.resource.id, + "tag": tag.id + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.resource.tags.push(response['result']); + sortedTags(); + data.panels.new.visible = false; + } + } + ) + }; + function sortedTags() { + if (data.resource.tags === undefined) {return [];} + data.resource.tags.sort( + function(a, b) { + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; + } + ); + }; + function newtag_clear() { + /* Очистка поля нового тега */ + data.newtag = ''; + data.panels.new.visible = false; + }; + function form_submit_tag_add(e) { + e.preventDefault(); + tag_add(); + }; + function form_submit_cancelNewTag(e) { + e.preventDefault(); + newtag_clear(); + }; + function groups_render() { + let result = []; + Object.keys(data.groups).forEach( + function(group, groupIdx) { + if (groupIdx % 2) { + result.push(m('a', {class: 'btn btn-outline-secondary font-monospace btn-lg me-2 mb-2', href: `#tag${groupIdx}`}, group)); + } else { + result.push(m('a', {class: 'btn btn-outline-primary font-monospace btn-lg me-2 mb-2', href: `#tag${groupIdx}`}, group)); + }; + } + ); + return result; + }; + function groups_tags_render() { + let result = []; + Object.keys(data.groups).forEach( + function(group, groupIdx) { + let tags = data.groups[group].map( + function(tag, tagIdx) { + if (tag_includes(tag)) { + return m('button', {type: 'button', class: 'btn btn-primary font-monospace me-2 mb-1', onclick: function() {tag_remove(tag)}}, tag.name); + } else { + return m('button', {type: 'button', class: 'btn btn-outline-secondary font-monospace me-2 mb-1', onclick: function() {tag_add_to_node(tag)}}, tag.name); + } + } + ); + result.push(m('div', {class: 'row mt-3'}, + m('div', {class: 'col pe-0'}, [ + m('a', {name: `tag${groupIdx}`}), + m('a', {class: 'btn btn-outline-danger me-2 mb-1', href: '#tags'}, group), + [...tags] + ]) + )); + } + ); + return result; + }; + function tags_render() { + let result = data.resource.tags.map( + function(tag) { + return m('div', {class: 'btn-group mb-1 me-2'}, [ + m(m.route.Link, {class: 'btn btn-outline-secondary font-monospace', href: `/tag/${tag.id}`}, tag.name), + m('button', {class: 'btn btn-outline-danger', type: 'button', onclick: function() {tag_remove(tag)}}, m('i', {class: 'fa fa-remove'})), + ]); + } + ); + return result; + }; + function button_add_render() { + if (data.panels.standart.visible) { + return m('button', {class: 'btn btn-outline-danger mb-1 me-2', type: 'button', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-minus'})); + } else { + return m('button', {class: 'btn btn-outline-success mb-1 me-2', type: 'button', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-plus'})); + } + }; + return { + data: data, + oninit: function(vnode) { + console.log('ComponentTags.oninit'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + tags_get(); + }, + onbeforeupdate: function(vnode) { + console.log('ComponentTags.onbeforeupdate'); + for (let key in vnode.attrs){ + data[key] = vnode.attrs[key]; + }; + // tags_get(); + }, + view: function(vnode) { + console.log('ComponentTags.view'); + let result = [] + if (data.resource) { + result.push( + m('div', {class: 'row mb-3'}, + m('div', {class: 'col'}, [ + m('a', {name: "tags"}), + m('div', {class: 'btn mb-1'}, m('i', {class: 'fa fa-tags'})), + button_add_render(), + tags_render() + ]) + ) + ); + if (data.panels.standart.visible) { + result.push(...[ + m('div', {class: 'row mb-3'}, + m('div', {class: 'col'}, [ + m('form', {onsubmit: form_submit_tag_add}, + m('div', {class: 'input-group'}, [ + m('button', {class: 'btn btn-outline-danger', type: 'button', onclick: newtag_clear}, m('i', {class: 'fa fa-remove'})), + m('input', {class: 'form-control', oninput: function(e) {data.newtag = e.target.value}, onkeyup: function(e) { if (e.keyCode==13) { tag_add() }}, value: data.newtag}), + m('button', {class: 'btn btn-outline-success', type: 'submit'}, m('i', {class: 'fa fa-save'})), + ]) + ) + ]) + ), + m('div', {class: 'row mb-3'}, + m('div', {class: 'col py-2'}, + (function() { + let result = tags_filtered().map( + function(tag) { + if (tag_includes(tag)) { + return m('button', {class: 'btn btn-primary font-monospace me-2 mb-1', type: 'button', onclick: function() { tag_remove(tag) }}, tag.name); + } else { + return m('button', {class: 'btn btn-outline-secondary font-monospace me-2 mb-1', type: 'button', onclick: function() { tag_add_to_node(tag) }}, tag.name); + } + } + ); + return result; + })() + ) + ), + m('div', {class: 'row mb-3'}, + m('div', {class: 'col py-2'}, + groups_render() + ) + ) + ]); + if (data.panels.new.visible) { + result.push( + m('form', {onsubmit: form_submit_cancelNewTag}, + m('div', {class: 'row mb-3'}, + m('div', {class: 'col py-2'}, [ + m('label', 'Добавить новый тег?'), + m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, + m('div', {class: 'input-group'}, [ + m('button', {class: 'btn btn-outline-danger', type: 'submit'}, m('i', {class: 'fa fa-close'})), + m('input', {class: 'form-control', oninput: function (e) {data.newtag = e.target.value}, value: data.newtag}), + m('button', {class: 'btn btn-outline-success', type: 'button', onclick: tag_new_add}, 'Добавить') + ]) + ) + ) + ]) + ) + ), + ); + }; + result.push( + groups_tags_render() + ); + }; + }; + return result; + } + }; +}; diff --git a/src/myapp/templates/private/domains/favorite/favorite.js b/src/myapp/templates/private/domains/favorite/favorite.js new file mode 100644 index 0000000..5a9e763 --- /dev/null +++ b/src/myapp/templates/private/domains/favorite/favorite.js @@ -0,0 +1,110 @@ +function Favorite() { + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')), + m('li', {class: 'breadcrumb-item active'}, 'Избранные вопросы'), + ]); + return result; + }; + return { + view: function(vnode) { + console.log('Favorite.view'); + let result = []; + result.push([ + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, [ + m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: '/profile', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})), + 'Избранное' + ]) + ), + m('hr'), + m(MenuFavorite), + breadcrumbs_render(), + ]); + return result; + } + }; +}; +/* +

+ +Избранное

+{# include 'favorite_menu.html' #} +
+ + + +Object.assign(root.data, { + questions: 0, + orders: 0, + pages: 0, + resumes: 0, + menuitem: null, +}); +root.created = function() { + let vm = this; + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'favorite.questions.count', + "params": {}, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'favorite.orders.count', + "params": {}, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'favorite.pages.count', + "params": {}, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'favorite.resumes.count', + "params": {}, + "id": get_id() + }, + ] + }).then( + function(response) { + if ('result' in response[0]) { + vm.questions = response[0]['result']; + } + if ('result' in response[1]) { + vm.orders = response[1]['result']; + } + if ('result' in response[2]) { + vm.pages = response[2]['result']; + } + if ('result' in response[3]) { + vm.resumes = response[3]['result']; + } + } + ); +}; +*/ diff --git a/src/myapp/templates/private/domains/favorite/inc.j2 b/src/myapp/templates/private/domains/favorite/inc.j2 new file mode 100644 index 0000000..251686f --- /dev/null +++ b/src/myapp/templates/private/domains/favorite/inc.j2 @@ -0,0 +1,15 @@ +{% include '/private/domains/favorite/favorite.js' %} +{% include '/private/domains/favorite/menu.js' %} +{% include '/private/domains/favorite/notes.js' %} +{% include '/private/domains/favorite/pages.js' %} + +Object.assign( + routes, + { + "/favorite": layout_decorator(Favorite), + "/favorite/notes": layout_decorator(FavoriteNotes), + "/favorite/notes/:page": layout_decorator(FavoriteNotes), + "/favorite/pages": layout_decorator(FavoritePages), + "/favorite/pages/:page": layout_decorator(FavoritePages), + } +); diff --git a/src/myapp/templates/private/domains/favorite/menu.js b/src/myapp/templates/private/domains/favorite/menu.js new file mode 100644 index 0000000..ac97d97 --- /dev/null +++ b/src/myapp/templates/private/domains/favorite/menu.js @@ -0,0 +1,45 @@ +function MenuFavorite() { + let data = { + menuitem: null, + } + function button_common() { + if (data.menuitem===null) { + return {tag: '<', children: '
'}; + } else { + return m(m.route.Link, {class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite', title: 'Избранное'}, m('i', {class: 'fa fa-bars'})) + } + }; + function button_pages() { + if (data.menuitem==='pages') { + return {tag: '<', children: '
Статьи
'}; + } else { + return m(m.route.Link, {class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite/pages'}, 'Статьи') + } + }; + function button_notes() { + if (data.menuitem === 'notes') { + return { tag: '<', children: '
Заметки
' }; + } else { + return m(m.route.Link, { class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite/notes' }, 'Заметки') + } + }; + return { + oninit: function(vnode) { + console.log('MenuFavorite.oninit'); + for (let key in vnode.attrs){ + console.log(key); + data[key] = vnode.attrs[key]; + }; + }, + view: function() { + console.log('MenuFavorite.view'); + return m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + button_common(), + button_notes(), + button_pages(), + ]) + ); + } + } +}; diff --git a/src/myapp/templates/private/domains/favorite/notes.js b/src/myapp/templates/private/domains/favorite/notes.js new file mode 100644 index 0000000..5e8770e --- /dev/null +++ b/src/myapp/templates/private/domains/favorite/notes.js @@ -0,0 +1,120 @@ +function FavoriteNotes() { + let data = { + filter: PanelFilter(), + order_by: PanelOrderBy({ + field: 'updated', + fields: [ + {value: 'id', text: 'ID'}, + {value: 'title', text: 'заголовку'}, + {value: 'created', text: 'дате создания'}, + {value: 'updated', text: 'дате обновления'} + ], + clickHandler: notes_get, + order: 'asc', + }), + notes: [], + pagination: { + page: 1, + size: 0, + prefix_url: '/favorite/notes' + }, + }; + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')), + m('li', {class: 'breadcrumb-item active'}, 'Избранные заметки'), + ]); + return result; + }; + function notes_get() { + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'favorite.notes', + "params": { + "page": data.pagination.page, + "order_by": data.order_by.value, + "fields": ["id", "title", "tags", "created", "updated", "user"] + }, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'favorite.notes.count', + "id": get_id() + } + ] + + }).then( + function(response) { + if ('result' in response[0]) { + data.notes = response[0]['result']; + } + if ('result' in response[1]) { + data.pagination.size = response[1]['result']; + } + } + ); + }; + return { + oninit: function(vnode) { + console.log('FavoriteNotes.oninit'); + document.title = `Избранные статьи - ${SETTINGS.TITLE}`; + if (vnode.attrs.page!==undefined) { + data.pagination.page = Number(vnode.attrs.page); + }; + notes_get(); + }, + onbeforeupdate: function(vnode) { + console.log('FavoriteNotes.onbeforeupdate'); + if (vnode.attrs.page!==undefined) { + if (data.pagination.page != Number(vnode.attrs.page)) { + data.pagination.page = Number(vnode.attrs.page); + notes_get(); + }; + } else { + if (data.pagination.page != 1) { + data.pagination.page = 1; + notes_get(); + }; + } + }, + view: function(vnode) { + console.log('FavoriteNotes.view'); + result = []; + result.push( + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, [ + m('div', {class: "btn-group btn-group-lg me-2"}, [ + m(m.route.Link, {class: "btn btn-outline-secondary", href: '/favorite', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})), + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }}, + m('i', {class: "fa fa-filter"}) + ), + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }}, + m('i', {class: "fa fa-sort-alpha-asc"}) + ) + ]), + `Избранные заметки` + ]) + ), + m('hr'), + m(MenuFavorite, {menuitem: 'notes'}), + ); + + result.push(m(data.filter)); + result.push(m(data.order_by)); + result.push(m(Pagination, data.pagination)); + if (data.notes.length>0) { + result.push(m(ComponentNotes, {notes: data.notes})); + result.push(m(Pagination, data.pagination)); + }; + result.push(breadcrumbs_render()); + return result; + } + }; +}; diff --git a/src/myapp/templates/private/domains/favorite/pages.js b/src/myapp/templates/private/domains/favorite/pages.js new file mode 100644 index 0000000..952dd98 --- /dev/null +++ b/src/myapp/templates/private/domains/favorite/pages.js @@ -0,0 +1,120 @@ +function FavoritePages() { + let data = { + filter: PanelFilter(), + order_by: PanelOrderBy({ + field: 'updated', + fields: [ + {value: 'id', text: 'ID'}, + {value: 'title', text: 'заголовку'}, + {value: 'created', text: 'дате создания'}, + {value: 'updated', text: 'дате обновления'} + ], + clickHandler: pages_get, + order: 'asc', + }), + pages: [], + pagination: { + page: 1, + size: 0, + prefix_url: '/favorite/pages' + }, + }; + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')), + m('li', {class: 'breadcrumb-item active'}, 'Избранные статьи'), + ]); + return result; + }; + function pages_get() { + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'favorite.pages', + "params": { + "page": data.pagination.page, + "order_by": data.order_by.value, + "fields": ["id", "title", "tags", "created", "updated", "user"] + }, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'favorite.pages.count', + "id": get_id() + } + ] + + }).then( + function(response) { + if ('result' in response[0]) { + data.pages = response[0]['result']; + } + if ('result' in response[1]) { + data.pagination.size = response[1]['result']; + } + } + ); + }; + return { + oninit: function(vnode) { + console.log('FavoritePages.oninit'); + document.title = `Избранные статьи - ${SETTINGS.TITLE}`; + if (vnode.attrs.page!==undefined) { + data.pagination.page = Number(vnode.attrs.page); + }; + pages_get(); + }, + onbeforeupdate: function(vnode) { + console.log('FavoritePages.onbeforeupdate'); + if (vnode.attrs.page!==undefined) { + if (data.pagination.page != Number(vnode.attrs.page)) { + data.pagination.page = Number(vnode.attrs.page); + pages_get(); + }; + } else { + if (data.pagination.page != 1) { + data.pagination.page = 1; + pages_get(); + }; + } + }, + view: function(vnode) { + console.log('FavoritePages.view'); + result = []; + result.push( + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, [ + m('div', {class: "btn-group btn-group-lg me-2"}, [ + m(m.route.Link, {class: "btn btn-outline-secondary", href: '/favorite', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})), + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }}, + m('i', {class: "fa fa-filter"}) + ), + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }}, + m('i', {class: "fa fa-sort-alpha-asc"}) + ) + ]), + `Избранные статьи` + ]) + ), + m('hr'), + m(MenuFavorite, {menuitem: 'pages'}), + ); + + result.push(m(data.filter)); + result.push(m(data.order_by)); + result.push(m(Pagination, data.pagination)); + if (data.pages.length>0) { + result.push(m(ComponentPages, {pages: data.pages})); + result.push(m(Pagination, data.pagination)); + }; + result.push(breadcrumbs_render()); + return result; + } + }; +}; diff --git a/src/myapp/templates/private/domains/inc.j2 b/src/myapp/templates/private/domains/inc.j2 index 151d2b3..dcfc224 100644 --- a/src/myapp/templates/private/domains/inc.j2 +++ b/src/myapp/templates/private/domains/inc.j2 @@ -1,3 +1,5 @@ +{% include '/private/domains/favorite/inc.j2' %} +{% include '/private/domains/note/inc.j2' %} {% include '/private/domains/page/inc.j2' %} {% include '/private/domains/profile/inc.j2' %} {% include '/private/domains/tag/inc.j2' %} diff --git a/src/myapp/templates/private/domains/note/add.js b/src/myapp/templates/private/domains/note/add.js new file mode 100644 index 0000000..64d8d5c --- /dev/null +++ b/src/myapp/templates/private/domains/note/add.js @@ -0,0 +1,103 @@ +function NoteAdd() { + let data = { + uuid: get_id(), + note: { + title: '', + body: '', + }, + editor: null, + }; + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список заметок')), + m('li', {class: 'breadcrumb-item active'}, 'Новая заметка'), + ]); + return result; + }; + function form_submit(e) { + e.preventDefault(); + note_add(); + }; + function note_add() { + if (data.note.title.length<2) { + return; + }; + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note.add', + "params": data.note, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + m.route.set(`/note/${response['result'].id}`); + } + } + ) + }; + function editor_events(ed) { + ed.on('change', function (e) { + data.note.body = ed.getContent(); + }); + }; + return { + oncreate(vnode) { + console.log('NoteAdd.oncreate'); + if (data.editor==null) { + tinymce_config = tinymce_config_init(); + tinymce_config.selector = `#note_body_${data.uuid}`; + tinymce_config.setup = editor_events; + tinymce.init(tinymce_config).then( + function (editors) { + data.editor = editors[0]; + } + ); + } + }, + onbeforeremove: function(vnode) { + console.log('NoteAdd.onbeforeremove'); + if (data.editor!=null) { + data.editor.remove(); + data.editor = null; + }; + }, + view: function(vnode) { + console.log('NoteAdd.view'); + result = []; + result.push(breadcrumbs_render()); + result.push([ + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, [ + m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: "/notes", title: "Список заметок"}, m('i', {class: 'fa fa-chevron-left'})), + 'Новая заметка' + ]) + ), + m('hr') + ]); + result.push( + m('form', {onsubmit: form_submit}, [ + m('div', {class: 'mb-2'}, [ + m('label', {class: 'form-label'}, 'Заголовок'), + m('input', {class: 'form-control', type: 'text', oninput: function (e) {data.note.title = e.target.value}, value: data.note.title}, 'Заголовок'), + ]), + m('div', {class: 'mb-2'}, [ + m('label', {class: 'form-label'}, 'Текст'), + m('textarea', { class: 'form-control', cols: '40', rows: '8', id: `note_body_${data.uuid}`, oninput: function (e) {data.note.body = e.target.value}, value: data.note.body}) + ]), + m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + m('button', {class: 'btn btn-outline-success btn-lg float-end', type: 'submit'}, [m('i', {class: 'fa fa-save me-2'}), 'Сохранить']), + ]) + ), + ]) + ); + result.push(breadcrumbs_render()); + return result; + } + }; +}; diff --git a/src/myapp/templates/private/domains/note/edit.js b/src/myapp/templates/private/domains/note/edit.js new file mode 100644 index 0000000..ebf1afc --- /dev/null +++ b/src/myapp/templates/private/domains/note/edit.js @@ -0,0 +1,170 @@ +function NoteEdit() { + let data = { + uuid: get_id(), + note: null, + editor: null, + }; + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список статей')), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: `/note/${data.note.id}`}, data.note.title)), + m('li', {class: 'breadcrumb-item active'}, 'Редактирование страницы'), + ]); + return result; + }; + function note_get(id) { + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note', + "params": { + "id": id, + "fields": ["id", "title", "body"] + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.note = response['result']; + } + } + ) + }; + function apply() { + /* Сохранить */ + if (data.note.title.length<2) { + return; + }; + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": "note.update", + "params": { + "id": data.note.id, + "title": data.note.title, + "body": data.note.body + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + } + } + ); + }; + function apply_and_close() { + /* Сохранить */ + if (data.note.title.length<2) { + return; + }; + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": "note.update", + "params": { + "id": data.note.id, + "title": data.note.title, + "body": data.note.body + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + m.route.set(`/note/${data.note.id}`); + } + } + ); + }; + function form_submit(e) { + e.preventDefault(); + apply_and_close(); + }; + function editor_events(ed) { + ed.on('change', function (e) { + data.note.body = ed.getContent(); + }); + }; + return { + oncreate(vnode) { + console.log('NoteEdit.oncreate'); + if (data.note!=null) { + console.log(data.note); + if (data.editor==null) { + tinymce_config = tinymce_config_init(); + tinymce_config.selector = `#note_body_${data.uuid}`; + tinymce_config.setup = editor_events; + tinymce.init(tinymce_config).then( + function (editors) { + data.editor = editors[0]; + } + ); + } + } + }, + oninit: function(vnode) { + console.log('NoteEdit.oninit'); + note_get(vnode.attrs.id); + }, + onupdate: function(vnode) { + console.log('NoteEdit.onupdate'); + if (data.note.id.toString()!==vnode.attrs.id) { + note_get(vnode.attrs.id); + }; + if (data.note!=null) { + if (data.editor==null) { + tinymce_config = tinymce_config_init(); + tinymce_config.selector = `#note_body_${data.uuid}`; + tinymce_config.setup = editor_events; + tinymce.init(tinymce_config).then( + function (editors) { + data.editor = editors[0]; + } + ); + }; + }; + }, + view: function(vnode) { + let result = []; + if (data.note!=null) { + result.push( + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, + m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: `/note/${data.note.id}`, title: data.note.title}, m('i', {class: 'fa fa-chevron-left'})), + 'Редактирование страницы', + ) + ), + m('hr'), + m('form', {onsubmit: form_submit}, [ + m('div', {class: 'mb-2'}, [ + m('label', {class: 'form-label'}, 'Заголовок'), + m('input', {class: 'form-control', type: 'text', oninput: function (e) {data.note.title = e.target.value}, value: data.note.title}, 'Заголовок'), + ]), + m('div', {class: 'mb-2'}, [ + m('label', {class: 'form-label'}, 'Текст'), + m('textarea', { class: 'form-control', cols: '40', rows: '8', id: `note_body_${data.uuid}`, oninput: function (e) {data.note.body = e.target.value}, value: data.note.body}) + ]), + m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + m('button', { class: 'btn btn-outline-success btn-lg float-end', type: 'button', onclick: apply }, [m('i', { class: 'fa fa-save me-2'}), 'Сохранить']), + m('button', {class: 'btn btn-outline-success btn-lg float-end', type: 'submit'}, [m('i', {class: 'fa fa-save me-2'}), 'Сохранить и закрыть']), + ]) + ), + ]) + ); + result.push(breadcrumbs_render()); + }; + return result; + } + }; +}; diff --git a/src/myapp/templates/private/domains/note/inc.j2 b/src/myapp/templates/private/domains/note/inc.j2 new file mode 100644 index 0000000..4f0756b --- /dev/null +++ b/src/myapp/templates/private/domains/note/inc.j2 @@ -0,0 +1,17 @@ +{% include '/private/domains/note/add.js' %} +{% include '/private/domains/note/edit.js' %} +{% include '/private/domains/note/note.js' %} +{% include '/private/domains/note/notes.js' %} +{% include '/private/domains/note/source.js' %} + +Object.assign( + routes, + { + "/note/add": layout_decorator(NoteAdd), + "/note/:id/edit": layout_decorator(NoteEdit), + "/note/:id/source": layout_decorator(NoteSource), + "/note/:id": layout_decorator(Note), + "/notes": layout_decorator(Notes), + "/notes/:page": layout_decorator(Notes), + } +); diff --git a/src/myapp/templates/private/domains/note/note.js b/src/myapp/templates/private/domains/note/note.js new file mode 100644 index 0000000..756fdcf --- /dev/null +++ b/src/myapp/templates/private/domains/note/note.js @@ -0,0 +1,220 @@ +function Note() { + let data = { + note: null, + panels: { + standart: { + visible: false + } + }, + }; + function breadcrumbs_render() { + let result = m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список заметок')), + m('li', {class: 'breadcrumb-item active'}, data.note.title), + ]); + return result; + }; + function settings_save(name) { + if (name==='status') { + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note.status', + "params": { + "id": data.note.id, + "status": data.note.status + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.note = response['result']; + } + } + ); + } else if (name==='typeOf') { + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note.settings.update', + "params": { + "id": data.note.id, + "name": name, + "typeOf": "string", + "value": value + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.note = response['result']; + } + } + ); + } + }; + function note_delete() { + /* Удалить статью в корзину */ + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note.delete', + "params": { + "id": data.note.id + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + m.route.set('/notes'); + } + } + ); + }; + function note_get(id) { + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'note', + "params": { + "id": id, + "fields": ["id", "title", "body", "created", "updated", "parent_id", "tags", "user", "nodes"] + }, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'note.nodes', + "params": { + "id": id + }, + "id": get_id() + } + ] + }).then( + function(response) { + if ('result' in response[0]) { + data.note = response[0]['result']; + document.title = `${data.note.title} - ${SETTINGS.TITLE}`; + } + } + ) + }; + function button_back_render() { + if (data.note.parent_id===null) { + return m(m.route.Link, {class: "btn btn-outline-secondary", href: "/notes", title: "Список статей"}, m('i', {class: 'fa fa-chevron-left'})) + } else { + return m(m.route.Link, {class: "btn btn-outline-secondary", href: `/note/${data.note.parent_id}`, title: "Список статей"}, m('i', {class: 'fa fa-chevron-left'})) + } + }; + function button_draft_render() { + if (data.note.status==='draft') { + return m('span', {class: 'small text-muted'}, 'черновик') + } + }; + function button_trash_render() { + if (data.note.trash) { + return m('button', {class: 'btn btn-outline-success', onclick: note_recovery}, [m('i', {class: 'fa fa-plus'}), ' Восстановить из корзины']) + } else { + return m('button', {class: 'btn btn-outline-warning', onclick: note_delete}, [m('i', {class: 'fa fa-trash-o'}), ' В корзину']) + } + }; + function panels_standart_render() { + if (data.panels.standart.visible) { + return m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + m(ComponentFavorite, {resource: data.note, name: 'note'}), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/edit`}, [m('i', {class: 'fa fa-edit'}), ' Редактировать']), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/moving`}, [m('i', {class: 'fa fa-arrows'}), ' Перемещение']), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/history`}, [m('i', {class: 'fa fa-history'}), ' История изменений']), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/source`}, [m('i', {class: 'fa fa-code'}), ' Исходный код']), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/print`}, [m('i', {class: 'fa fa-print'}), ' Печать']), + m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/recovery`}, [m('i', {class: 'fa fa-print'}), ' Печать']), + button_trash_render(), + m('h3', 'Свойства'), + m('div', {class: 'row py-2'}, + m('div', {class: 'col-md-4'}, m('label', 'Статус')), + m('div', {class: 'col-md-8'}, + m('div', {class: 'input-group'}, [ + m('button', {class: 'btn btn-outline-warning', type: 'button'}, m('i', {class: 'fa fa-rotate-left'})), + m('select', {class: 'form-select', onchange: function(e) { data.note.status = e.target.value; }, value: data.note.status}, [ + m('option', {value: 'draft'}, 'Черновик'), + m('option', {value: 'published'}, 'Опубликовано') + ]), + m('button', {class: 'btn btn-outline-success', type: 'button', onclick: function() { settings_save('status') }}, m('i', {class: 'fa fa-save'})) + ]) + ) + ) + ]) + ); + } + }; + return { + oninit: function(vnode) { + console.log('Note.oninit'); + note_get(Number(vnode.attrs.id)); + }, + onbeforeupdate: function(vnode) { + console.log('Note.onbeforeupdate'); + if (data.note != null) { + if (data.note.id!==Number(vnode.attrs.id)) { + note_get(Number(vnode.attrs.id)); + } + } + }, + view: function(vnode) { + console.log('Note.view'); + result = []; + if (data.note!=null) { + result.push( + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-2'}, + m(m.route.Link, {class: 'btn btn-outline-success float-end', href: `/note/${data.note.id}/add`}, m('i', {class: 'fa fa-plus'})), + m('div', {class: 'btn-group btn-group-lg me-2'}, [ + button_back_render(), + m('button', {type: 'button', class: 'btn btn-outline-secondary', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-cog'})), + ]), + data.note.title, + button_draft_render(), + ) + ), + panels_standart_render(), + m('hr'), + m(ComponentInfo, {item: data.note}), + m(ComponentTags, {resource: data.note, typeOf: 'note'}), + m('div', {class: 'row'}, + m('div', {class: 'col'}, m.trust(data.note.body)) + ), + ); + if (data.note.nodes.length>0) { + let nodes = data.note.nodes.map( + function(subnote, subnoteIdx) { + return m('li', + m(m.route.Link, {href: `/note/${subnote.id}`}, subnote.title) + ); + } + ); + result.push( + m('ul', [...nodes]) + ); + } + result.push(breadcrumbs_render()); + } + return result; + } + } +}; diff --git a/src/myapp/templates/private/domains/note/notes.js b/src/myapp/templates/private/domains/note/notes.js new file mode 100644 index 0000000..9615818 --- /dev/null +++ b/src/myapp/templates/private/domains/note/notes.js @@ -0,0 +1,112 @@ +function Notes() { + let data = { + filter: PanelFilter(), + order_by: PanelOrderBy({ + field: 'title', + fields: [ + {value: 'id', text: 'ID'}, + {value: 'title', text: 'заголовку'}, + {value: 'created', text: 'дате создания'}, + {value: 'updated', text: 'дате обновления'} + ], + clickHandler: notes_get, + order: 'asc', + }), + notes: [], + pagination: { + page: 1, + size: 0, + prefix_url: '/notes' + }, + } + function breadcrumbs_render() { + return m('ul', {class: 'breadcrumb mt-2'}, [ + m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))), + m('li', {class: 'breadcrumb-item active'}, 'Список заметок') + ]); + }; + function notes_get() { + m.request({ + url: '/api', + method: "POST", + body: [ + { + "jsonrpc": "2.0", + "method": 'notes', + "params": { + "page": data.pagination.page, + "order_by": data.order_by.value, + "fields": ["id", "title", "tags", "created", "updated"] + }, + "id": get_id() + }, + { + "jsonrpc": "2.0", + "method": 'notes.count', + "params": {}, + "id": get_id() + } + ] + + }).then( + function(response) { + if ('result' in response[0]) { + data.notes = response[0]['result']; + } + if ('result' in response[1]) { + data.pagination.size = response[1]['result']; + } + } + ); + } + return { + oninit: function(vnode) { + console.log('Notes.oninit'); + document.title = `Список статей - ${SETTINGS.TITLE}`; + if (vnode.attrs.page!==undefined) { + data.pagination.page = Number(vnode.attrs.page); + }; + notes_get(); + }, + onbeforeupdate: function(vnode) { + console.log('Notes.onbeforeupdate'); + if (vnode.attrs.page!==undefined) { + if (data.pagination.page.toString() != vnode.attrs.page) { + data.pagination.page = Number(vnode.attrs.page); + notes_get(); + } + }; + }, + view: function(vnode) { + console.log('Notes.view'); + result = []; + result.push([ + breadcrumbs_render(), + m('div', {class: 'row'}, + m('div', {class: 'col py-2 h1'}, [ + m(m.route.Link, {class: 'btn btn-outline-success btn-lg float-end', href: '/note/add'}, m('i', {class: 'fa fa-plus'})), + m('div', {class: "btn-group btn-group-lg me-2"}, [ + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }}, + m('i', {class: "fa fa-filter"}) + ), + m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }}, + m('i', {class: "fa fa-sort-alpha-asc"}) + ) + ]), + 'Мои заметки', + ]) + ), + m('hr') + ]); + result.push(m(data.filter)); + result.push(m(data.order_by)); + result.push(m(Pagination, data.pagination)); + if (data.notes.length>0) { + result.push(m(ComponentNotes, {notes: data.notes})); + result.push(m(Pagination, data.pagination)); + }; + result.push(breadcrumbs_render()); + return result; + } + } +}; diff --git a/src/myapp/templates/private/domains/note/source.js b/src/myapp/templates/private/domains/note/source.js new file mode 100644 index 0000000..86c86a1 --- /dev/null +++ b/src/myapp/templates/private/domains/note/source.js @@ -0,0 +1,130 @@ +function NoteSource() { + let data = { + note: null, + editor: null, + } + function note_get(id) { + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note', + "params": { + "id": id + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + data.note = response['result']; + } + } + ) + } + function note_update() { + data.note.body = data.editor.getValue(); + m.request({ + url: '/api', + method: "POST", + body: { + "jsonrpc": "2.0", + "method": 'note.update', + "params": { + "id": data.note.id, + "title": data.note.title, + "body": data.note.body + }, + "id": get_id() + } + }).then( + function(response) { + if ('result' in response) { + m.route.set(`/note/${response['result'].id}`); + } + } + ) + } + function editor_init() { + let vCode = document.getElementById('note_body'); + const {EditorState} = CM["@codemirror/state"]; + const {basicSetup, EditorView} = CM["codemirror"]; + + data.editor = {}; + data.editor.state = EditorState.create({ + doc: data.note.body, + extensions: [ + basicSetup, + EditorView.updateListener.of(function(e) { + data.note.body = e.state.doc.toString(); + }), + EditorView.lineWrapping + ] + }); + data.editor.view = new EditorView({ + lineWrapping: true, + state: data.editor.state, + parent: vCode + }); + }; + function editor_remove() { + data.editor = null; + } + return { + oncreate(vnode) { + console.log(vnode); + if (data.note!=null) { + console.log(data.note); + editor_init(); + } + }, + onbeforeremove: function(vnode) { + editor_remove(); + }, + oninit: function(vnode) { + document.title = `Исходный код статьи - ${SETTINGS.TITLE}`; + note_get(vnode.attrs.id); + }, + onupdate(vnode) { + console.log(vnode); + if (data.note!=null) { + if (data.editor==null) { + console.log(data.note); + editor_init(); + } + } + }, + view: function(vnode) { + let result = []; + if (data.note!=null) { + result.push([ + m('div', {class: 'row'}, + m('div', {class: 'col h1 py-1'}, [ + m(m.route.Link, {class: "btn btn-outline-secondary btn-lg", href: `/note/${data.note.id}`, title: data.note.title}, m('i', {class: 'fa fa-chevron-left'})), + {tag: '<', children: ' '}, + 'Исходный код статьи', + m('hr'), + ]) + ), + m(NoteMenu, {menuitem: 'source', note: data.note}), + m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + m('button', {class: 'btn btn-outline-success float-end', type: 'button', onclick: note_update}, [m('i', {class: 'fa fa-save'}), ' Сохранить']), + ]) + ), + m('div', {class: 'form-group mb-2'}, [ + m('label', {class: 'form-label'}, 'Текст'), + m('div', {id: 'note_body', oninput: function (e) {data.note.body = e.target.value}, value: data.note.body}) + ]), + m('div', {class: 'row'}, + m('div', {class: 'col py-2'}, [ + m('button', {class: 'btn btn-outline-success float-end', type: 'button', onclick: note_update}, [m('i', {class: 'fa fa-save'}), ' Сохранить']), + ]) + ), + ]); + }; + return result + } + } +}