Compare commits

...

217 Commits

Author SHA1 Message Date
Garrett Mills fe2ed05271 Drone: only build & push container on tag/promote
continuous-integration/drone Build is passing Details
continuous-integration/drone/promote/production Build is passing Details
1 year ago
Garrett Mills ddff22919a Drone: fix k8s rollout name
continuous-integration/drone Build is passing Details
1 year ago
Garrett Mills 0c64fa661b Drone: add k8s rollout step
continuous-integration/drone Build is failing Details
1 year ago
Garrett Mills db98b5758f Drone: make node.js build use uuid package instead of uuidgen command
continuous-integration/drone Build is passing Details
1 year ago
Garrett Mills cdb17c8e4a Drone: add openssl compat flag
continuous-integration/drone Build is passing Details
1 year ago
Garrett Mills c1c768e8b3 Drone: initial builds
continuous-integration/drone Build is failing Details
1 year ago
Garrett Mills 1b1c1f5f5e Add build tooling for Docker
2 years ago
Garrett Mills 349b8a9e7d Improve print layout - still need to handle virtual scroll
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills f3f8578834
#91 - replace sidebar tree component with custom 1st-party implementation
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 87b99473bd
Add devel info to README
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 858fa25c48
pnpm i monkeypatch; dark mode logout; node-link search fix
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 8db18c9315
Add support for real-time editing in markdown
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 5319af6fe9 Update '.drone.yml'
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 67b44769d5
Drone - monkey patch duplicate type definitions
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills 48c045e952
Add real-time collab support to code editor
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills 7be3bf5259
Logging out now properly redirects to login page
continuous-integration/drone/push Build was killed Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 2284c84897
Remove experiment code
continuous-integration/drone/push Build was killed Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 5888af4331
Make sharing public links generate site viewer links
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills 4a1df4ee6a
Add ability to visit publicly available sites in "site viewer" mode
3 years ago
Garrett Mills 400a985c11
Exclude version query params if no version specified
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 94c6a66dff
Improve version modal previews and remove debugging logging
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills aa27fefef4
Database - in place updates to preserve editing flow
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills b39cab61a8
Fix markdown KaTeX rendering; syntax highlighting for code blocks
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 70df4c4681
tag
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 01c2fc18f2
#4 - add support for sharing and viewing a page publicly without login
3 years ago
Garrett Mills 4f14a40994
Misc login page fixes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 280e39eb37
Downgrade Markdown packages
3 years ago
Garrett Mills 59ce3c43fd
Derp
continuous-integration/drone Build is passing Details
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 8d4627f5f6
Make logo url part of environment
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 28aabc0960
Migrate to PNPM support
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills d99503250a
Right click tree to show node context menu
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 3474fe4cc4
#84 - include SVG logo in build artifacts
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills ce4c524c54
#85 - clean up styles in search interface
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 066e7b85ee
#86 - remove build error property
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 6a75900908
#86 - implement basic bookmarks in sidebar
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills 8c253ac283
Add ability to filter search results by category
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 72ab2064dd
Centralize opener logic to shared service
3 years ago
Garrett Mills 0fecb8a4ba
#84 - Add login page as part of SPA instead of relying on external page
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills aad0aea79a
#75 - support searching file box files in global search, open to correct subdir
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 95948573df
#83 - add loading mask to file box
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills eaa7b69024
#77 - support opening file box in full screen mode
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills ca79913a46
#77 - pass along node ID to file box API, refresh sidebar on name change
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills ed81610f31
#75 - add quick filter to file box
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 53a6d6d316
#78 - file box color for sidebar icon; delete file box on node delete
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 0e0a36db72
#80 - clean up file box read only mode
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills f5258429af
#81 - clean up file box dark/light mode styling
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 388a788b8b
Tag
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills cd8a17471a
Tag
3 years ago
Garrett Mills bb3eda2577
Initial pass of the new file box node
3 years ago
Garrett Mills fc247b3570
Clean up read-only support
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills c300b7cfea
WYSYWIG - only replace links with <a> if they are NOT already in a tag
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills a338347486
#29 - allow uploading multiple files; ajax upload w/o reloading page
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills fac0a07dd1
Fix database load issue; start row drag infra
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 60e08d8a76
Add mutation handling to norm editor; disable socket connection for now
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills c4e641545c
#55 - add checkbox support to database boolean column type
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills bd69f0814d
#58 - add ability to pin database column left or right
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills d2bc04803d
Add hyperlink database column type
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 859b571133
Fix click-to-edit text, member visibility build error
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 7b23e96a61
update wysiwyg to start positioning remote carets more accurately
continuous-integration/drone/push Build is failing Details
continuous-integration/drone Build is failing Details
3 years ago
Garrett Mills 96423d7145
Add ability to drag and reorganize tree pages in sidebar
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills c7f9a59cc4
Start adding real-time collab support to WYSIWYG
continuous-integration/drone/push Build is failing Details
3 years ago
Garrett Mills 8f7ff1de73
Add UI activation callback which will open database when opened from sidebar
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills 9f104e8744
Scroll to element when node is selected in nav sidebar
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 0c93860816
Fix page reload navigation and add more debugging
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
3 years ago
Garrett Mills bbb727bd38
Improve search loading with debounce and loading spinner
continuous-integration/drone/push Build is passing Details
3 years ago
Garrett Mills 37468489fb
Add ability to fetch menu items for a virtual root
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 5eb68b9338
Add form mapping to database interface
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills c69aed2488
Clean up search and strip HTML
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills d954777b89
Make back button dismiss open modal
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 8dde908b97
Fix some browser style consistencies
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 99a1f0103f
Update monaco and refactor usage to remove all internal scrollbars
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 3fd6a54622
Initial form builder support!
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills a8f8c0ebf1
Make page sidebar buttons dock to top of sidebar
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills afc1ded36b
Force package-lock.json regen
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 2510fc0d68
Hide page link icon when no page; make markdown edit rwork better on small width
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 7cb5745dc4
Add new database column type - link to other page
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 0378522e9a
Create db_api service wrapper and OO classes
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 65de891fe8
Fix full-screen database button background color in dark mode
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills df82f2444f
Fixes #37
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills b000ae0a51
Fix file upload endpoint path
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 273ecdfafc
Database performance improvements
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 2b8a7972a0
Add ability to open database in full-screen modal
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 3b14c2dc1c
Database column filters
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills f1a34b7d1f
Markdown editor respects dark mode
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills c4e236a0bf
Allow resizing and saving column widths
4 years ago
Garrett Mills a5dc7f7a19
Refactor application initialization & allow public user load
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 556806ea69
Fix bad package lock
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 8f3cc53ca5
Force online check at boot; more dark mode fixes
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills ab990caca4
Improve offline/online detection
4 years ago
Garrett Mills 0107716183
Database - clean up keyboard nav & prevent needless save on load
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills c85cc00c1f
Database - dynamic col size & mobile style fixes
4 years ago
Garrett Mills 138723929e
Add WYSIWYG column to database (#30)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills c76fc2e82a
Move WYSIWYG editor to separate component
4 years ago
Garrett Mills c4e797b6a8
Make database column order reload after column update
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills b5b36cf614
Confirm before deleting database columns
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 4f777855a5
Automatically stop editing when rich cell editor closes (#2)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills e3402f7501
Associate database column data by UUID, not header name (#31)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 69cfef0193
Fix lingering darkmode style bugs
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 62a6f6532b
Sync offline data when we come back online
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 042494128a
Automatically hyperlink links in WYSIWYG editors
4 years ago
Garrett Mills a9dd16cc64
Fix some dark mode styling
4 years ago
Garrett Mills 826f01d1ab
Fix editor page button background issue
4 years ago
Garrett Mills 4c0fbc7594
Add interface logic for reverting page to previous version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills cd37ea1df1
Add ability to load page version in editor service and show in version modal
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills b2eb33f6a0
Make wysiwyg and markdown editors respect readonly
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills ab811bb54c
Show read-only editor in versions (not actually versioning yet)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 26e8d6ecd6
Every editor page will instantiate its own editor service
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 0e0d237d4f
Start page versions modal
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 36d97c9eca
Finalize support for offline/online sync logic
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 36ea67a9d6
Track offline record modify dates to help with sync processes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 708c029079
Fix database offline record format bug & basic sync logic
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 9258bf4d71
Clean up styling for markdown rendered HTML (#34)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 8190f1704b
Add popup confirm before deleting node (#28)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 0f90515252
Remove editor page button background coloring (#33)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills f788654ff7
Add ability to prefetch and auto-prefetch offline data
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 44026f1306
Make ionic cli a dependency and remove separate build step
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills ad8259e795
cleanup cache dir for .drone.yml
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills fdb98eefaa
Move sftp cache settings into settings key
continuous-integration/drone/push Build was killed Details
4 years ago
Garrett Mills c471bee62f
Add node_modules caching to build
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 021dfe0e3e
Remove scope from manifest
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 1a7fe37b67
Clean up syntax errors in notify json .drone.yml
continuous-integration/drone/push Build was killed Details
4 years ago
Garrett Mills 8155df25e5
Make build notifications more verbose
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 2fbf607707
Add proper catch for fetch errors
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 562c31c947
Add separate index.html and index.prod.html for build
4 years ago
Garrett Mills c541cee5b9
make stat and version requests bypass ngsw
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 22c7dc2c95
add server ping check
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills f98369436e
Don't modify index.html during build - invalidates ngsw cache
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 8de76fcddb
Try removing schema from ngsw config
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 5983e56d11
Double click to edit, double escape to stop
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 6bcb4bf455
Cache pages and page nodes offline
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 8f5ad697b3
Handle top-level page creation for offline
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills fe7e955875
Cache pages and page nodes for offline use
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 380a139de3
Fix build script typo
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 1da11574d3
Update manifest start URL and make drone auto-promote staging
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 67f900c5a7
Change manifest scope
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 4005779c01
Add build-step to generate service worker config with correct prefix
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 2ab5c7a6f5
Add logic for fetching device tokens and using them to resume session
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 294b312641
Add offline cachine for file group elements and contents (not files, though)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 02d8505b05
Add offline caching for databases, database columns, and database entries
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 8de9db08a6
Add offline caching for code editor contents
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
nova38 5737dd23ca Upload files to 'src/assets/icon'
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 2056ba8c5f
Fix index.html template for deploy regex
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 0839d4dd60
Update application icons & manifest info
continuous-integration/drone/push Build is passing Details
4 years ago
nova38 a3abccaa26 Upload files to 'src/assets/icon'
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 42d6245cf2
Add PWA infrastructure
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills f6168b6b7c
Start offline support
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills a72fc72c83
Prompt refresh when build UUID changes (#24)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills a686ffd498
Wrap contents of markdown editor rather than scrolling
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 5053632bf6
Save dark mode to session; auto-save session (Noded/backend#17)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills e1c666e3ad
Write build version to file on deploy (#24)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 07664e29cd
Save page on name change & refresh sidebar (#20)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 292bf6c729
Allow exporting subtrees as HTML (Noded/backend#3)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 6c3ebfe36d
Markdown show up in full-text search results
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 0d329ac18a
On escape, hide WYSIWYG toolbar
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills b588865137
Add markdown editor node
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 74b7cdadc7
Do not reset WYSIWYG editor to beginning on save (#21)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills d7b2b3156c
Add logic to open files returned in search results (#19)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 8ca9b736eb Merge pull request 'editor-refactor' (#18) from editor-refactor into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 9f80842b9f
Add auto-save support and saving indicator
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 9a53faf338
Refactor support for the files component
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills e30e8681e4
Add files type icons and colors
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills fdeea85450
Add ability to reposition nodes and insert before/after
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills d6611d9c82
Remove dead code
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 8b28109ab0
Start logic for adding nodes
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 413fb8b94e
Centralize node type icons
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills ff75876e2d
Finish converting database editor
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills c07f334d60
Start database editor conversion
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills ae24674717
Convert code editor to new format
continuous-integration/drone/push Build is failing Details
4 years ago
Garrett Mills 0a6a775fdb
Re-implement "add node" popover menu with colors
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills b0cf07ab49
Add ability to delete nodes in editor
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 331d40e49c
Add logic to the editor service for saving
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills ef5c53ae04
Finish WYSIWYG editor commands and keybindings
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 2291b99512
Initial editor functionality and data bindings
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 8a9f6d508e
Start new WYSIWYG node editor
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 35eb824b45
Allow searching databases by name (#7)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 468f210d59
Add ability to name databases (#16)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 25085a9bad
Show node icon in sidebar; include node types (#17)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 527697b2fc
Include code snippets in full-text search (#7)
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 28d6986eea
Add support for full-text search (#7)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 6297f9d0f0
Better fonts! (#12)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 3e9a0a03f8
Add session service and startup logic to check for authed user (#15)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills d3af6611c6
Fix sidebar search to force-include virtual nodes (#14)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 6532bd7dc1
Update tree listing component to latest version
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills d36e861502
Upgrade to Ionic 5 and Angular 9 Ivy
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 8e5aee5344
fix deploy site key in CI config
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills ee212dd891
Fix duplicate pipeline step name
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is failing Details
4 years ago
Garrett Mills b3eb3c6e80
Add prod deployment pipeline
continuous-integration/drone/push Build encountered an error Details
4 years ago
Garrett Mills e8baaa0507
Fix escaping of sed command for CI
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 3158b0408d
Finish staging deploy logic
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 288c6ae2eb
Add dev site deploy to Drone CI config
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
4 years ago
Garrett Mills 11bdcf8eae
Fix ionic scrip location for CI
continuous-integration/drone/push Build is passing Details
4 years ago
Garrett Mills 08a08ab4e4
Add drone config for CI
continuous-integration/drone/push Build is failing Details
4 years ago
garrettmills 42f24914d5 add multiselect editor, datetime editor, currency/datetime/boolean renderers, column reordering
4 years ago
garrettmills 1edd696bdb fix manage menu permissions
4 years ago
garrettmills a463334c94 Fix member visibility build error
4 years ago
garrettmills 7916c7966f Add new column types and editors
4 years ago
garrettmills 1bd3795a4a Add new cell editors; refactor editor framework
4 years ago
garrettmills 407d26eb05 Add ^S save binding to editor; fix bullet point delete bug
4 years ago
garrettmills 9f361896ee Implement sub-tree sharing; read-only pages
4 years ago
garrettmills 1eda3d0b30 Add inline host options button and implement horizontal row
4 years ago
garrettmills fd76f43c7e Menu enhancements
4 years ago
garrettmills 6a7618f971 Add suppport for UL; fix file uploader redirects
4 years ago
garrettmills 665fdc91a8 Add options menu and option to export to HTML
4 years ago
garrettmills 7ee79dc1d9 Fix build error
4 years ago
garrettmills 813f4b094b Add file uploader support
4 years ago
garrettmills f4c86a06e2 Add VSCode editor component
4 years ago
Thomas Atkins b5d2f64fd3 Changed menu title to Noded
4 years ago
Thomas Atkins 0813bf62b1 take to log out
4 years ago
Thomas b63598145b Deleted .env
4 years ago
Thomas Atkins 7aac6b0d3e Merge branch 'master' of https://dev.azure.com/HackKu/HackKu%202020/_git/frontend
4 years ago
Thomas Atkins 734ca65231 Added logout
4 years ago
garrettmills dc4f8fe7ed minor usability enhancements
4 years ago
garrettmills 3852d6aab7 fix build errors
4 years ago
garrettmills e97c19f19d finish database implementation
4 years ago
Thomas Atkins e2dd56ab72 Merge branch 'master' of https://dev.azure.com/HackKu/HackKu%202020/_git/frontend
4 years ago
Thomas Atkins 781d9c7bba Fixed chrome title bug, maybe
4 years ago
garrettmills 6594acab1a fix merge error
4 years ago

@ -0,0 +1,58 @@
---
kind: pipeline
type: kubernetes
name: build
metadata:
labels:
pod-security.kubernetes.io/audit: privileged
services:
- name: docker daemon
image: docker:dind
privileged: true
environment:
DOCKER_TLS_CERTDIR: ""
when:
event:
- tag
- promote
steps:
- name: node.js build
image: node:18
commands:
- npm add --global pnpm
- pnpm i
- rm -f ./node_modules/ngx-monaco-editor/lib/monaco.d.ts
- sed -i '1d' ./node_modules/ngx-monaco-editor/lib/types.d.ts
- ./node_modules/.bin/ionic build --prod
- ./node_modules/.bin/ngsw-config ./www/ ./ngsw-config.json /i
- echo -n $(npx uuid) | tee ./www/version.html
environment:
NODE_OPTIONS: --openssl-legacy-provider
- name: container build
image: docker:latest
privileged: true
commands:
- "while ! docker stats --no-stream; do sleep 1; done"
- docker image build -t $DOCKER_REGISTRY/noded/frontend .
- docker push $DOCKER_REGISTRY/noded/frontend
environment:
DOCKER_HOST: tcp://localhost:2375
DOCKER_REGISTRY:
from_secret: DOCKER_REGISTRY
when:
event:
- tag
- promote
- name: k8s rollout
image: bitnami/kubectl:latest
commands:
- kubectl rollout restart -n noded deployment/noded-frontend
when:
event:
- tag
- promote

@ -0,0 +1 @@
shamefully-hoist=true

@ -0,0 +1,3 @@
FROM joseluisq/static-web-server:2
COPY ./www /public/i

@ -0,0 +1,12 @@
# Requirements for frontend development
- Node.js 14.x
- The PNPM package manager
- The Ionic CLI, globally
- `npm --global add @ionic/cli`
## For development:
```sh
pnpm i
ionic serve
```

91
android/.gitignore vendored

@ -0,0 +1,91 @@
# NPM renames .gitignore to .npmignore
# In order to prevent that, we remove the initial "."
# And the CLI then renames it
# Using Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore
# Built application files
*.apk
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

@ -0,0 +1,46 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "io.ionic.starter"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,27 @@
package com.getcapacitor.myapp;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.ionic.starter">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="io.ionic.starter.MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Camera, Photos, input file -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Geolocation API -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />
<!-- Network API -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Navigator.getUserMedia -->
<!-- Video -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Audio -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
</manifest>

@ -0,0 +1,14 @@
{
"appId": "io.ionic.starter",
"appName": "frontend",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "www",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {},
"linuxAndroidStudioPath": "/home/garrettmills/.local/android-studio/bin/studio.sh"
}

@ -0,0 +1,21 @@
package io.ionic.starter;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import java.util.ArrayList;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initializes the Bridge
this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
// Additional plugins you've installed go here
// Ex: add(TotallyAwesomePlugin.class);
}});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">frontend</string>
<string name="title_activity_main">frontend</string>
<string name="package_name">io.ionic.starter</string>
<string name="custom_url_scheme">io.ionic.starter</string>
</resources>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

@ -0,0 +1,17 @@
package com.getcapacitor.myapp;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

188
android/gradlew vendored

@ -0,0 +1,188 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

100
android/gradlew.bat vendored

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

@ -0,0 +1,17 @@
ext {
minSdkVersion = 21
compileSdkVersion = 29
targetSdkVersion = 29
androidxAppCompatVersion = '1.1.0'
androidxCoreVersion = '1.2.0'
androidxMaterialVersion = '1.1.0-rc02'
androidxBrowserVersion = '1.2.0'
androidxLocalbroadcastmanagerVersion = '1.0.0'
androidxExifInterfaceVersion = '1.2.0'
firebaseMessagingVersion = '20.1.2'
playServicesLocationVersion = '17.0.0'
junitVersion = '4.12'
androidxJunitVersion = '1.1.1'
androidxEspressoCoreVersion = '3.2.0'
cordovaAndroidVersion = '7.0.0'
}

@ -25,21 +25,41 @@
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
},
{ "glob": "**/*", "input": "node_modules/ngx-monaco-editor/assets/monaco", "output": "./assets/monaco/" },
"src/manifest.webmanifest"
],
"styles": [
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css"
},
{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
},
{
"input": "src/assets/font/fonts.css"
},
{
"input": "node_modules/katex/dist/katex.min.css"
}
],
"scripts": []
"scripts": [
"node_modules/marked/lib/marked.js",
"node_modules/katex/dist/katex.min.js"
]
},
"configurations": {
"production": {
@ -49,6 +69,10 @@
"with": "src/environments/environment.prod.ts"
}
],
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
@ -62,9 +86,15 @@
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"ci": {
"progress": false
@ -111,7 +141,8 @@
"glob": "**/*",
"input": "src/assets",
"output": "/assets"
}
},
"src/manifest.webmanifest"
]
},
"configurations": {

@ -0,0 +1,14 @@
{
"appId": "io.ionic.starter",
"appName": "frontend",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "www",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {},
"linuxAndroidStudioPath": "/home/garrettmills/.local/android-studio/bin/studio.sh"
}

@ -0,0 +1,30 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

23625
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,35 +9,60 @@
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"e2e": "ng e2e",
"build:prod": "rm -f ./node_modules/ngx-monaco-editor/lib/monaco.d.ts && sed -i '1d' ./node_modules/ngx-monaco-editor/lib/types.d.ts && ionic build --prod && ngsw-config ./www/ ./ngsw-config.json /i && echo -n $(uuidgen) | tee ./www/version.html",
"docker:build": "docker build -t ${DOCKER_REGISTRY}/noded/frontend .",
"docker:push": "docker push ${DOCKER_REGISTRY}/noded/frontend",
"postinstall": "sed -i '/^declare let MonacoEnvironment/d' node_modules/ngx-monaco-editor/lib/monaco.d.ts"
},
"private": true,
"dependencies": {
"@angular/common": "~8.1.2",
"@angular/core": "~8.1.2",
"@angular/forms": "~8.1.2",
"@angular/platform-browser": "~8.1.2",
"@angular/platform-browser-dynamic": "~8.1.2",
"@angular/router": "~8.1.2",
"@angular/common": "~10.1.5",
"@angular/core": "~10.1.5",
"@angular/forms": "~10.1.5",
"@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/pwa": "^0.1001.7",
"@angular/router": "~10.1.5",
"@angular/service-worker": "~10.1.5",
"@ckeditor/ckeditor5-angular": "^2.0.1",
"@ckeditor/ckeditor5-build-decoupled-document": "^27.0.0",
"@convergencelabs/monaco-collab-ext": "^0.3.2",
"@fortawesome/fontawesome-free": "^5.15.1",
"@ionic-native/core": "^5.0.0",
"@ionic-native/splash-screen": "^5.0.0",
"@ionic-native/status-bar": "^5.0.0",
"@ionic/angular": "^4.7.1",
"angular-tree-component": "^8.5.2",
"@ionic/angular": "^5.3.5",
"@ionic/cli": "^6.12.0",
"@ng-stack/contenteditable": "^1.1.0",
"ag-grid-angular": "^24.1.0",
"ag-grid-community": "^24.1.0",
"angular-resize-event": "^2.0.1",
"core-js": "^2.5.4",
"rxjs": "~6.5.1",
"dexie": "^3.0.2",
"highlight.js": "^10.6.0",
"ionic-selectable": "^4.7.1",
"katex": "0.12.0",
"marked": "1.2.5",
"moment": "^2.24.0",
"ng-connection-service": "^1.0.4",
"ngx-highlightjs": "^4.1.3",
"ngx-markdown": "^10.1.1",
"ngx-monaco-editor": "^9.0.0",
"rxjs": "~6.6.3",
"tslib": "^1.9.0",
"zone.js": "~0.9.1"
"uuid": "^3.4.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/architect": "~0.801.2",
"@angular-devkit/build-angular": "~0.801.2",
"@angular-devkit/core": "~8.1.2",
"@angular-devkit/schematics": "~8.1.2",
"@angular/cli": "~8.1.2",
"@angular/compiler": "~8.1.2",
"@angular/compiler-cli": "~8.1.2",
"@angular/language-service": "~8.1.2",
"@angular-devkit/build-angular": "~0.1001.6",
"@angular-devkit/core": "~10.1.6",
"@angular-devkit/schematics": "^10.1.6",
"@angular/cli": "^10.1.6",
"@angular/compiler": "~10.1.5",
"@angular/compiler-cli": "~10.1.5",
"@angular/language-service": "~10.1.5",
"@ionic/angular-toolkit": "^2.1.1",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
@ -53,7 +78,7 @@
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.4.3"
"typescript": "~4.0.3"
},
"description": "An Ionic project"
}

File diff suppressed because it is too large Load Diff

@ -2,6 +2,8 @@
"/link_api": {
"target": "http://localhost:8000",
"secure": false,
"ws": true,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {"^/link_api": ""}
}

@ -1,5 +1,8 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import {LoginPage} from './pages/login/login.page';
import {AuthService} from './service/auth.service';
import {GuestOnlyGuard} from './service/guard/GuestOnly.guard';
const routes: Routes = [
{
@ -9,15 +12,17 @@ const routes: Routes = [
},
{
path: 'home',
canActivate: [AuthService],
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'list',
loadChildren: () => import('./list/list.module').then(m => m.ListPageModule)
path: 'editor',
loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule)
},
{
path: 'editor',
loadChildren: () => import('./pages/editor/editor.module').then( m => m.EditorPageModule)
path: 'login',
canActivate: [GuestOnlyGuard],
component: LoginPage,
}
];

@ -1,48 +1,56 @@
<ion-app class="dark">
<ion-split-pane when="sm">
<ion-menu class="sidebar">
<ion-split-pane contentId="main-content" *ngIf="ready$ | async">
<ion-menu class="sidebar no-print" menuId="main-menu" contentId="main-content" content="content" type="push" side="start" *ngIf="!api.isPublicUser">
<ion-header>
<ion-toolbar color="primary">
<ion-title>Menu</ion-title>
<ion-title style="font-weight: bold; color: white;">{{ appName }}
<ion-menu-toggle menu="first" autoHide="false"></ion-menu-toggle>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header>
Navigate
<ion-buttons class="ion-padding-end">
<ion-button fill="outline" color="light" (click)="onTopLevelCreate()">
<ion-icon color="primary" name="add-circle"></ion-icon>
<ion-buttons>
<ion-button fill="outline" [color]="refreshingMenu ? 'success' : 'light'" (click)="onMenuRefresh()">
<ion-icon color="tertiary" name="refresh"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" (click)="onChildCreate()" [disabled]="!addChildTarget">
<ion-icon color="primary" name="add-circle"></ion-icon>&nbsp;<span class="button-text">Child</span>
<ion-button fill="outline" color="light" (click)="onCreateClick($event)">
<ion-icon color="primary" name="add-circle"></ion-icon>&nbsp;<span class="button-text">Create</span>
</ion-button>
<ion-button fill="outline" color="light" (click)="onDeleteClick()" [disabled]="!deleteTarget">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" (click)="onNodeMenuClick($event)" [disabled]="!menuTarget || !menuTarget.id">
<i class="fa fa-ellipsis-v" style="color: darkgrey"></i>
</ion-button>
<ion-button fill="outline" color="light" (click)="onVirtualRootClear($event)" *ngIf="virtualRootPageId" title="Show entire tree">
<i class="fa fa-search-minus" style="color: darkgrey"></i>
</ion-button>
</ion-buttons>
</ion-list-header>
<tree-root [nodes]="nodes" [options]="options"></tree-root>
</ion-list>
</ion-header>
<ion-content>
<app-tree-root
#menuTree
[items]="nodes"
iconClassField="faIconClass"
(itemSelected)="onMenuItemClick($event)"
(itemActivated)="onMenuItemActivate($event)"
(itemRightClicked)="onMenuItemRightClick($event)"
></app-tree-root>
</ion-content>
<ion-footer>
<ion-item slot="end" lines="full">
<ion-icon slot="start" name="moon"></ion-icon>
<ion-label>
Dark mode
</ion-label>
<ion-toggle (ionChange)="toggleDark()" id="themeToggle" slot="end"></ion-toggle>
<ion-searchbar
placeholder="Quick filter"
(ionChange)="onMenuFilterChange($event)"
></ion-searchbar>
<ion-item button lines="full" (click)="showOptions($event)">
<ion-icon name="list" slot="start"></ion-icon>
<ion-label>Menu</ion-label>
</ion-item>
</ion-footer>
</ion-menu>
<div class="ion-page" main>
<ion-header> </ion-header>
<ion-content class="ion-padding">
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-content>
</div>
<ion-router-outlet id="main-content" #content main></ion-router-outlet>
</ion-split-pane>
</ion-app>
</ion-app>

@ -6,3 +6,33 @@
.button-text {
color: var(--ion-color-medium-shade);
}
.tree-node-container {
.tree-node-icon {
margin-right: 10px;
}
&.page {
.tree-node-icon {
color: var(--noded-background-note);
}
}
&.db {
.tree-node-icon {
color: var(--noded-background-db);
}
}
&.code {
.tree-node-icon {
color: var(--noded-background-code);
}
}
&.files, &.file_box {
.tree-node-icon {
color: var(--noded-background-files);
}
}
}

@ -1,67 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy;
beforeEach(async(() => {
statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
platformReadySpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: StatusBar, useValue: statusBarSpy },
{ provide: SplashScreen, useValue: splashScreenSpy },
{ provide: Platform, useValue: platformSpy },
],
imports: [ RouterTestingModule.withRoutes([])],
}).compileComponents();
}));
it('should create the app', async () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it('should initialize the app', async () => {
TestBed.createComponent(AppComponent);
expect(platformSpy.ready).toHaveBeenCalled();
await platformReadySpy;
expect(statusBarSpy.styleDefault).toHaveBeenCalled();
expect(splashScreenSpy.hide).toHaveBeenCalled();
});
it('should have menu labels', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-label');
expect(menuItems.length).toEqual(2);
expect(menuItems[0].textContent).toContain('Home');
expect(menuItems[1].textContent).toContain('List');
});
it('should have urls', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-item');
expect(menuItems.length).toEqual(2);
expect(menuItems[0].getAttribute('ng-reflect-router-link')).toEqual('/home');
expect(menuItems[1].getAttribute('ng-reflect-router-link')).toEqual('/list');
});
});

@ -1,12 +1,31 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit, ViewChild, HostListener} from '@angular/core';
import { AlertController, Platform } from '@ionic/angular';
import {
AlertController,
ModalController,
Platform,
PopoverController,
LoadingController,
ToastController, NavController
} from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ApiService } from './service/api.service';
import { Router } from '@angular/router';
import { TREE_ACTIONS } from 'angular-tree-component';
import { Observable } from 'rxjs';
import {BehaviorSubject, Observable} from 'rxjs';
import {OptionPickerComponent} from './components/option-picker/option-picker.component';
import {OptionMenuComponent} from './components/option-menu/option-menu.component';
import {SelectorComponent} from './components/sharing/selector/selector.component';
import {SessionService} from './service/session.service';
import {SearchComponent} from './components/search/Search.component';
import {NodeTypeIcons} from './structures/node-types';
import {NavigationService} from './service/navigation.service';
import {DatabaseService} from './service/db/database.service';
import {EditorService} from './service/editor.service';
import {debug} from './utility';
import {AuthService} from './service/auth.service';
import {OpenerService} from './service/opener.service';
import {TreeRootComponent} from './components/tree-root/tree-root.component';
@Component({
selector: 'app-root',
@ -14,47 +33,371 @@ import { Observable } from 'rxjs';
styleUrls: ['app.component.scss']
})
export class AppComponent implements OnInit {
@ViewChild('menuTree') menuTree: TreeRootComponent;
public readonly ready$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public addChildTarget: any = false;
public deleteTarget: any = false;
public lastClickEvent: Array<any> = [];
public menuTarget: any = false;
public refreshingMenu = false;
public lastClickEvent?: {event: MouseEvent, item: any};
public nodes = [];
public options = {
actionMapping: {
mouse: {
dblClick: (tree, node, $event) => {
console.log({ tree, node, $event });
const id = node.data.id;
this.router.navigate(['/editor', { id }]);
},
click: (tree, node, $event) => {
console.log('click', { tree, node, $event });
TREE_ACTIONS.FOCUS(tree, node, $event);
this.addChildTarget = node;
this.deleteTarget = node;
this.lastClickEvent = [tree, node, $event];
}
}
}
};
public virtualRootPageId?: string;
public typeIcons = NodeTypeIcons;
public get appName(): string {
return this.session.appName || 'Noded';
}
public darkMode = false;
protected loader?: any;
protected hasSearchOpen = false;
protected versionInterval?: any;
protected showedNewVersionAlert = false;
protected showedOfflineAlert = false;
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private api: ApiService,
public readonly api: ApiService,
protected router: Router,
protected alerts: AlertController
) {
this.initializeApp();
protected alerts: AlertController,
protected popover: PopoverController,
protected modal: ModalController,
protected session: SessionService,
protected loading: LoadingController,
protected navService: NavigationService,
protected toasts: ToastController,
protected db: DatabaseService,
protected editor: EditorService,
protected auth: AuthService,
protected opener: OpenerService,
) { }
public onMenuItemClick(event: {event: MouseEvent, item: any}) {
this.addChildTarget = false;
this.deleteTarget = false;
this.menuTarget = false;
if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) {
this.addChildTarget = event.item;
}
if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) {
this.deleteTarget = event.item;
}
this.menuTarget = event.item;
this.lastClickEvent = event;
}
public onMenuItemActivate(event: {event: MouseEvent, item: any}) {
this.navigateEditorToNode(event.item);
}
public onMenuItemRightClick(event: {event: MouseEvent, item: any}) {
event.event.preventDefault();
event.event.stopPropagation();
this.addChildTarget = false;
this.deleteTarget = false;
this.menuTarget = false;
if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) {
this.addChildTarget = event.item;
}
if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) {
this.deleteTarget = event.item;
}
this.menuTarget = event.item;
this.lastClickEvent = event;
this.onNodeMenuClick(event.event, true);
}
public onMenuFilterChange(event) {
const filterValue = event?.detail?.value;
debug('Filtering tree:', filterValue);
this.menuTree?.filterTree(filterValue);
}
async checkNewVersion() {
if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) {
const toast = await this.toasts.create({
cssClass: 'compat-toast-container',
header: 'Update Available',
message: `A new version of ${this.appName} is available. Please refresh to update.`,
buttons: [
{
side: 'end',
text: 'Refresh',
handler: () => {
window.location.reload();
},
},
],
});
this.showedNewVersionAlert = true;
await toast.present();
}
}
ngOnInit() {
debug('Initializing application.');
this.initializeApp();
}
@HostListener('window:popstate', ['$event'])
dismissModal(event) {
const modal = this.modal.getTop();
if ( modal ) {
event.preventDefault();
event.stopPropagation();
this.modal.dismiss();
}
}
showOptions($event) {
this.popover.create({
event: $event,
component: OptionPickerComponent,
componentProps: {
toggleDark: () => this.toggleDark(),
isDark: () => this.isDark(),
showSearch: () => this.handleKeyboardEvent(),
isPrefetch: () => this.isPrefetch(),
togglePrefetch: () => this.togglePrefetch(),
doPrefetch: () => this.doPrefetch(),
}
}).then(popover => popover.present());
}
@HostListener('document:keyup.control./', ['$event'])
async handleKeyboardEvent() {
if ( this.hasSearchOpen ) {
return;
}
const modal = await this.modal.create({
component: SearchComponent,
cssClass: 'modal-med',
});
const modalState = {
modal : true,
desc : 'Search everything'
};
history.pushState(modalState, null);
this.hasSearchOpen = true;
await modal.present();
await modal.onDidDismiss();
this.hasSearchOpen = false;
}
public navigateEditorToNode(node: any) {
if ( !node.data ) {
node = { data: node };
}
const id = node.data.id;
const nodeId = node.data.node_id;
if ( !node.data.virtual ) {
debug('Navigating editor to node:', {id, nodeId});
this.opener.currentPageId = id;
this.opener.openTarget(id, nodeId);
}
}
async onNodeMenuClick($event, fromContextMenu = false) {
let canManage = this.menuTarget.level === 'manage';
if ( !canManage ) {
if ( !this.menuTarget.level ) {
canManage = true;
}
}
if ( !this.menuTarget.id ) {
return;
}
const options = [
{name: 'Make Virtual Root', icon: 'fa fa-search-plus', value: 'virtual_root'},
{name: 'Export to HTML', icon: 'fa fa-file-export', value: 'export_html'},
// {name: 'Export as PDF', icon: 'fa fa-file-export', value: 'export_pdf'},
];
const manageOptions = [
...(fromContextMenu ? this.getCreateNodeMenuItems() : []),
{name: 'Share Sub-Tree', icon: 'fa fa-share-alt', value: 'share'},
{name: 'Delete Sub-Tree', icon: 'fa fa-trash noded-danger', value: 'delete'},
];
if ( this.menuTarget.bookmark ) {
options.push({name: 'Remove Bookmark', icon: 'fa fa-star', value: 'bookmark_remove'});
} else {
options.push({name: 'Bookmark', icon: 'fa fa-star', value: 'bookmark_add'});
}
const popover = await this.popover.create({
component: OptionMenuComponent,
componentProps: {
menuItems: [
...(!canManage ? options : [...options, ...manageOptions]),
],
},
event: $event,
});
popover.onDidDismiss().then((result) => {
if ( result.data === 'share' ) {
this.modal.create({
component: SelectorComponent,
cssClass: 'modal-med',
componentProps: {
node: this.menuTarget,
}
}).then(modal => {
const modalState = {
modal : true,
desc : 'Share page'
};
history.pushState(modalState, null);
modal.present();
});
} else if ( result.data === 'export_html' ) {
this.exportTargetAsHTML();
} else if ( result.data === 'export_pdf' ) {
// this.exportTargetAsPDF();
} else if ( result.data === 'virtual_root' ) {
this.setVirtualRoot();
} else if ( result.data === 'bookmark_add' ) {
this.addBookmark();
} else if ( result.data === 'bookmark_remove' ) {
this.removeBookmark();
} else if ( result.data === 'top-level' ) {
this.onTopLevelCreate();
} else if ( result.data === 'child' ) {
this.onChildCreate();
} else if ( result.data === 'form' ) {
this.onChildCreate('form');
} else if ( result.data === 'delete' ) {
this.onDeleteClick();
}
});
await popover.present();
}
async setVirtualRoot() {
if ( this.menuTarget && this.menuTarget?.type === 'page' ) {
debug('virtual root menu target', this.menuTarget);
this.virtualRootPageId = this.menuTarget.id;
this.reloadMenuItems().subscribe();
}
}
onVirtualRootClear(event) {
delete this.virtualRootPageId;
this.reloadMenuItems().subscribe();
}
async exportTargetAsHTML() {
const exportRecord: any = await new Promise((res, rej) => {
const reqData = {
format: 'html',
PageId: this.menuTarget.id,
};
this.api.post(`/exports/subtree`, reqData).subscribe({
next: (result) => {
res(result.data);
},
error: rej
});
});
const dlUrl = this.api._build_url(`/exports/${exportRecord.UUID}/download`);
window.open(dlUrl, '_blank');
}
addBookmark() {
const bookmarks = this.session.get('user.preferences.bookmark_page_ids') || [];
if ( !bookmarks.includes(this.menuTarget.id) ) {
bookmarks.push(this.menuTarget.id);
}
this.session.set('user.preferences.bookmark_page_ids', bookmarks);
this.session.save().then(() => this.navService.requestSidebarRefresh({ quiet: true }));
}
removeBookmark() {
let bookmarks = this.session.get('user.preferences.bookmark_page_ids') || [];
bookmarks = bookmarks.filter(x => x !== this.menuTarget.id);
this.session.set('user.preferences.bookmark_page_ids', bookmarks);
this.session.save().then(() => this.navService.requestSidebarRefresh({ quiet: true }));
}
getCreateNodeMenuItems() {
return [
{
name: 'Create Top-Level Note',
icon: 'fa fa-sticky-note noded-note',
value: 'top-level',
title: 'Create a new top-level note page',
},
...(this.addChildTarget ? [
{
name: 'Create Child Note',
icon: 'fa fa-sticky-note noded-note',
value: 'child',
title: 'Create a note page as a child of the given note',
},
{
name: 'Create Form',
icon: 'fa fa-clipboard-list noded-form',
value: 'form',
title: 'Create a new form page as a child of the given note',
},
] : []),
];
}
async onCreateClick($event: MouseEvent) {
const menuItems = this.getCreateNodeMenuItems();
const popover = await this.popover.create({
event: $event,
component: OptionMenuComponent,
componentProps: {
menuItems,
},
});
popover.onDidDismiss().then(({ data: value }) => {
if ( value === 'top-level' ) {
this.onTopLevelCreate();
} else if ( value === 'child' ) {
this.onChildCreate();
} else if ( value === 'form' ) {
this.onChildCreate('form');
}
});
await popover.present();
}
async onTopLevelCreate() {
const alert = await this.alerts.create({
header: 'Create Page',
@ -76,10 +419,9 @@ export class AppComponent implements OnInit {
{
text: 'Create',
handler: async args => {
this.api.post('/page/create', args).subscribe(res => {
this.router.navigate(['/editor', { id: res.data.UUID }]);
this.reloadMenuItems().subscribe();
});
const page = await this.editor.createPage(args.name);
this.reloadMenuItems().subscribe();
await this.router.navigate(['/editor', { id: page.UUID }]);
}
}
]
@ -88,7 +430,7 @@ export class AppComponent implements OnInit {
await alert.present();
}
async onChildCreate() {
async onChildCreate(pageType?: string) {
const alert = await this.alerts.create({
header: 'Create Sub-Page',
message: 'Please enter a new name for the page:',
@ -111,15 +453,11 @@ export class AppComponent implements OnInit {
handler: async args => {
args = {
name: args.name,
parentId: this.addChildTarget.data.id
parentId: this.addChildTarget.id,
pageType,
};
this.api.post('/page/create-child', args).subscribe(res => {
this.reloadMenuItems().subscribe(() => {
TREE_ACTIONS.EXPAND(
this.lastClickEvent[0],
this.lastClickEvent[1],
this.lastClickEvent[2]
);
this.router.navigate(['/editor', { id: res.data.UUID }]);
});
});
@ -135,7 +473,7 @@ export class AppComponent implements OnInit {
const alert = await this.alerts.create({
header: 'Delete page?',
message:
'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?',
'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?',
buttons: [
{
text: 'Keep It',
@ -145,11 +483,16 @@ export class AppComponent implements OnInit {
text: 'Delete It',
handler: async () => {
this.api
.post(`/page/delete/${this.deleteTarget.data.id}`)
.post(`/page/delete/${this.deleteTarget.id}`)
.subscribe(res => {
if ( this.opener.currentPageId === this.deleteTarget.id ) {
this.router.navigate(['/home']);
}
this.reloadMenuItems().subscribe();
this.deleteTarget = false;
this.addChildTarget = false;
this.menuTarget = false;
});
}
}
@ -159,27 +502,321 @@ export class AppComponent implements OnInit {
await alert.present();
}
onMenuRefresh(quiet = false) {
if ( !quiet ) {
this.refreshingMenu = true;
}
this.reloadMenuItems().subscribe();
setTimeout(() => {
if ( !quiet ) {
this.refreshingMenu = false;
}
}, 2000);
}
reloadMenuItems() {
return new Observable(sub => {
this.api.get('/menu/items').subscribe(result => {
this.nodes = result.data;
this.api.getMenuItems(false, this.virtualRootPageId).then(nodes => {
this.nodes = nodes;
sub.next();
sub.complete();
});
});
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
async initializeApp() {
const initializedOnce = this.navService.initialized$.getValue();
if ( this.isDark() ) {
this.toggleDark();
}
debug('app', this);
this.loader = await this.loading.create({
message: 'Setting things up...',
cssClass: 'noded-loading-mask',
showBackdrop: true,
});
debug('Initializing platform and database...');
await this.loader.present();
await this.platform.ready();
await this.db.createSchemata();
let toast: any;
if ( !initializedOnce ) {
debug('Subscribing to offline changes...');
this.api.offline$.subscribe(async isOffline => {
if ( isOffline && !this.showedOfflineAlert ) {
debug('Application went offline!');
toast = await this.toasts.create({
cssClass: 'compat-toast-container',
message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
});
this.showedOfflineAlert = true;
await toast.present();
} else if ( !isOffline && this.showedOfflineAlert ) {
debug('Appliation went online!');
await toast.dismiss();
this.showedOfflineAlert = false;
await this.api.syncOfflineData();
}
});
}
debug('Getting initial status...');
let stat: any = await this.session.stat();
debug('Got stat:', stat);
this.api.isPublicUser = !!stat.public_user;
this.api.isAuthenticated = !!stat.authenticated_user;
this.api.systemBase = stat.system_base;
if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
debug('Unauthenticated or public user...');
if ( !this.api.isOffline ) {
debug('Trying to resume session...');
await this.api.resumeSession();
debug('Checking new status...');
stat = await this.session.stat();
debug('Got session resume stat:', stat);
this.api.isAuthenticated = stat.authenticated_user;
this.api.isPublicUser = stat.public_user;
if ( !stat.authenticated_user ) {
debug('Not authenticated! Redirecting.');
window.location.href = `${stat.system_base}start`;
return;
}
} else {
debug('Unauthenticated offline user. Purging local data!');
await this.db.purge();
window.location.href = `${stat.system_base}start`;
return;
}
}
debug('Set app name and system base:', stat.app_name, stat.system_base);
this.session.appName = stat.app_name;
this.session.systemBase = stat.system_base;
debug('Initializing session...');
await this.session.initialize();
if ( this.session.get('user.preferences.dark_mode') && !this.darkMode ) {
this.toggleDark();
}
debug('Hiding native splash screen & setting status bar styles...');
await this.statusBar.styleDefault();
await this.splashScreen.hide();
// If we went online after being offline, sync the local data
if ( !this.api.isOffline && await this.api.needsSync() ) {
this.loader.message = 'Syncing data...';
try {
await this.api.syncOfflineData();
} catch (e) {
this.toasts.create({
cssClass: 'compat-toast-container',
message: 'An error occurred while syncing offline data. Not all data was saved.',
buttons: [
'Okay'
],
}).then(tst => {
tst.present();
});
}
}
if ( this.isPrefetch() && !this.api.isPublicUser ) {
debug('Pre-fetching offline data...');
this.loader.message = 'Downloading data...';
try {
await this.api.prefetchOfflineData();
} catch (e) {
debug('Pre-fetch error:', e);
this.toasts.create({
cssClass: 'compat-toast-container',
message: 'An error occurred while pre-fetching offline data. Not all data was saved.',
buttons: [
'Okay'
],
}).then(tst => {
tst.present();
});
}
}
this.navService.initialized$.next(true);
if ( !this.api.isPublicUser && this.session.get('user.preferences.default_page') ) {
debug('Navigating to default page!');
const id = this.session.get('user.preferences.default_page');
const node = this.findNode(id);
if ( node ) {
this.navigateEditorToNode(node);
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
if ( !initializedOnce ) {
debug('Creating menu subscription...');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
this.onMenuRefresh(quiet);
});
this.navService.navigationRequest$.subscribe(request => {
debug('Page navigation request: ', request);
if ( !request.pageId ) {
debug('Empty page ID. Will not navigate.');
return;
}
this.opener.currentPageId = request.pageId;
this.router.navigate(['/editor', {
id: request.pageId,
...(request.nodeId ? { node_id: request.nodeId } : {}),
}]);
});
this.navService.initializationRequest$.subscribe((count) => {
if ( count === 0 ) {
return;
}
this.initializeApp().then(() => {
this.router.navigate(['/login']);
});
});
}
debug('Reloading menu items...');
this.reloadMenuItems().subscribe(() => {
debug('Reloaded menu items. Displaying interface.');
this.ready$.next(true);
setTimeout(() => {
this.loader.dismiss();
// this.menuTree?.treeModel?.expandAll();
}, 10);
if ( !this.versionInterval ) {
this.versionInterval = setInterval(() => {
debug('Checking for new application version.');
this.checkNewVersion();
}, 1000 * 60 * 5); // Check for new version every 5 mins
}
});
this.auth.authInProgress = false;
}
async doPrefetch() {
if ( this.api.isOffline ) {
return;
}
this.loader = await this.loading.create({
message: 'Pre-fetching data...',
cssClass: 'noded-loading-mask',
showBackdrop: true,
});
await new Promise(res => setTimeout(res, 2000));
await this.loader.present();
try {
if (await this.api.needsSync()) {
this.loader.message = 'Syncing data...';
await this.api.syncOfflineData();
}
this.loader.message = 'Downloading data...';
await this.api.prefetchOfflineData();
} catch (e) {
const msg = await this.alerts.create({
header: 'Uh, oh!',
message: 'An unexpected error occurred while trying to sync offline data, and we were unable to continue.',
buttons: [
'OK',
],
});
await msg.present();
}
this.loader.dismiss();
}
toggleDark() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
// const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
this.darkMode = !this.darkMode;
console.log('toggel Dark mode');
this.session.set('user.preferences.dark_mode', this.darkMode);
document.body.classList.toggle('dark', this.darkMode);
}
togglePrefetch() {
this.session.set('user.preferences.auto_prefetch', !this.isPrefetch());
}
findNode(id: string, nodes = this.nodes) {
for ( const node of nodes ) {
if ( node.id === id ) {
return node;
}
if ( node.children ) {
const foundNode = this.findNode(id, node.children);
if ( foundNode ) {
return foundNode;
}
}
}
}
isDark() {
return !!this.darkMode;
}
isPrefetch() {
return !!this.session.get('user.preferences.auto_prefetch');
}
async onTreeNodeMove({ node, to }) {
if ( this.api.isOffline ) {
debug('Cannot move node. API is offline.');
return;
}
const { parent } = to;
debug('Moving node:', { node, parent });
try {
await this.api.moveMenuNode(node.id, to.parent.id);
} catch (error) {
console.error('Error moving tree node:', error);
this.alerts.create({
header: 'Error Moving Node',
message: error.message,
buttons: [
{
text: 'Okay',
role: 'cancel',
},
],
}).then(x => x.present());
}
await this.reloadMenuItems().toPromise();
}
}

@ -1,16 +1,41 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouteReuseStrategy } from "@angular/router";
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from "@ionic/angular";
import { SplashScreen } from "@ionic-native/splash-screen/ngx";
import { StatusBar } from "@ionic-native/status-bar/ngx";
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from "./app.component";
import { AppRoutingModule } from "./app-routing.module";
import { HttpClientModule } from "@angular/common/http";
import { ComponentsModule } from "./components/components.module";
import { TreeModule } from "angular-tree-component";
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { ComponentsModule } from './components/components.module';
import {AgGridModule} from 'ag-grid-angular';
import {MonacoEditorModule} from 'ngx-monaco-editor';
import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import {MarkdownModule, MarkedOptions} from 'ngx-markdown';
import {ConnectionServiceModule} from 'ng-connection-service';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { AngularResizedEventModule } from 'angular-resize-event';
import { IonicSelectableModule } from 'ionic-selectable';
import * as hljs from 'highlight.js';
import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
/**
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
* This is an exported function, instead of a private function or inline lambda, to prevent this error:
*
* `Error encountered resolving symbol values statically.`
* `Function calls are not supported.`
* `Consider replacing the function or lambda with a reference to an exported function.`
*
* @param platformLocation an Angular service used to interact with a browser's URL
* @return a string instance of the `<base href="" />` value from `index.html`
*/
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}
@NgModule({
declarations: [AppComponent],
@ -21,12 +46,45 @@ import { TreeModule } from "angular-tree-component";
AppRoutingModule,
HttpClientModule,
ComponentsModule,
TreeModule.forRoot()
AgGridModule.withComponents([]),
MonacoEditorModule.forRoot(),
HighlightModule,
MarkdownModule.forRoot({
markedOptions: {
provide: MarkedOptions,
useValue: {
highlight(code: string, lang: string, callback?: (error: any, code: string) => void): string {
const highlighted = hljs.highlight(lang, code, true);
if ( callback ) {
callback(null, highlighted.value);
}
return highlighted.value;
},
},
}
}),
ConnectionServiceModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
AngularResizedEventModule,
IonicSelectableModule,
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: APP_BASE_HREF,
useFactory: getBaseHref,
deps: [PlatformLocation]
},
{
provide: HIGHLIGHT_OPTIONS,
useValue: {
fullLibraryLoader: () => import('highlight.js'),
}
},
],
bootstrap: [AppComponent]
})

@ -1,11 +1,202 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HostComponent } from './editor/host/host.component';
import {NodePickerComponent} from './editor/node-picker/node-picker.component';
import {IonicModule} from '@ionic/angular';
import {DatabaseComponent} from './editor/database/database.component';
import {AgGridModule} from 'ag-grid-angular';
import {ColumnsComponent} from './editor/database/columns/columns.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ContenteditableModule} from '@ng-stack/contenteditable';
import {CodeComponent} from './editor/code/code.component';
import {MonacoEditorModule} from 'ngx-monaco-editor';
import {IonicSelectableModule} from 'ionic-selectable';
import {FilesComponent} from './editor/files/files.component';
import {OptionPickerComponent} from './option-picker/option-picker.component';
import {HostOptionsComponent} from './editor/host-options/host-options.component';
import {OptionMenuComponent} from './option-menu/option-menu.component';
import {SelectorComponent} from './sharing/selector/selector.component';
import {NumericEditorComponent} from './editor/database/editors/numeric/numeric-editor.component';
import {ParagraphEditorComponent} from './editor/database/editors/paragraph/paragraph-editor.component';
import {ParagraphModalComponent} from './editor/database/editors/paragraph/paragraph-modal.component';
import {BooleanEditorComponent} from './editor/database/editors/boolean/boolean-editor.component';
import {SelectEditorComponent} from './editor/database/editors/select/select-editor.component';
import {MultiSelectEditorComponent} from './editor/database/editors/select/multiselect-editor.component';
import {DatetimeEditorComponent} from './editor/database/editors/datetime/datetime-editor.component';
import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component';
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
import {SearchComponent} from './search/Search.component';
import {TreeRootComponent} from './tree-root/tree-root.component';
import {NormComponent} from './nodes/norm/norm.component';
import {MarkdownComponent as MarkdownEditorComponent} from './nodes/markdown/markdown.component';
import {DirectivesModule} from '../directives/directives.module';
import {MarkdownModule} from 'ngx-markdown';
import {VersionModalComponent} from './version-modal/version-modal.component';
import {EditorPageRoutingModule} from '../pages/editor/editor-routing.module';
import {ViewerPageRoutingModule} from '../pages/viewer/viewer-routing.module';
import {EditorPage} from '../pages/editor/editor.page';
import {ViewerPage} from '../pages/viewer/viewer.page';
import {LoginPage} from '../pages/login/login.page';
import {WysiwygComponent} from './wysiwyg/wysiwyg.component';
import {WysiwygEditorComponent} from './editor/database/editors/wysiwyg/wysiwyg-editor.component';
import {WysiwygModalComponent} from './editor/database/editors/wysiwyg/wysiwyg-modal.component';
import {AngularResizedEventModule} from 'angular-resize-event';
import {DateTimeFilterComponent} from './editor/database/filters/date-time.filter';
import {DatabasePageComponent} from './editor/database/database-page.component';
import {PageLinkRendererComponent} from './editor/database/renderers/page-link-renderer.component';
import {PageLinkEditorComponent} from './editor/database/editors/page-link/page-link-editor.component';
import {LinkRendererComponent} from './editor/database/renderers/link-renderer.component';
import {FormInputComponent} from './nodes/form-input/form-input.component';
import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component';
import {DatabaseLinkComponent} from './editor/forms/database-link.component';
import {FileBoxComponent} from './nodes/file-box/file-box.component';
import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component';
@NgModule({
declarations: [HostComponent],
imports: [CommonModule],
entryComponents: [HostComponent],
exports: [HostComponent]
declarations: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
],
imports: [
CommonModule,
IonicModule,
AgGridModule,
FormsModule,
ReactiveFormsModule,
ContenteditableModule,
MonacoEditorModule,
DirectivesModule,
MarkdownModule,
EditorPageRoutingModule,
ViewerPageRoutingModule,
AngularResizedEventModule,
IonicSelectableModule,
],
entryComponents: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
],
exports: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
]
})
export class ComponentsModule {}

@ -0,0 +1,23 @@
<div class="code-wrapper" style="width: 100%; margin-top: 10px;" *ngIf="!notAvailableOffline">
<ion-toolbar>
<ion-item>
<ion-label position="floating">Language</ion-label>
<ion-select style="min-width: 40px;" [(ngModel)]="editorOptions.language" (ionChange)="onSelectChange()" [disabled]="readonly">
<ion-select-option *ngFor="let lang of languageOptions" [value]="lang.toLowerCase()">{{lang}}</ion-select-option>
</ion-select>
</ion-item>
</ion-toolbar>
<div class="editor-container" #editorContainer>
<ngx-monaco-editor style="width: 100%; height: 100%;"
[options]="editorOptions"
[(ngModel)]="editorValue"
(onInit)="onMonacoEditorInit($event)"
(ngModelChange)="onEditorModelChange($event)"
#theEditor
class="editor"
></ngx-monaco-editor>
</div>
</div>
<div class="code-wrapper not-offline" style="width: 100%; height: 600px; margin-top: 10px;" *ngIf="notAvailableOffline">
Sorry, this code editor is not available offline yet.
</div>

@ -0,0 +1,10 @@
div.code-wrapper {
border: 2px solid #8c8c8c;
border-radius: 3px;
&.not-offline {
text-align: center;
padding-top: 100px;
color: #595959;
}
}

@ -0,0 +1,470 @@
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {v4} from 'uuid';
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {EditorComponent} from 'ngx-monaco-editor';
import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext';
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
import {debug} from '../../../utility';
import {environment} from '../../../../environments/environment';
export interface RemoteUser {
uuid: string;
uid: string;
display: string;
color: string;
cursor?: any;
selection?: any;
}
export interface RemoteInsertOperation {
type: 'insert';
index: number;
text: string;
fullContents?: string;
}
export interface RemoteReplaceOperation {
type: 'replace';
index: number;
text: string;
length: number;
fullContents?: string;
}
export interface RemoteDeleteOperation {
type: 'delete';
index: number;
length: number;
fullContents?: string;
}
export type RemoteOperation = RemoteInsertOperation | RemoteReplaceOperation | RemoteDeleteOperation;
@Component({
selector: 'editor-code',
templateUrl: './code.component.html',
styleUrls: ['./code.component.scss'],
})
export class CodeComponent extends EditorNodeContract implements OnInit, OnDestroy {
@Input() nodeId: string;
@Input() editorUUID?: string;
@ViewChild('theEditor') theEditor: EditorComponent;
@ViewChild('editorContainer') editorContainer: ElementRef;
public dirty = false;
protected dbRecord: any = {};
protected codeRefId!: string;
public notAvailableOffline = false;
public containerHeight = 540;
protected cursorManager?: MonacoCollabExt.RemoteCursorManager;
protected selectionManager?: MonacoCollabExt.RemoteSelectionManager;
protected contentManager?: MonacoCollabExt.EditorContentManager;
protected remoteUsers: RemoteUser[] = [];
protected localUser?: RemoteUser;
protected socket?: FlitterSocketConnection;
protected editorGroupID!: string;
public editorOptions = {
theme: this.isDark() ? 'vs-dark' : 'vs',
language: 'javascript',
uri: v4(),
readOnly: false,
automaticLayout: true,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
};
public editorValue = '';
public get readonly() {
return !this.node || !this.editorService.canEdit();
}
public languageOptions: Array<string> = [
'ABAP',
'AES',
'Apex',
'AZCLI',
'Bat',
'C',
'Cameligo',
'Clojure',
'CoffeeScript',
'Cpp',
'Csharp',
'CSP',
'CSS',
'Dockerfile',
'Fsharp',
'Go',
'GraphQL',
'Handlebars',
'HTML',
'INI',
'Java',
'JavaScript',
'JSON',
'Kotlin',
'LeSS',
'Lua',
'Markdown',
'MiPS',
'MSDAX',
'MySQL',
'Objective-C',
'Pascal',
'Pascaligo',
'Perl',
'pgSQL',
'PHP',
'Plaintext',
'Postiats',
'PowerQuery',
'PowerShell',
'Pug',
'Python',
'R',
'Razor',
'Redis',
'RedShift',
'RestructuredText',
'Ruby',
'Rust',
'SB',
'Scheme',
'SCSS',
'Shell',
'SOL',
'SQL',
'St',
'Swift',
'TCL',
'Twig',
'TypeScript',
'VB',
'XML',
'YAML',
];
protected hadLoad = false;
constructor(
public editorService: EditorService,
public readonly api: ApiService,
) { super(); }
public isDark() {
return document.body.classList.contains('dark');
}
public isDirty(): boolean | Promise<boolean> {
return this.dirty;
}
public needsSave(): boolean | Promise<boolean> {
return this.dirty;
}
public writeChangesToNode(): void | Promise<void> {
this.node.Value.Mode = 'code';
this.node.Value.Value = this.codeRefId;
this.node.value = this.codeRefId;
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && !this.hadLoad;
}
public performLoad(): void | Promise<void> {
return new Promise((res, rej) => {
if ( !this.node.Value ) {
this.node.Value = {};
}
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
this.dbRecord = data;
this.node.Value.Mode = 'code';
this.node.Value.Value = data.UUID;
this.node.value = data.UUID;
this.codeRefId = data.UUID;
this.editorOptions.readOnly = this.readonly;
this.onSelectChange(false);
this.hadLoad = true;
this.notAvailableOffline = false;
res();
}).catch(rej);
} else {
this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum).then(data => {
this.dbRecord = data;
this.initialValue = this.dbRecord.code;
this.editorValue = this.dbRecord.code;
this.editorOptions.language = this.dbRecord.Language;
this.codeRefId = this.node.Value.Value;
this.editorOptions.readOnly = this.readonly;
this.onSelectChange(false);
this.hadLoad = true;
this.notAvailableOffline = false;
res();
}).catch(e => {
if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailableOffline = true;
} else {
rej(e);
}
});
}
});
}
public performSave(): void | Promise<void> {
if ( !this.editorService.canEdit() ) {
return;
}
return new Promise((res, rej) => {
this.dbRecord.code = this.editorValue;
this.dbRecord.Language = this.editorOptions.language;
this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
this.dbRecord = data;
this.editorOptions.language = this.dbRecord.Language;
this.editorValue = this.dbRecord.code;
this.dirty = false;
res();
}).catch(rej);
});
}
public performDelete(): void | Promise<void> {
return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
}
ngOnInit() {
this.editorService = this.editorService.getEditor(this.editorUUID);
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
this.editorOptions.readOnly = !this.editorService.canEdit();
});
const url = `${environment.websocketBase}/api/v1/socket/code/.websocket`;
debug(`Editor socket URL: ${url}`);
if ( !this.editorService.isVersion() ) {
const socket = new FlitterSocketConnection(url);
socket.controller(this);
socket.on_open().then(() => {
debug('Connected to code editor socket', socket);
socket.asyncRequest('subscribe', { resource_id: this.node.Value.Value }).then(([transaction, _, data]) => {
debug('Subscribed to editor group:', data);
if ( data.editor_group_id ) {
this.editorGroupID = data.editor_group_id;
this.socket = socket;
this.localUser = data.local_user;
}
});
});
}
}
ngOnDestroy() {
if ( this.socket ) {
this.socket.socket.close();
}
}
onMonacoEditorInit(editor) {
let ignoreEvent = false;
const updateHeight = () => {
const contentHeight = Math.max(540, editor.getContentHeight());
this.containerHeight = contentHeight;
try {
ignoreEvent = true;
editor.layout({ width: this.editorContainer.nativeElement.offsetWidth, height: contentHeight });
} finally {
ignoreEvent = false;
}
};
editor.onDidChangeCursorPosition(event => {
this.socket?.asyncRequest('update_cursor', {
position: event.position,
uuid: this.localUser?.uuid,
editor_group_id: this.editorGroupID,
});
});
editor.onDidChangeCursorSelection(event => {
this.socket?.asyncRequest('update_selection', {
startPosition: {
lineNumber: event.selection.startLineNumber,
column: event.selection.startColumn,
},
endPosition: {
lineNumber: event.selection.endLineNumber,
column: event.selection.endColumn,
},
uuid: this.localUser?.uuid,
editor_group_id: this.editorGroupID,
});
});
editor.onDidContentSizeChange(updateHeight);
updateHeight();
this.cursorManager = new MonacoCollabExt.RemoteCursorManager({
editor,
tooltips: true,
tooltipDuration: 2,
});
this.selectionManager = new MonacoCollabExt.RemoteSelectionManager({ editor });
this.contentManager = new MonacoCollabExt.EditorContentManager({
editor,
onInsert: (index, text) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'insert',
index,
text,
},
],
});
},
onReplace: (index, length, text) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'replace',
index,
text,
length,
},
],
});
},
onDelete: (index, length) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'delete',
index,
length,
},
],
});
},
});
}
applyOperation(op: RemoteOperation) {
if ( op.type === 'insert' ) {
this.contentManager?.insert(op.index, op.text);
} else if ( op.type === 'replace' ) {
this.contentManager?.replace(op.index, op.length, op.text);
} else if ( op.type === 'delete' ) {
this.contentManager?.delete(op.index, op.length);
}
}
async applyRemoteOperation(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const ops: RemoteOperation[] = transaction.incoming.operations || [];
for ( const op of ops ) {
this.applyOperation(op);
}
}
async updateCursorPosition(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const position = transaction.incoming.position;
const uuid = transaction.incoming.uuid;
for ( const user of this.remoteUsers ) {
if ( user.uuid === uuid ) {
user.cursor?.setPosition(position);
}
}
}
async updateSelection(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const startPosition = transaction.incoming.startPosition;
const endPosition = transaction.incoming.endPosition;
const uuid = transaction.incoming.uuid;
for ( const user of this.remoteUsers ) {
if ( user.uuid === uuid ) {
if ( startPosition && endPosition ) {
user.selection?.setPositions(startPosition, endPosition);
user.selection?.show();
} else {
user.selection?.hide();
}
}
}
}
async setEditorGroupUsers(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const remoteUsers: RemoteUser[] = Array.isArray(transaction.incoming?.users) ? transaction.incoming.users : [];
console.log('set editor group users', remoteUsers, transaction);
for ( const user of this.remoteUsers ) {
this.cursorManager?.removeCursor(user.uuid);
user.cursor?.dispose();
delete user.cursor;
this.selectionManager?.removeSelection(user.uuid);
user.selection?.dispose();
delete user.selection;
}
while ( !this.cursorManager ) {
await new Promise(r => setTimeout(r, 500));
}
this.remoteUsers = remoteUsers;
for ( const user of this.remoteUsers ) {
user.cursor = this.cursorManager?.addCursor(user.uuid, user.color, user.display);
user.cursor?.setOffset(0);
user.cursor?.show();
user.selection = this.selectionManager?.addSelection(user.uuid, user.color);
}
}
public onEditorModelChange($event) {
if ( this.editorValue !== this.dbRecord.code ) {
this.dirty = true;
this.editorService.triggerSave();
}
}
public onSelectChange(updateDbRecord = true) {
if ( updateDbRecord ) {
this.dbRecord.Language = this.editorOptions.language;
this.editorService.triggerSave();
this.dirty = true;
}
this.editorOptions = {...this.editorOptions};
}
}

@ -0,0 +1,150 @@
<ion-header>
<ion-toolbar>
<ion-title>Manage Database Columns</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal(false)">
<ion-icon name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row
*ngFor="let colSet of columnSets; let i = index"
class="column-def"
>
<ion-col size="5">
<ion-item>
<ion-label position="floating">Field Label</ion-label>
<ion-input type="text" required [(ngModel)]="columnSets[i].headerName"></ion-input>
</ion-item>
</ion-col>
<ion-col size="4">
<ion-item>
<ion-label position="floating">Data Type</ion-label>
<ion-select interface="popover" [(ngModel)]="columnSets[i].Type">
<ion-select-option value="text">Text</ion-select-option>
<ion-select-option value="number">Number</ion-select-option>
<ion-select-option value="paragraph">Paragraph</ion-select-option>
<ion-select-option value="wysiwyg">Rich-Text</ion-select-option>
<ion-select-option value="boolean">Boolean</ion-select-option>
<ion-select-option value="select">Select</ion-select-option>
<ion-select-option value="multiselect">Multi-Select</ion-select-option>
<ion-select-option value="datetime">Date-Time</ion-select-option>
<ion-select-option value="currency">Currency</ion-select-option>
<ion-select-option value="index">Incrementing Index</ion-select-option>
<ion-select-option value="page_link">Link to Page</ion-select-option>
<ion-select-option value="link">Hyperlink</ion-select-option>
<!-- <ion-select-option value="person">Person</ion-select-option>-->
<!-- <ion-select-option value="url">URL</ion-select-option>-->
<!-- <ion-select-option value="email">E-Mail</ion-select-option>-->
</ion-select>
</ion-item>
</ion-col>
<ion-col size="3" align-items-center>
<ion-row>
<ion-button fill="outline" color="light" (click)="onDeleteClick(i)">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
<ion-button
fill="outline"
color="light"
(click)="iteratePin(i)"
[title]="columnSets[i].additionalData.pinned ? 'Column is pinned to the ' + columnSets[i].additionalData.pinned : 'Column is not pinned'"
>
<i
class="fa fa-caret-square-left"
style="font-size: 1.7em; color: #cccccc" *ngIf="columnSets[i].additionalData.pinned === 'left'"
></i>
<i
class="fa fa-caret-square-right"
style="font-size: 1.7em; color: #cccccc" *ngIf="columnSets[i].additionalData.pinned === 'right'"
></i>
<i
class="fa fa-square"
style="font-size: 1.7em; color: #cccccc" *ngIf="!columnSets[i].additionalData.pinned"
></i>
</ion-button>
</ion-row>
<ion-row>
<ion-button fill="outline" color="light" size="small" (click)="onUpArrow(i)">
<ion-icon color="dark" name="arrow-up"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" size="small" (click)="onDownArrow(i)">
<ion-icon color="dark" name="arrow-down"></ion-icon>
</ion-button>
</ion-row>
</ion-col>
<ion-col size="5" *ngIf="columnSets[i].Type === 'boolean'">
<ion-item>
<ion-label position="floating">Label Type</ion-label>
<ion-select interface="popover" [(ngModel)]="columnSets[i].additionalData.labelType">
<ion-select-option value="true_false">True/False</ion-select-option>
<ion-select-option value="yes_no">Yes/No</ion-select-option>
<ion-select-option value="1_0">1/0</ion-select-option>
<ion-select-option value="checkbox">Checkbox</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'select' || columnSets[i].Type === 'multiselect'">
<ion-button (click)="onAddOption(i)" fill="outline">Add Option</ion-button>
<ng-container *ngIf="columnSets[i].additionalData.options">
<ion-row *ngFor="let option of columnSets[i].additionalData.options; let n = index">
<ion-col size="10">
<ion-item>
<ion-label position="floating">Value</ion-label>
<ion-input [(ngModel)]="columnSets[i].additionalData.options[n].value"></ion-input>
</ion-item>
</ion-col>
<ion-col size="2">
<ion-button fill="outline" color="light" size="small" (click)="onDeleteOptionClick(i, n)">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ng-container>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'datetime'">
<ion-list>
<ion-radio-group value="YYYY-MM-DD h:mm a" [(ngModel)]="columnSets[i].additionalData.format">
<ion-list-header>Format</ion-list-header>
<ion-item>
<ion-label>Date Only</ion-label>
<ion-radio slot="start" value="YYYY-MM-DD"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Time Only</ion-label>
<ion-radio slot="start" value="h:mm a"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Both</ion-label>
<ion-radio slot="start" value="YYYY-MM-DD h:mm a"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'currency'">
<ion-item>
<ion-label position="floating">Currency</ion-label>
<ion-select [(ngModel)]="columnSets[i].additionalData.currency">
<ion-select-option value="USD">US Dollar</ion-select-option>
<ion-select-option value="EUR">Euro</ion-select-option>
<ion-select-option value="MXN">Mexican Peso</ion-select-option>
<ion-select-option value="CNY">Chinese Yuan</ion-select-option>
<ion-select-option value="XAG">Silver</ion-select-option>
<ion-select-option value="XAU">Gold</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer>
<ion-buttons style="padding: 10px;">
<ion-button (click)="onAddColumnClick()" fill="outline">Add Column</ion-button>
<ion-button (click)="dismissModal(true)" color="success" fill="outline">Save</ion-button>
</ion-buttons>
</ion-footer>

@ -0,0 +1,6 @@
.column-def {
border: 2px solid #ccc;
margin: 5px;
padding: 5px;
border-radius: 7px;
}

@ -0,0 +1,96 @@
import {Component, Input, OnInit} from '@angular/core';
import {AlertController, ModalController} from '@ionic/angular';
import {uuid_v4} from '../../../../utility';
@Component({
selector: 'editor-database-columns',
templateUrl: './columns.component.html',
styleUrls: ['./columns.component.scss'],
})
export class ColumnsComponent implements OnInit {
@Input() columnSets: Array<{headerName: string, field: string, Type: string, additionalData: any}> = [];
constructor(
protected modals: ModalController,
protected alerts: AlertController,
) { }
ngOnInit() {}
onAddColumnClick() {
this.columnSets.push({headerName: '', field: uuid_v4(), Type: '', additionalData: {}});
}
onAddOption(i) {
const set = this.columnSets[i];
if ( !Array.isArray(set.additionalData.options) ) {
set.additionalData.options = [];
}
set.additionalData.options.push({value: ''});
}
onDeleteOptionClick(i, n) {
const set = this.columnSets[i];
set.additionalData.options = set.additionalData.options.filter((x, index) => index !== n);
}
dismissModal(doSave = true) {
if ( doSave ) {
this.columnSets = this.columnSets.map(x => {
if ( !x.field ) {
x.field = uuid_v4();
}
return x;
});
this.modals.dismiss(this.columnSets);
} else {
this.modals.dismiss();
}
}
onDeleteClick(i) {
this.alerts.create({
header: 'Delete column?',
message: 'Are you sure you want to delete this column? Its data will be lost.',
buttons: [
{ text: 'Keep It', role: 'cancel' },
{
text: 'Delete It',
handler: () => {
this.columnSets = this.columnSets.filter((x, index) => {
return index !== i;
});
},
}
],
}).then(alert => alert.present());
}
onUpArrow(i) {
if ( this.columnSets[i - 1] ) {
const temp = this.columnSets[i];
this.columnSets[i] = this.columnSets[i - 1];
this.columnSets[i - 1] = temp;
}
}
onDownArrow(i) {
if ( this.columnSets[i + 1] ) {
const temp = this.columnSets[i];
this.columnSets[i] = this.columnSets[i + 1];
this.columnSets[i + 1] = temp;
}
}
iteratePin(i) {
if ( !this.columnSets[i].additionalData.pinned ) {
this.columnSets[i].additionalData.pinned = 'left';
} else if ( this.columnSets[i].additionalData.pinned === 'left' ) {
this.columnSets[i].additionalData.pinned = 'right';
} else {
delete this.columnSets[i].additionalData.pinned;
}
}
}

@ -0,0 +1,41 @@
import {Component, Input, OnInit} from '@angular/core';
import {EditorService} from '../../../service/editor.service';
@Component({
selector: 'noded-database-page',
template: `
<div class="container" *ngIf="ready">
<editor-database [nodeId]="nodeId" [editorUUID]="this.editorService.instanceUUID" [fullPage]="true"></editor-database>
</div>
`,
styles: [
`
.container {
height: 100%;
}
editor-database {
height: 100%;
display: flex;
}
`,
],
})
export class DatabasePageComponent implements OnInit {
@Input() nodeId: string;
@Input() pageId: string;
public ready = false;
constructor(
public editorService: EditorService,
) {
this.editorService = editorService.getEditor();
}
ngOnInit() {
this.editorService.startEditing(this.pageId).then(() => {
this.ready = true;
});
}
}

@ -0,0 +1,61 @@
<div [ngClass]="fullPage ? 'database-wrapper full-page' : 'database-wrapper'" *ngIf="!notAvailableOffline" (resized)="onResized()">
<ion-toolbar>
<div style="display: flex; flex-direction: row">
<ion-input
[readonly]="readonly"
[(ngModel)]="dbName"
(ionChange)="onCellValueChanged()"
style="flex: 1; font-size: 15pt;"
></ion-input>
<button
class="clear-btn"
style="padding: 0 20px;"
*ngIf="fullPage && (editorService.isSaving || editorService.willSave)"
title="Saving..."
>
<i class="fa fa-spin fa-circle-notch"></i>
</button>
<button
class="clear-btn"
style="padding: 0 20px;"
*ngIf="fullPage && !(editorService.isSaving || editorService.willSave)"
title="Changes saved."
(click)="editorService.triggerSave()"
>
<i class="fa fa-check-circle"></i>
</button>
<button style="padding: 0 20px;" *ngIf="fullPage" (click)="dismiss()" class="clear-btn"><i class="fa fa-times"></i></button>
</div>
<ion-buttons *ngIf="!readonly" style="flex-wrap: wrap;">
<ion-button (click)="onManageColumns()"><ion-icon name="build" color="primary"></ion-icon>&nbsp;Manage Columns</ion-button>
<ion-button (click)="onInsertRow()"><ion-icon name="add-circle" color="success"></ion-icon>&nbsp;Insert Row</ion-button>
<ion-button (click)="onRemoveRow()" [disabled]="lastClickRow < 0"><ion-icon name="remove-circle" color="danger"></ion-icon>&nbsp;Delete Row</ion-button>
<ion-button (click)="openDatabase()" *ngIf="!fullPage"><i class="fa fa-external-link-alt" style="padding-right: 10px;"></i> Open</ion-button>
</ion-buttons>
</ion-toolbar>
<div class="grid-wrapper">
<ag-grid-angular
[style]="fullPage ? 'width: 100%; height: 100%;' : 'width: 100%; height: 500px;'"
[ngClass]="isDark() ? 'ag-theme-balham-dark' : 'ag-theme-balham'"
[rowData]="rowData"
[getRowNodeId]="getRowNodeId"
[columnDefs]="columnDefs"
[singleClickEdit]="true"
[enterMovesDownAfterEdit]="true"
[rowDragManaged]="true"
[suppressMoveWhenRowDragging]="true"
suppressMovableColumns="true"
(rowClicked)="onRowClicked($event)"
(cellValueChanged)="onCellValueChanged()"
(gridReady)="onGridReady($event)"
(columnResized)="onColumnResize($event)"
(rowDragMove)="onRowDragEnd($event)"
[frameworkComponents]="frameworkComponents"
#agGridElement
></ag-grid-angular>
</div>
</div>
<div class="database-wrapper not-available" *ngIf="notAvailableOffline">
Sorry, this database is not available offline yet.
</div>

@ -0,0 +1,21 @@
div.database-wrapper {
border: 2px solid #8c8c8c;
border-radius: 3px;
&.not-available {
height: 600px;
text-align: center;
padding-top: 100px;
color: #494949;
}
}
.full-page {
width: 100%;
display: flex;
flex-direction: column;
.grid-wrapper {
flex: 1;
}
}

@ -0,0 +1,539 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {AlertController, LoadingController, ModalController} from '@ionic/angular';
import {ColumnsComponent} from './columns/columns.component';
import {AgGridAngular} from 'ag-grid-angular';
import {NumericEditorComponent} from './editors/numeric/numeric-editor.component';
import {ParagraphEditorComponent} from './editors/paragraph/paragraph-editor.component';
import {BooleanEditorComponent} from './editors/boolean/boolean-editor.component';
import {SelectEditorComponent} from './editors/select/select-editor.component';
import {MultiSelectEditorComponent} from './editors/select/multiselect-editor.component';
import {DatetimeEditorComponent} from './editors/datetime/datetime-editor.component';
import {DatetimeRendererComponent} from './renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './renderers/currency-renderer.component';
import {BooleanRendererComponent} from './renderers/boolean-renderer.component';
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {WysiwygEditorComponent} from './editors/wysiwyg/wysiwyg-editor.component';
import {debounce, debug, uuid_v4} from '../../../utility';
import {DateTimeFilterComponent} from './filters/date-time.filter';
import {DatabasePageComponent} from './database-page.component';
import {PageLinkRendererComponent} from './renderers/page-link-renderer.component';
import {LinkRendererComponent} from './renderers/link-renderer.component';
import {PageLinkEditorComponent} from './editors/page-link/page-link-editor.component';
@Component({
selector: 'editor-database',
templateUrl: './database.component.html',
styleUrls: ['./database.component.scss'],
})
export class DatabaseComponent extends EditorNodeContract implements OnInit {
@Input() nodeId: string;
@Input() editorUUID?: string;
@Input() fullPage = false;
@ViewChild('agGridElement') agGridElement: AgGridAngular;
frameworkComponents = {
agDateInput: DateTimeFilterComponent
};
public dbRecord: any;
public pendingSetup = true;
public dirty = false;
public lastClickRow = -1;
public dbName = '';
public notAvailableOffline = false;
public pages = [];
protected dbId!: string;
protected isInitialLoad = false;
protected triggerSaveDebounce = debounce(() => {
if ( this.agGridElement.api.getCellEditorInstances().length < 1 ) {
this.editorService.triggerSave();
} else {
this.triggerSaveDebounce();
}
}, 1000);
protected gridReady = false;
protected deferredUIActivation = false;
title = 'app';
columnDefs = [];
rowData = [];
public isDark() {
return document.body.classList.contains('dark');
}
public get readonly() {
return !this.node || !this.editorService.canEdit();
}
constructor(
protected api: ApiService,
protected modals: ModalController,
protected alerts: AlertController,
protected loader: LoadingController,
public editorService: EditorService,
) { super(); }
public isDirty(): boolean | Promise<boolean> {
return this.dirty;
}
public needsSave(): boolean | Promise<boolean> {
return this.dirty;
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && this.pendingSetup;
}
public writeChangesToNode(): void | Promise<void> {
this.node.Value.Mode = 'database';
}
async ngOnInit() {
this.pages = this.flattenItems(await this.api.getMenuItems(true));
this.editorService = this.editorService.getEditor(this.editorUUID);
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
});
}
protected flattenItems(items: any[], level = 0) {
let newItems = [];
for ( const item of items ) {
item.level = level;
newItems.push(item);
if ( Array.isArray(item.children) ) {
newItems = [...newItems, ...this.flattenItems(item.children, level + 1)];
}
}
return newItems;
}
onGridReady($event) {
this.gridReady = true;
if ( this.deferredUIActivation && !this.pendingSetup ) {
this.performUIActivation();
}
}
onColumnResize($event) {
if ( $event.source === 'uiColumnDragged' && $event.finished ) {
debug('Column resized: ', $event);
const state = $event.columnApi.getColumnState().find(x => x.colId === $event.column.colId );
if ( state ) {
const colDef = this.columnDefs.find(x => x.field === $event.column.colId);
if ( colDef ) {
colDef.width = state.width;
this.dirty = true;
this.triggerSaveDebounce();
}
}
}
}
onRowDragEnd($event) {
if ( !this.isInitialLoad && this.editorService.canEdit() ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
onCellValueChanged() {
if ( !this.isInitialLoad ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
async onManageColumns() {
if ( this.readonly ) {
return;
}
const modal = await this.modals.create({
component: ColumnsComponent,
componentProps: {columnSets: this.columnDefs.slice(1)},
cssClass: 'modal-med',
});
modal.onDidDismiss().then(result => {
if ( result?.data ) {
this.setColumns(result.data);
}
});
const modalState = {
modal : true,
desc : 'Manage Columns'
};
history.pushState(modalState, null);
await modal.present();
}
onInsertRow() {
if ( this.readonly ) {
return;
}
this.rowData.push({});
this.agGridElement.api.setRowData(this.rowData);
this.dirty = true;
this.triggerSaveDebounce();
}
async onRemoveRow() {
if ( this.readonly ) {
return;
}
const alert = await this.alerts.create({
header: 'Are you sure?',
message: `You are about to delete row ${this.lastClickRow + 1}. This cannot be undone.`,
buttons: [
{
text: 'Keep It',
role: 'cancel',
},
{
text: 'Delete It',
handler: () => {
this.rowData = this.rowData.filter((x, i) => {
return i !== this.lastClickRow;
});
this.agGridElement.api.setRowData(this.rowData);
this.lastClickRow = -1;
this.dirty = true;
this.triggerSaveDebounce();
},
}
],
});
await alert.present();
}
onRowClicked($event) {
this.lastClickRow = $event.rowIndex;
}
setColumns(data, triggerSave = true) {
this.columnDefs = [{
width: 20,
// rowDrag: !this.readonly,
// rowDragText: (params, dragItemCount) => `${dragItemCount} ${dragItemCount === 1 ? 'row' : 'rows'}`,
}, ...data.map(x => {
x.editable = !this.readonly;
x.minWidth = 150;
x.resizable = true;
x._parentEditorUUID = this.editorUUID;
if ( x.additionalData?.width ) {
x.width = x.additionalData.width;
}
if ( x.additionalData?.pinned ) {
x.pinned = x.additionalData.pinned;
}
// Set editors and renderers for different types
if ( x.Type === 'text' ) {
x.editor = 'agTextCellEditor';
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'number' ) {
x.cellEditorFramework = NumericEditorComponent;
x.filter = 'agNumberColumnFilter';
} else if ( x.Type === 'paragraph' ) {
x.cellEditorFramework = ParagraphEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'boolean' ) {
x.cellRendererFramework = BooleanRendererComponent;
x.cellEditorFramework = BooleanEditorComponent;
x.suppressSizeToFit = true;
} else if ( x.Type === 'select' ) {
x.cellEditorFramework = SelectEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'multiselect' ) {
x.cellEditorFramework = MultiSelectEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'datetime' ) {
x.cellEditorFramework = DatetimeEditorComponent;
x.cellRendererFramework = DatetimeRendererComponent;
x.filter = 'agDateColumnFilter';
x.filterParams = {
buttons: ['apply', 'clear'],
displayFormat: x.additionalData.format,
comparator: (filterDate: Date, cellValue) => {
if ( !cellValue ) {
return 0;
}
const cellDate = new Date(cellValue);
if ( x.additionalData.format === 'YYYY-MM-DD' ) {
cellDate.setHours(0);
cellDate.setMinutes(0);
cellDate.setSeconds(0);
cellDate.setMilliseconds(0);
} else if ( x.additionalData.format === 'h:mm a' ) {
cellDate.setFullYear(filterDate.getFullYear());
cellDate.setMonth(filterDate.getMonth());
cellDate.setDate(filterDate.getDate());
}
// Now that both parameters are Date objects, we can compare
if (cellDate < filterDate) {
return -1;
} else if (cellDate > filterDate) {
return 1;
} else {
return 0;
}
},
};
} else if ( x.Type === 'currency' ) {
x.cellEditorFramework = NumericEditorComponent;
x.cellRendererFramework = CurrencyRendererComponent;
x.filter = 'agNumberColumnFilter';
} else if ( x.Type === 'index' ) {
x.editable = false;
x.suppressSizeToFit = true;
x.filter = 'agNumberColumnFilter';
if ( !x.width ) {
x.width = 80;
}
x.minWidth = 80;
} else if ( x.Type === 'wysiwyg' ) {
x.cellEditorFramework = WysiwygEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'page_link' ) {
x.cellRendererFramework = PageLinkRendererComponent;
x.cellEditorFramework = PageLinkEditorComponent;
x.filter = 'agTextColumnFilter';
if ( !x.cellEditorParams ) {
x.cellEditorParams = {} as any;
}
if ( !x.cellRendererParams ) {
x.cellRendererParams = {} as any;
}
x.cellEditorParams._pagesData = this.pages;
x.cellRendererParams._pagesData = this.pages;
} else if ( x.Type === 'link' ) {
x.cellRendererFramework = LinkRendererComponent;
x.editor = 'agTextCellEditor';
x.filter = 'agTextColumnFilter';
}
return x;
})];
this.agGridElement.api.setColumnDefs([]);
this.agGridElement.api.setColumnDefs(this.columnDefs);
this.agGridElement.api.sizeColumnsToFit();
if ( triggerSave ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
public onResized() {
this.agGridElement.api.sizeColumnsToFit();
}
public async performLoad(): Promise<void> {
this.isInitialLoad = true;
if ( !this.node.Value ) {
this.node.Value = {};
}
// Load the database record itself
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
this.dbRecord = await this.api.createDatabase(this.page.UUID, this.node.UUID);
this.dbName = this.dbRecord.Name;
this.node.Value.Mode = 'database';
this.node.Value.Value = this.dbRecord.UUID;
this.node.value = this.dbRecord.UUID;
} else {
try {
this.dbRecord = await this.api.getDatabase(
this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum);
this.dbName = this.dbRecord.Name;
this.notAvailableOffline = false;
} catch (e: unknown) {
if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailableOffline = true;
} else {
throw e;
}
}
}
// Load the columns
const columns = await this.api.getDatabaseColumns(
this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum);
this.setColumns(columns, false);
const rows = await this.api.getDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value);
this.rowData = rows.map(x => x.RowData);
this.agGridElement.api.setRowData(this.rowData);
this.pendingSetup = false;
this.dirty = false;
this.isInitialLoad = false;
if ( this.deferredUIActivation && this.gridReady ) {
await this.performUIActivation();
}
}
public async performDelete(): Promise<void> {
await this.api.deleteDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value);
}
public getSaveColumns() {
return this.columnDefs.slice(1).map(x => {
if ( !x.additionalData ) {
x.additionalData = {};
}
if ( x.width ) {
x.additionalData.width = x.width;
}
return x;
});
}
public getRowNodeId(data: any) {
if ( !data.UUID ) {
data.UUID = uuid_v4();
}
return data.UUID;
}
protected getAllRows() {
const rowData: any[] = [];
this.agGridElement.api.forEachNode(node => {
if ( !node.data.UUID ) {
node.data.UUID = uuid_v4();
}
rowData.push(node.data);
});
return rowData;
}
public async performSave(): Promise<void> {
// Save the columns first
await this.api.saveDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value, this.getSaveColumns());
// Save the data
const allRows = this.getAllRows();
const rows = await this.api.saveDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value, allRows);
// this.rowData = rows.map(x => x.RowData);
// Dynamically update the row data to avoid breaking open editors
const returnedUUIDs = rows.map(x => x.UUID);
const existingUUIDs = [];
const rowDataTransaction = {
add: [],
remove: [],
update: [],
};
this.agGridElement.api.forEachNode((rowNode, index) => {
const data = rowNode.data;
if ( !returnedUUIDs.includes(data.UUID) ) {
rowDataTransaction.remove.push(rowNode.id);
} else {
existingUUIDs.push(data.UUID);
const updatedRow = rows.find(x => x.UUID === data.UUID);
if ( updatedRow ) {
for ( const prop in updatedRow ) {
if ( !updatedRow.hasOwnProperty(prop) ) {
continue;
}
data[prop] = updatedRow[prop];
}
rowDataTransaction.update.push(data);
}
}
});
// for ( const row of rows ) {
// if ( !gridUUIDs.includes(row.UUID) ) {
// rowDataTransaction.add.push(row);
// }
// }
// @ts-ignore
this.agGridElement.api.applyTransaction(rowDataTransaction);
// this.agGridElement.api.setRowData(this.rowData);
// Save the name
await this.api.saveDatabaseName(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbName);
this.dirty = false;
}
async openDatabase() {
const modal = await this.modals.create({
component: DatabasePageComponent,
componentProps: {
nodeId: this.nodeId,
pageId: this.editorService.currentPageId,
},
cssClass: 'modal-big',
});
modal.onDidDismiss().then(() => {
this.editorService.reload();
});
const modalState = {
modal : true,
desc : 'Open Database'
};
history.pushState(modalState, null);
await modal.present();
}
performUIActivation() {
if ( this.deferredUIActivation ) {
this.deferredUIActivation = false;
}
if ( this.gridReady && !this.pendingSetup ) {
return this.openDatabase();
} else {
this.deferredUIActivation = true;
}
}
dismiss() {
this.modals.dismiss();
}
}

@ -0,0 +1,68 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {debounce} from '../../../../../utility';
@Component({
selector: 'cell-editor-paragraph',
template: `<input #input [value]="display" readonly (click)="onClick()">`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class BooleanEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: boolean;
public get display() {
if ( typeof this.value === 'undefined' ) {
return this.emptyValue;
} else if ( this.value ) {
return this.trueValue;
} else {
return this.falseValue;
}
}
protected trueValue = 'True';
protected falseValue = 'False';
protected emptyValue = '';
protected autoDismissDebounce = debounce(() => {
this.params.stopEditing();
}, 2000);
@ViewChild('input') input: ElementRef;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
const values = params.colDef.additionalData.labelType.split('_');
this.trueValue = values[0].charAt(0).toUpperCase() + values[0].slice(1);
this.falseValue = values[1].charAt(0).toUpperCase() + values[1].slice(1);
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.onClick();
}
onClick() {
if ( this.value === true ) {
this.value = false;
} else if ( this.value === false ) {
this.value = undefined;
} else {
this.value = true;
}
this.autoDismissDebounce();
}
}

@ -0,0 +1,39 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonDatetime} from '@ionic/angular';
@Component({
selector: 'cell-editor-select',
template: `<ion-datetime
#picker [displayFormat]="format" [(ngModel)]="value" style="padding: 0 0 0 5px;" (ionChange)="finishEdit($event)"
></ion-datetime>`,
})
export class DatetimeEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public format = 'YYYY-MM-DD h:mm a';
@ViewChild('picker') picker: IonDatetime;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
if ( this.params.colDef.additionalData.format ) {
// @ts-ignore
this.format = this.params.colDef.additionalData.format;
}
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.picker.open();
}
finishEdit($event) {
this.params.stopEditing();
}
}

@ -0,0 +1,75 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
@Component({
selector: 'cell-editor-numeric',
template: `<input #input (keydown)="onKeyDown($event)" [(ngModel)]="value">`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class NumericEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: number;
private cancelBeforeStart = false;
@ViewChild('input') input: ElementRef;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// Only cancel before start if the pressed key is numeric
this.cancelBeforeStart = params.charPress && ('1234567890'.indexOf(params.charPress) < 0);
}
getValue(): any {
return this.value;
}
isCancelBeforeStart(): boolean {
return this.cancelBeforeStart;
}
onKeyDown($event: KeyboardEvent) {
if ( !this.isKeyPressedAllowed($event) ) {
if ($event.preventDefault) {
$event.preventDefault();
}
}
}
ngAfterViewInit(): void {
setTimeout(() => {
this.input.nativeElement.focus();
});
}
private getCharCodeFromEvent(event): any {
return (typeof event.which === 'undefined') ? event.keyCode : event.which;
}
private isCharNumeric(charStr): boolean {
return !!/\d/.test(charStr);
}
private isKeyPressedAllowed(event): boolean {
const charCode = this.getCharCodeFromEvent(event);
const charStr = event.key ? event.key : String.fromCharCode(charCode);
if (this.isCharNumeric(charStr)) {
return true;
} else if ( charStr === '.' && this.value % 1 === 0 ) {
return true;
} else if ( ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Enter'].includes(event.code) ) {
return true;
} else if ( charStr === 'a' && event.ctrlKey ) {
return true;
}
return false;
}
}

@ -0,0 +1,14 @@
<ionic-selectable
#selectable
[items]="pages"
itemTextField="name"
itemValueField="id"
[canSearch]="true"
[title]="'Select a page'"
[(ngModel)]="value"
(onClose)="finishEdit()"
>
<ng-template ionicSelectableItemTemplate let-port="item" let-isPortSelected="itemIsSelected">
<div [ngStyle]="{ marginLeft: (20 * port.level) + 'px' }">{{ port.name }}</div>
</ng-template>
</ionic-selectable>

@ -0,0 +1,42 @@
import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ApiService} from '../../../../../service/api.service';
import {IonicSelectableComponent} from "ionic-selectable";
@Component({
selector: 'editor-cel-page-link',
templateUrl: './page-link-editor.component.html',
styleUrls: ['./page-link-editor.component.scss'],
})
export class PageLinkEditorComponent implements OnInit {
@ViewChild('selectable') selectable: IonicSelectableComponent;
public params: ICellEditorParams;
public value: any;
public pages: any[] = [];
constructor(
public readonly api: ApiService,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = { id: this.params.value };
// @ts-ignore
this.pages = params._pagesData || [];
setTimeout(() => {
this.selectable.open();
});
}
getValue(): any {
return this.value.id;
}
async ngOnInit() {}
finishEdit() {
this.params.stopEditing();
}
}

@ -0,0 +1,69 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ModalController} from '@ionic/angular';
import {ParagraphModalComponent} from './paragraph-modal.component';
@Component({
selector: 'cell-editor-paragraph',
template: `<input #input [(ngModel)]="value" readonly>`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class ParagraphEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: string;
@ViewChild('input') input: ElementRef;
constructor(
protected modals: ModalController,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
setTimeout(() => {
this.modals.create({
component: ParagraphModalComponent,
componentProps: {
title: this.params.colDef.headerName,
value: this.value,
}
}).then(modal => {
modal.onDidDismiss().then(value => {
if ( typeof value.data === 'undefined' ) {
return;
}
this.value = String(value.data);
this.finishEdit();
});
const modalState = {
modal : true,
desc : 'Paragraph editor'
};
history.pushState(modalState, null);
modal.present();
});
});
}
finishEdit() {
this.params.stopEditing();
}
}

@ -0,0 +1,27 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal()">
<ion-icon name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-grid>
<ion-row>
<ion-col size="12">
<ion-item>
<ion-label position="floating">Content</ion-label>
<ion-textarea [(ngModel)]="value" rows="15"></ion-textarea>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer>
<ion-button fill="invisible" (click)="dismissModal()"><ion-icon name="save"></ion-icon>&nbsp;&nbsp;Save</ion-button>
</ion-footer>

@ -0,0 +1,20 @@
import {Component, Input} from '@angular/core';
import {ModalController} from '@ionic/angular';
@Component({
selector: 'editor-paragraph-modal',
templateUrl: './paragraph-modal.component.html',
styleUrls: ['./paragraph-modal.component.scss'],
})
export class ParagraphModalComponent {
@Input() value = '';
@Input() title: string;
constructor(
protected modals: ModalController,
) {}
dismissModal() {
this.modals.dismiss(this.value);
}
}

@ -0,0 +1,40 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonSelect} from '@ionic/angular';
@Component({
selector: 'cell-editor-multiselect',
template: `
<ion-select #select [(ngModel)]="value" style="padding: 0;" (ionChange)="finishEdit()"
[interfaceOptions]="{header: params.colDef.headerName, cssClass: 'big-alert'}" multiple="true">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{ option.value }}</ion-select-option>
</ion-select>
`,
})
export class MultiSelectEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public options: Array<{value: string}> = [];
@ViewChild('select') select: IonSelect;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
this.options = this.params.colDef.additionalData.options;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.select.open();
}
finishEdit() {
this.params.stopEditing();
}
}

@ -0,0 +1,39 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonSelect} from '@ionic/angular';
@Component({
selector: 'cell-editor-select',
template: `
<ion-select #select [(ngModel)]="value" style="padding: 0;" (ionChange)="finishEdit()"
[interfaceOptions]="{header: params.colDef.headerName, cssClass: 'big-alert'}">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{ option.value }}</ion-select-option>
</ion-select>
`,
})
export class SelectEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public options: Array<{value: string}> = [];
@ViewChild('select') select: IonSelect;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
this.options = this.params.colDef.additionalData.options;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.select.open();
}
finishEdit() {
this.params.stopEditing();
}
}

@ -0,0 +1,69 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ModalController} from '@ionic/angular';
import {WysiwygModalComponent} from './wysiwyg-modal.component';
@Component({
selector: 'cell-editor-wysiwyg',
template: `<input #input [(ngModel)]="value" readonly>`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class WysiwygEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: string;
@ViewChild('input') input: ElementRef;
constructor(
protected modals: ModalController,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
setTimeout(() => {
this.modals.create({
component: WysiwygModalComponent,
componentProps: {
title: this.params.colDef.headerName,
value: this.value,
}
}).then(modal => {
modal.onDidDismiss().then(value => {
if ( typeof value.data === 'undefined' ) {
return;
}
this.value = String(value.data);
this.finishEdit();
});
const modalState = {
modal : true,
desc : 'WYSIWYG editor'
};
history.pushState(modalState, null);
modal.present();
});
});
}
finishEdit() {
this.params.stopEditing();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save