259 Commits

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

58
.drone.yml Normal file
View File

@@ -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

1
.npmrc Normal file
View File

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

3
Dockerfile Normal file
View File

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

12
README.md Normal file
View File

@@ -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 Normal file
View File

@@ -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

3
android/.idea/.gitignore generated vendored Normal file
View File

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

6
android/.idea/compiler.xml generated Normal file
View File

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

30
android/.idea/jarRepositories.xml generated Normal file
View File

@@ -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>

9
android/.idea/misc.xml generated Normal file
View File

@@ -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>

2
android/app/.npmignore Normal file
View File

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

46
android/app/build.gradle Normal file
View File

@@ -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")
}

View File

@@ -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()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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>

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

29
android/build.gradle Normal file
View File

@@ -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
}

View File

@@ -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')

24
android/gradle.properties Normal file
View File

@@ -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.

View File

@@ -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 Executable file
View File

@@ -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 Normal file
View File

@@ -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

5
android/settings.gradle Normal file
View File

@@ -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'

17
android/variables.gradle Normal file
View File

@@ -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'
}

View File

@@ -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": {

View File

@@ -16,8 +16,45 @@ steps:
displayName: 'Install Node.js'
- script: |
# npm install -g @angular/cli
npm install -g @ionic/cli
npm update
npm install
ionic build --prod
displayName: 'npm install and build'
- task: CmdLine@2
inputs:
script: |
echo $(Build.Repository.LocalPath)
ls -la $(Build.Repository.LocalPath)
# # Archive files
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.Repository.LocalPath)/www'
includeRootFolder: true
archiveType: 'tar'
archiveFile: '$(Build.ArtifactStagingDirectory)/target-www.tar.gz'
replaceExistingArchive: true
verbose: true
- task: PublishPipelineArtifact@1
inputs:
# targetPath: '$(Pipeline.Workspace)'
targetPath: '$(Build.ArtifactStagingDirectory)/target-www.tar.gz'
artifact: 'FrontEnd'
publishLocation: 'pipeline'
- task: CopyFilesOverSSH@0
displayName: 'Securely copy files to the remote machine'
inputs:
sshEndpoint: GoogleServer
sourceFolder: '$(Build.ArtifactStagingDirectory)'
targetFolder: /var/lib/app/pipeline/
failOnEmptySource: true
- task: SSH@0
displayName: 'Run shell commands on remote machine'
inputs:
sshEndpoint: GoogleServer
commands: '/var/lib/app/pipeline/frontend-deploy.sh'

14
capacitor.config.json Normal file
View File

@@ -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"
}

30
ngsw-config.json Normal file
View File

@@ -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)"
]
}
}
]
}

14472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,34 +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",
"@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",
@@ -52,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"
}

11337
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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,
}
];

View File

@@ -1,24 +1,56 @@
<ion-app>
<ion-split-pane contentId="main-content">
<ion-menu contentId="main-content" type="overlay">
<ion-app class="dark">
<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>
<ion-title>Menu</ion-title>
<ion-toolbar color="primary">
<ion-title style="font-weight: bold; color: white;">{{ appName }}
<ion-menu-toggle menu="first" autoHide="false"></ion-menu-toggle>
</ion-title>
</ion-toolbar>
<ion-list>
<ion-list-header>
<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)="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>
</ion-list>
</ion-header>
<ion-content>
<ion-list>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages">
<ion-item [routerDirection]="'root'" [routerLink]="[p.url]">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>
{{p.title}}
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<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-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>
<ion-router-outlet id="main-content"></ion-router-outlet>
<ion-router-outlet id="main-content" #content main></ion-router-outlet>
</ion-split-pane>
</ion-app>

View File

@@ -0,0 +1,38 @@
.sidebar {
max-width: 20em !important;
}
//text portion of buttons
.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);
}
}
}

View File

@@ -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');
});
});

View File

@@ -1,45 +1,822 @@
import { Component } from '@angular/core';
import {Component, OnInit, ViewChild, HostListener} from '@angular/core';
import { 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 {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',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss']
})
export class AppComponent {
public appPages = [
{
title: 'Home',
url: '/home',
icon: 'home'
},
{
title: 'List',
url: '/list',
icon: 'list'
},
{
title: 'Editor',
url: '/editor',
icon: 'edit'
}
];
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 menuTarget: any = false;
public refreshingMenu = false;
public lastClickEvent?: {event: MouseEvent, item: any};
public nodes = [];
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 statusBar: StatusBar,
public readonly api: ApiService,
protected router: Router,
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();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
@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',
message: 'Please enter a new name for the page:',
cssClass: 'page-prompt',
inputs: [
{
name: 'name',
type: 'text',
placeholder: 'My Awesome Page'
}
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary'
},
{
text: 'Create',
handler: async args => {
const page = await this.editor.createPage(args.name);
this.reloadMenuItems().subscribe();
await this.router.navigate(['/editor', { id: page.UUID }]);
}
}
]
});
await alert.present();
}
async onChildCreate(pageType?: string) {
const alert = await this.alerts.create({
header: 'Create Sub-Page',
message: 'Please enter a new name for the page:',
cssClass: 'page-prompt',
inputs: [
{
name: 'name',
type: 'text',
placeholder: 'My Awesome Page'
}
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary'
},
{
text: 'Create',
handler: async args => {
args = {
name: args.name,
parentId: this.addChildTarget.id,
pageType,
};
this.api.post('/page/create-child', args).subscribe(res => {
this.reloadMenuItems().subscribe(() => {
this.router.navigate(['/editor', { id: res.data.UUID }]);
});
});
}
}
]
});
await alert.present();
}
async onDeleteClick() {
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?',
buttons: [
{
text: 'Keep It',
role: 'cancel'
},
{
text: 'Delete It',
handler: async () => {
this.api
.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;
});
}
}
]
});
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.getMenuItems(false, this.virtualRootPageId).then(nodes => {
this.nodes = nodes;
sub.next();
sub.complete();
});
});
}
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)');
this.darkMode = !this.darkMode;
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();
}
}

View File

@@ -8,8 +8,34 @@ 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 { 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],
@@ -20,11 +46,45 @@ import {ComponentsModule} from './components/components.module';
AppRoutingModule,
HttpClientModule,
ComponentsModule,
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]
})

View File

@@ -1,15 +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
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,
],
entryComponents: [HostComponent],
exports: [
HostComponent
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 { }
export class ComponentsModule {}

View File

@@ -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>

View File

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

View File

@@ -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};
}
}

View File

@@ -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>

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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;
});
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

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