Kaynağa Gözat

feat: page edit + setup instructions

Nicolas Giard 2 yıl önce
ebeveyn
işleme
8cbdc88ac6

+ 97 - 379
README.md

@@ -2,13 +2,8 @@
 
 <img src="https://static.requarks.io/logo/wikijs-full.svg" alt="Wiki.js" width="600" />
 
-[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases)
 [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE)
 [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/)
-[![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases)
-[![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/)  
-[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)
-[![Huntr](https://img.shields.io/badge/security%20bounty-disclose-brightgreen.svg?style=flat&logo=cachet&logoColor=white)](https://huntr.dev/bounties/disclose)
 [![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)
 [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)  
 [![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack)
@@ -16,412 +11,135 @@
 [![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)
 [![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe)
 
-##### A modern, lightweight and powerful wiki app built on NodeJS
+##### Next Generation Open Source Wiki
 
 </div>
 
-- **[Official Website](https://js.wiki/)**
-- **[Documentation](https://docs.requarks.io/)**
-- [Requirements](https://docs.requarks.io/install/requirements)
-- [Installation](https://docs.requarks.io/install)
-- [Demo](https://docs.requarks.io/demo)
-- [Changelog](https://docs.requarks.io/releases)
-- [Feature Requests](https://feedback.js.wiki/wiki)
-- [Chat with us on Slack](https://wiki.requarks.io/slack)
-- [Translations](https://docs.requarks.io/dev/translations) *(We need your help!)*
-- [E2E Testing Results](https://dashboard.cypress.io/projects/r7qxah/runs)
-- [Special Thanks](#special-thanks)
-- [Contribute](#contributors)
+- **[Official Website](https://next.js.wiki/)**
+- **[Documentation](https://next.js.wiki/docs/)**
 
-[Follow our Twitter feed](https://twitter.com/requarks) to learn about upcoming updates and new releases!
+:warning: :warning: **THIS IS A VERY BUGGY, INCOMPLETE AND NON-SECURE DEVELOPMENT BRANCH! USE AT YOUR OWN RISK! THERE'S NO UPGRADE PATH FROM THIS BUILD.** :warning: :warning:
 
-<h2 align="center">Donate</h2>
+The current stable release (2.x) is available at https://js.wiki
 
-<div align="center">
+---
 
-Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://wiki.js.org/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`).
-  
-  [![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship)
-  [![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks)
-  [![Donate on OpenCollective](https://img.shields.io/badge/donate-open%20collective-blue.svg?style=popout&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNTZweCIgaGVpZ2h0PSIyNTZweCIgdmlld0JveD0iMCAwIDI1NiAyNTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxnPjxwYXRoIGQ9Ik0yMDkuNzY1MTQ0LDEyOC4xNDk5NzkgQzIwOS43NjUxNDQsMTQ0LjE2MzMgMjA0Ljg2NDM4MSwxNTkuNDg5ODkgMTk2LjQ5ODc0NywxNzIuNzI1MDcyIEwyMjkuOTQ1Njc1LDIwNi4xNzE5OTkgQzI0Ni42ODIxMDUsMTgzLjg1Njc1OSAyNTUuNzI5MzA3LDE1Ni43MTUxNTIgMjU1LjcyOTMwNywxMjguODIxMTAyIEMyNTUuNzI5MzA3LDk5LjU1Njk5MTcgMjQ1Ljk3NDYwMyw3My4wNzEwMjA3IDIyOS4yNTg5NDQsNTEuNDg1ODEyOCBMMTk2LjQ4MzE0LDg0LjIxNDc5NCBDMjA1LjEyMjU2MSw5Ny4yMjI0NjgzIDIwOS43MzY5MDcsMTEyLjQ4NzgxIDIwOS43NDk1MzcsMTI4LjEwMzE1NiBMMjA5Ljc2NTE0NCwxMjguMTQ5OTc5IFoiIGZpbGw9IiNCOEQzRjQiPjwvcGF0aD48cGF0aCBkPSJNMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEM4Mi4xNDYwODcyLDIxMC4yNjg5NTggNDUuMzg3NTA5NCwxNzMuNTE3MzU4IDQ1LjI5MzAzOTMsMTI4LjE0OTk3OSBDNDUuMzYxNzUwMiw4Mi43NjQzMTM4IDgyLjEyNzg0ODcsNDUuOTg0MjU3IDEyNy41MTM0ODQsNDUuODk4MzE4NiBDMTQ0LjI0NDc1Miw0NS44OTgzMTg2IDE1OS41NzEzNDIsNTAuNzk5MDgxNyAxNzIuMTE5NzkyLDU5LjE2NDcxNTQgTDIwNC44NjQzODEsMjYuMzg4OTExNiBDMTgyLjU0MzY1LDkuNjY2NjUxMjkgMTU1LjQwMzQyOSwwLjYzMDg2MzI5OCAxMjcuNTEzNDg0LDAuNjM2NDk0NDAzIEM1Ny4xMjM1NDM3LDAuNjM2NDk0NDAzIDAsNTcuNzYwMDM4MSAwLDEyOC4xNDk5NzkgQzAsMTk4LjUwODcwNCA1Ny4xMjM1NDM3LDI1NS42NjM0NjMgMTI3LjUxMzQ4NCwyNTUuNjYzNDYzIEMxNTUuNTM3MzUyLDI1NS43NDA4NzYgMTgyLjc3NTk4OSwyNDYuNDA4NTEgMjA0Ljg2NDM4MSwyMjkuMTYxODg0IEwxNzEuNDE3NDU0LDE5NS43MzA1NjQgQzE1OS41NTU3MzQsMjA1LjQ4NTI2OCAxNDQuMjYwMzU5LDIxMC4zNTQ4MTYgMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEwxMjcuNTEzNDg0LDIxMC4zNTQ4MTYgWiIgZmlsbD0iIzdGQURGMiI+PC9wYXRoPjwvZz48L3N2Zz4=)](https://opencollective.com/wikijs)
-  [![Donate via Paypal](https://img.shields.io/badge/donate-paypal-blue.svg?style=popout&logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url)  
-  [![Donate via Ethereum](https://img.shields.io/badge/donate-ethereum-999.svg?style=popout&logo=ethereum&logoColor=CCC)](https://etherscan.io/address/0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5)
-  [![Donate via Bitcoin](https://img.shields.io/badge/donate-bitcoin-ff9900.svg?style=popout&logo=bitcoin&logoColor=CCC)](https://checkout.opennode.com/p/2553c612-f863-4407-82b3-1a7685268747)
-  [![Buy a T-Shirt](https://img.shields.io/badge/buy-t--shirts-teal.svg?style=popout&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4Igp3aWR0aD0iMjQiIGhlaWdodD0iMjQiCnZpZXdCb3g9IjAgMCAxOTIgMTkyIgpzdHlsZT0iIGZpbGw6IzAwMDAwMDsiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIGZvbnQtZmFtaWx5PSJub25lIiBmb250LXdlaWdodD0ibm9uZSIgZm9udC1zaXplPSJub25lIiB0ZXh0LWFuY2hvcj0ibm9uZSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0wLDE5MnYtMTkyaDE5MnYxOTJ6IiBmaWxsPSJub25lIj48L3BhdGg+PGcgZmlsbD0iIzFhYmM5YyI+PGcgaWQ9InN1cmZhY2UxIj48cGF0aCBkPSJNOTYsMGMtMTUuMjE4NzUsMCAtMjQuNjg3NSwzLjY1NjI1IC0yNS41LDRsLTIyLjUsNy4yNWMtMTAuNDA2MjUsMy4xODc1IC0xOS4wOTM3NSw5LjQzNzUgLTI1LjUsMTguMjVsLTIyLjUsNDIuNWwyNy4yNSwxNi43NWwxMi43NSwtMjR2MTE5LjI1YzAsNC40MDYyNSAyNS4wNjI1LDggNTYsOGMzMC45Mzc1LDAgNTYsLTMuNTkzNzUgNTYsLTh2LTExOS4yNWwxMi43NSwyNGwyNy4yNSwtMTYuNzVsLTIyLjUsLTQyLjVjLTYuNDA2MjUsLTguODEyNSAtMTUuMTU2MjUsLTE1LjA2MjUgLTI0Ljc1LC0xOC4yNWwtMjIuMjUsLTcuMjVjLTAuMTg3NSwwIC0xLjAzMTI1LDEuMzEyNSAtMiwyLjc1bDEuMjUsLTIuNWMwLDAgLTkuODQzNzUsLTQuMjUgLTI1Ljc1LC00LjI1ek05Niw4YzExLjQwNjI1LDAgMTguNDM3NSwyLjI1IDIxLDMuMjVjLTQuNDY4NzUsNS43NSAtMTEuNDA2MjUsMTIuNzUgLTIxLDEyLjc1Yy05LjQwNjI1LDAgLTE2LjQwNjI1LC03LjA2MjUgLTIwLjc1LC0xMi43NWMyLjg3NSwtMS4wNjI1IDkuODc1LC0zLjI1IDIwLjc1LC0zLjI1eiI+PC9wYXRoPjwvZz48L2c+PC9nPjwvc3ZnPg==)](https://wikijs.threadless.com)
+## Requirements
 
-</div>
+- Node.js **18.x** or later
+- Yarn
+- PostgreSQL **11** or later
 
-<h2 align="center">Gold Tier Sponsors</h2>
+## Setup
 
-<div align="center">
-<table>
-  <tbody>
-    <tr>
-      <td align="center" valign="middle" width="444">
-        <a href="https://trans-zero.com/" target="_blank">
-          <img src="https://cdn.js.wiki/images/sponsors/transzero.png">
-        </a>
-      </td>
-    </tr>
-  </tbody>
-</table>
-</div>
+1. Clone the project
+1. Make a copy of `config.sample.yml` and rename it to `config.yml`
+1. Edit `config.yml` and fill in the database details. **You need an empty PostgreSQL database.**
+1. Run the following commands to install dependencies and generate the client assets:
+    ```sh
+    yarn
+    yarn legacy:build
+    cd ux
+    yarn
+    yarn build
+    cd ..
+    ```
+1. Run this command to start the server:
+    ```sh
+    node server
+    ```
+1. In your browser, navigate to `http://localhost:3000` *(or the IP/hostname of the server and the PORT you defined earlier.)*
+1. Login using the default administrator user:
+    - Email: `admin@example.com`
+    - Password: `12345678`
 
-<h2 align="center">GitHub Sponsors</h2>
+> **DO NOT** report bugs. This build is **VERY** buggy and **VERY** incomplete. Absolutely **NO** support is provided either.
 
-Support this project by becoming a sponsor. Your name will show up in the Contribute page of all Wiki.js installations as well as here with a link to your website! [[Become a sponsor](https://github.com/users/NGPixel/sponsorship)]
+## Using VS Code Dev Environment
 
-<div align="center">
-<table>
-  <tbody>
-    <tr>
-      <td align="center" valign="middle" width="444">
-        <a href="https://www.stellarhosted.com/" target="_blank">
-          <img src="https://cdn.js.wiki/images/sponsors/stellarhosted.png">
-        </a>
-      </td>
-      <td align="center" valign="middle" width="444">
-        <a href="https://www.hostwiki.com/" target="_blank">
-          <img src="https://cdn.js.wiki/images/sponsors/hostwiki.png">
-        </a>
-      </td>
-    </tr>
-  </tbody>
-</table>
-</div>
+### Requirements
 
-<div align="center">
-<table>
-  <tbody>
-    <tr>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/alexksso" target="_blank">
-          Alexander Casassovici<br />(@alexksso)
-        </a>
-      </td>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/broxen" target="_blank">
-          Broxen<br />(@broxen)
-        </a>
-      </td>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/xDacon" target="_blank">
-          Dacon<br />(@xDacon)
-        </a>
-      </td>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/GigabiteLabs" target="_blank">
-          <img src="https://static.requarks.io/sponsors/gigabitelabs-148x129.png">
-        </a>
-      </td>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/JayDaley" target="_blank">
-          Jay Daley<br />(@JayDaley)
-        </a>
-      </td>
-      <td align="center" valign="middle" width="148">
-        <a href="https://github.com/idokka" target="_blank">
-          Oleksii<br />(@idokka)
-        </a>
-      </td>
-      <!--<td align="center" valign="middle" width="148">
-        <a href="https://github.com/sponsors/NGPixel" target="_blank">
-          <img src="https://static.requarks.io/sponsors/become-148x72.png">
-        </a>
-      </td>-->
-    </tr>
-  </tbody>
-</table>
-
-<table><tbody><tr><td>
-<img width="441" height="1" />
-
-- Akira Suenami ([@a-suenami](https://github.com/a-suenami))
-- Arnaud Marchand ([@snuids](https://github.com/snuids))
-- Brian Douglass ([@bhdouglass](https://github.com/bhdouglass))
-- Bryon Vandiver ([@asterick](https://github.com/asterick))
-- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer))
-- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))
-- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))
-- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))
-- Dragan Espenschied ([@despens](https://github.com/despens))
-- Elijah Zobenko ([@he110](https://github.com/he110))
-- Ernie ([@iamernie](https://github.com/iamernie))
-- Fabio Ferrari ([@devxops](https://github.com/devxops))
-- Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa))
-- Florian Moss ([@florianmoss](https://github.com/florianmoss))
-- GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen))
-- HeavenBay ([@HeavenBay](https://github.com/heavenbay))
-- Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy))
-- Jaimyn Mayer ([@jabelone](https://github.com/jabelone))
-- Jay Lee ([@polyglotm](https://github.com/polyglotm))
-- Kelly Wardrop ([@dropcoded](https://github.com/dropcoded))
-- Loki ([@binaryloki](https://github.com/binaryloki))
-- MaFarine ([@MaFarine](https://github.com/MaFarine))
-- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))
-        
-</td><td>
-<img width="441" height="1" />
-        
-- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
-- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
-- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
-- M. Scott Ford ([@mscottford](https://github.com/mscottford))
-- Nick Halase ([@nhalase](https://github.com/nhalase))
-- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
-- Noel Cower ([@nilium](https://github.com/nilium))
-- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))
-- Robert Lanzke ([@winkelement](https://github.com/winkelement))
-- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))
-- Sean Coffey ([@seanecoffey](https://github.com/seanecoffey))
-- Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro))
-- Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu))
-- Tyler Denman ([@tylerguy](https://github.com/tylerguy))
-- Victor Bilgin ([@vbilgin](https://github.com/vbilgin))
-- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))
-- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
-- aytaa ([@aytaa](https://github.com/aytaa))
-- magicpotato ([@fortheday](https://github.com/fortheday))
-- motoacs ([@motoacs](https://github.com/motoacs))
-- rburckner ([@rburckner](https://github.com/rburckner))
-- scorpion ([@scorpion](https://github.com/scorpion))
-- valantien ([@valantien](https://github.com/valantien))
-        
-</td></tr></tbody></table>
-</div>
+- VS Code
+- Docker Desktop
+- **Windows-only:** WSL 2 + WSL Integration enabled in Docker Desktop
 
-<h2 align="center">OpenCollective Sponsors</h2>
+### Usage
 
-Support this project by becoming a sponsor. Your logo will show up in the Contribute page of all Wiki.js installations as well as here with a link to your website! [[Become a sponsor](https://opencollective.com/wikijs#sponsor)]
+1. Clone the project
+1. Open the project in VS Code
+1. Make sure you have **Dev Containers** extension installed. (On Windows, you need the **WSL** VS Code extension as well.)
+1. Reopen the project in container (from the popup in the lower-right corner of the screen when opening the project, or via the Command Palette (Ctrl+Shift+P) afterwards).
+1. Once in container mode, run the task "Create terminals" from the Command Palette:
+    - Launch the Command Palette (Ctrl+Shift+P)
+    - Type "Run Task" and press Enter
+    - Select the task "Create terminals" and press Enter
+1. Two terminals will launch in split-screen mode at the bottom of the screen. **Server** on the left and **UX** on the right.
+1. In the left-side terminal (Server), run the command:
+    ```sh
+    yarn legacy:build
+    ```
+1. In the right-side terminal (UX), run the command:
+    ```sh
+    yarn build
+    ```
+1. Back in the left-side terminal (Server), run the command:
+    ```sh
+    yarn dev
+    ```
+1. Open your browser to `http://localhost:3000`
+1. Login using the default administrator user:
+    - Email: `admin@example.com`
+    - Password: `12345678`
 
-<div align="center">
-<table>
-  <tbody>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/0/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/1/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/1/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/2/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/2/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/3/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/3/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/4/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/4/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/5/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/5/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/6/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/6/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/7/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/7/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/8/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/8/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/9/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/9/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/10/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/10/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/11/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/11/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/12/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/12/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/13/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/13/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/14/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/14/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/15/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/15/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/16/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/16/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/17/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/17/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/18/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/18/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/19/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/19/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/20/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/20/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/21/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/21/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/22/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/22/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/23/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/23/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/24/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/24/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/25/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/25/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/26/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/26/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/27/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/27/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/28/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/28/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/29/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/29/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/30/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/30/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/31/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/31/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/32/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/32/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/33/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/33/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/34/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/34/avatar.svg"></a>
-      </td>
-    </tr>
-    <tr>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/35/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/35/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/36/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/36/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/37/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/37/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/38/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/38/avatar.svg"></a>
-      </td>
-      <td align="center" valign="middle">
-        <a href="https://opencollective.com/wikijs/sponsor/39/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/39/avatar.svg"></a>
-      </td>
-    </tr>
-  </tbody>
-</table>
-</div>
+> **DO NOT** report bugs. This build is **VERY** buggy and **VERY** incomplete. Absolutely **NO** support is provided either.
 
-<h2 align="center">Patreon Backers</h2>
+### Server Development
 
-Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/requarks)]
+From the left-side terminal (Server), run the command:
 
-<div align="center">
-<table><tbody><tr><td>
-<img width="441" height="1" />
-
-- Al Romano
-- Alex Balabanov
-- Alex Zen
-- Arti Zirk
-- Brandon Curtis
-- Dave 'Sri' Seah
-- djagoo
-- Douglas Lassance
-- Ernie Reid
-- Etienne
-- Flemis Jurgenheimer
-- Florent
-- Günter Pavlas
-- hong
-- Hope
-- Ian
-  
-</td><td>
-<img width="441" height="1" />
-
-- Iskander Callos
-- Josh Stewart
-- Justin Dunsworth
-- Keir
-- Loïc CRAMPON
-- Ludgeir Ibanez
-- Mark Mansur
-- Matt Gedigian
-- Patryk
-- Philipp Schürch
-- Tracey Duffy
-- Richeir
-- Shad Narcher
-- SmartNET.works
-- Stepan Sokolovskyi
-- Zach Maynard
-- 张白驹
-
-</td></tr></tbody></table>
-</div>
+```sh
+yarn dev
+```
+
+This will launch the server and automatically restart upon modification of any server files.
+
+Only precompiled client assets are served in this mode. See the sections below on how to modify the frontend and run in SPA (Single Page Application) mode.
 
-<h2 align="center">OpenCollective Backers</h2>
+### Frontend Development (Quasar/Vue 3)
 
-Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/wikijs#backer)]
+> Make sure you are running `yarn dev` in the left-side terminal (Server) first! Requests still need to be forwarded to the server, even in SPA mode!
 
-<a href="https://opencollective.com/wikijs#backers" target="_blank"><img src="https://opencollective.com/wikijs/backers.svg?width=890"></a>
+If you wish to modify any frontend content (under `/ux`), you need to start the Quasar Dev Server in the right-side terminal (UX):
 
-<h2 align="center">Contributors</h2>
+```sh
+yarn dev
+```
 
-This project exists thanks to all the people who contribute. [[Contribute]](https://github.com/Requarks/wiki/blob/master/.github/CONTRIBUTING.md).
-<a href="https://github.com/Requarks/wiki/graphs/contributors"><img src="https://opencollective.com/wikijs/contributors.svg?width=890" /></a>
+You can then access the site at `http://localhost:3001`. Notice the port being `3001` rather than `3000`. The app runs in a SPA (single-page application) mode and automatically hot-reload any modified component. Any requests made to the `/graphql` endpoint are automatically forwarded to the server running on port `3000`, which is why both must be running at the same time.
 
-<h2 align="center">Special Thanks</h2>
+Note that not all sections/features are available from this mode, notably the page editing features which still relies on the old client code (Vuetify/Vue 2). For example, trying to edit a page will simply not work. You must use the normal mode (port 3000) to edit pages as it relies on legacy client code. As more features gets ported / developed for Vue 3, they will become available in the SPA mode.
 
-![Algolia](https://js.wiki/legacy/logo_algolia.png)  
-[Algolia](https://www.algolia.com/) for providing access to their incredible search engine.
+Any change you make to the frontend will not be reflected on port 3000 until you run the command `yarn build` in the right-side terminal.
 
-![Browserstack](https://js.wiki/legacy/logo_browserstack.png)  
-[Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools.
+### Legacy Frontend Development (Vuetify/Vue 2)
 
-![Cloudflare](https://js.wiki/legacy/logo_cloudflare.png)  
-[Cloudflare](https://www.cloudflare.com/) for providing their great CDN, SSL and advanced networking services.
+Client code from Wiki.js 2.x is located under `/client`. Some sections still rely on this legacy code (notably the page editing features). Code is gradually being removed from this location and replaced with newer code in `/ux`.
 
-![DigitalOcean](https://js.wiki/legacy/logo_digitalocean.png)  
-[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site.
+In the unlikely event that you need to modify legacy code and regenerate the old client files, you can do so by running in this command in the left-side terminal (Server):
+```sh
+yarn legacy:build
+```
 
-![Icons8](https://static.requarks.io/logo/icons8-text-h40.png)  
-[Icons8](https://icons8.com/) for providing beautiful icon sets.
+Then run `yarn dev` to start the server again.
 
-![Lokalise](https://static.requarks.io/logo/lokalise-text-h40.png)  
-[Lokalise](https://lokalise.com/) for providing access to their great localization tool.
+### pgAdmin
 
-![Netlify](https://js.wiki/legacy/logo_netlify.png)  
-[Netlify](https://www.netlify.com) for providing hosting for landings and blog websites.
+A web version of pgAdmin (a PostgreSQL administration tool) is available at `http://localhost:8000`. Use the login `dev` / `123123` to login.
 
-![ngrok](https://static.requarks.io/logo/ngrok-h40.png)  
-[ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services.
+The server **dev** should already be available under **Servers**. If that's not the case, add a new one with the following settings:
 
-![Porkbun](https://static.requarks.io/logo/porkbun.png)  
-[Porkbun](https://www.porkbun.com) for providing domain registration services.
+- Hostname: `db`
+- Port: `5432`
+- Username: `postgres`
+- Password: `postgres`
+- Database: `postgres`

+ 66 - 73
client/components/editor.vue

@@ -141,8 +141,8 @@ export default {
       default: null
     },
     pageId: {
-      type: Number,
-      default: 0
+      type: String,
+      default: ''
     },
     checkoutDate: {
       type: String,
@@ -369,33 +369,32 @@ export default {
           // -> UPDATE EXISTING PAGE
           // --------------------------------------------
 
-          const conflictResp = await this.$apollo.query({
-            query: gql`
-              query ($id: Int!, $checkoutDate: Date!) {
-                pages {
-                  checkConflicts(id: $id, checkoutDate: $checkoutDate)
-                }
-              }
-            `,
-            fetchPolicy: 'network-only',
-            variables: {
-              id: this.pageId,
-              checkoutDate: this.checkoutDateActive
-            }
-          })
-          if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
-            this.$root.$emit('saveConflict')
-            throw new Error(this.$t('editor:conflict.warning'))
-          }
+          // const conflictResp = await this.$apollo.query({
+          //   query: gql`
+          //     query ($id: Int!, $checkoutDate: Date!) {
+          //       pages {
+          //         checkConflicts(id: $id, checkoutDate: $checkoutDate)
+          //       }
+          //     }
+          //   `,
+          //   fetchPolicy: 'network-only',
+          //   variables: {
+          //     id: this.pageId,
+          //     checkoutDate: this.checkoutDateActive
+          //   }
+          // })
+          // if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
+          //   this.$root.$emit('saveConflict')
+          //   throw new Error(this.$t('editor:conflict.warning'))
+          // }
 
           let resp = await this.$apollo.mutate({
             mutation: gql`
               mutation (
-                $id: Int!
+                $id: UUID!
                 $content: String
                 $description: String
                 $editor: String
-                $isPrivate: Boolean
                 $isPublished: Boolean
                 $locale: String
                 $path: String
@@ -406,32 +405,27 @@ export default {
                 $tags: [String]
                 $title: String
               ) {
-                pages {
-                  update(
-                    id: $id
-                    content: $content
-                    description: $description
-                    editor: $editor
-                    isPrivate: $isPrivate
-                    isPublished: $isPublished
-                    locale: $locale
-                    path: $path
-                    publishEndDate: $publishEndDate
-                    publishStartDate: $publishStartDate
-                    scriptCss: $scriptCss
-                    scriptJs: $scriptJs
-                    tags: $tags
-                    title: $title
-                  ) {
-                    operation {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    page {
-                      updatedAt
-                    }
+                updatePage(
+                  id: $id
+                  content: $content
+                  description: $description
+                  editor: $editor
+                  isPublished: $isPublished
+                  locale: $locale
+                  path: $path
+                  publishEndDate: $publishEndDate
+                  publishStartDate: $publishStartDate
+                  scriptCss: $scriptCss
+                  scriptJs: $scriptJs
+                  tags: $tags
+                  title: $title
+                ) {
+                  operation {
+                    succeeded
+                    message
+                  }
+                  page {
+                    updatedAt
                   }
                 }
               }
@@ -442,7 +436,6 @@ export default {
               description: this.$store.get('page/description'),
               editor: this.$store.get('editor/editorKey'),
               locale: this.$store.get('page/locale'),
-              isPrivate: false,
               isPublished: this.$store.get('page/isPublished'),
               path: this.$store.get('page/path'),
               publishEndDate: this.$store.get('page/publishEndDate') || '',
@@ -453,7 +446,7 @@ export default {
               title: this.$store.get('page/title')
             }
           })
-          resp = _.get(resp, 'data.pages.update', {})
+          resp = _.get(resp, 'data.updatePage', {})
           if (_.get(resp, 'operation.succeeded')) {
             this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
             this.isConflict = false
@@ -547,30 +540,30 @@ export default {
         styl.appendChild(document.createTextNode(css))
       }
     }, 1000)
-  },
-  apollo: {
-    isConflict: {
-      query: gql`
-        query ($id: Int!, $checkoutDate: Date!) {
-          pages {
-            checkConflicts(id: $id, checkoutDate: $checkoutDate)
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      pollInterval: 5000,
-      variables () {
-        return {
-          id: this.pageId,
-          checkoutDate: this.checkoutDateActive
-        }
-      },
-      update: (data) => _.cloneDeep(data.pages.checkConflicts),
-      skip () {
-        return this.mode === 'create' || this.isSaving || !this.isDirty
-      }
-    }
   }
+  // apollo: {
+  //   isConflict: {
+  //     query: gql`
+  //       query ($id: Int!, $checkoutDate: Date!) {
+  //         pages {
+  //           checkConflicts(id: $id, checkoutDate: $checkoutDate)
+  //         }
+  //       }
+  //     `,
+  //     fetchPolicy: 'network-only',
+  //     pollInterval: 5000,
+  //     variables () {
+  //       return {
+  //         id: this.pageId,
+  //         checkoutDate: this.checkoutDateActive
+  //       }
+  //     },
+  //     update: (data) => _.cloneDeep(data.pages.checkConflicts),
+  //     skip () {
+  //       return this.mode === 'create' || this.isSaving || !this.isDirty
+  //     }
+  //   }
+  // }
 }
 </script>
 

+ 0 - 45
server/controllers/common.js

@@ -238,51 +238,6 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
         js: ''
       }
     }
-
-    // -> From Template
-    if (req.query.from && tmplCreateRegex.test(req.query.from)) {
-      let tmplPageId = 0
-      let tmplVersionId = 0
-      if (req.query.from.indexOf(',')) {
-        const q = req.query.from.split(',')
-        tmplPageId = _.toSafeInteger(q[0])
-        tmplVersionId = _.toSafeInteger(q[1])
-      } else {
-        tmplPageId = _.toSafeInteger(req.query.from)
-      }
-
-      if (tmplVersionId > 0) {
-        // -> From Page Version
-        const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId })
-        if (!pageVersion) {
-          _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-          return res.status(404).render('notfound', { action: 'template' })
-        }
-        if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) {
-          _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-          return res.render('unauthorized', { action: 'sourceVersion' })
-        }
-        page.content = Buffer.from(pageVersion.content).toString('base64')
-        page.editorKey = pageVersion.editor
-        page.title = pageVersion.title
-        page.description = pageVersion.description
-      } else {
-        // -> From Page Live
-        const pageOriginal = await WIKI.db.pages.query().findById(tmplPageId)
-        if (!pageOriginal) {
-          _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-          return res.status(404).render('notfound', { action: 'template' })
-        }
-        if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) {
-          _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-          return res.render('unauthorized', { action: 'source' })
-        }
-        page.content = Buffer.from(pageOriginal.content).toString('base64')
-        page.editorKey = pageOriginal.editorKey
-        page.title = pageOriginal.title
-        page.description = pageOriginal.description
-      }
-    }
   }
 
   res.render('editor', { page, injectCode, effectivePermissions })

+ 1 - 1
server/core/asar.js

@@ -9,7 +9,7 @@ const fs = require('fs')
  */
 
 const packages = {
-  'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)
+  'twemoji': path.join(WIKI.ROOTPATH, `assets-legacy/svg/twemoji.asar`)
 }
 
 module.exports = {

+ 2 - 1
server/db/migrations/3.0.0.js

@@ -603,7 +603,8 @@ exports.up = async knex => {
       auth: {
         [authModuleId]: {
           password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
-          mustChangePwd: !process.env.ADMIN_PASS,
+          mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented
+          // mustChangePwd: !process.env.ADMIN_PASS,
           restrictLogin: false,
           tfaRequired: false,
           tfaSecret: ''

+ 20 - 3
server/graph/resolvers/page.js

@@ -1,5 +1,6 @@
 const _ = require('lodash')
 const graphHelper = require('../../helpers/graph')
+const pageHelper = require('../../helpers/page')
 
 module.exports = {
   Query: {
@@ -139,7 +140,7 @@ module.exports = {
       return results
     },
     /**
-     * FETCH SINGLE PAGE
+     * FETCH SINGLE PAGE BY ID
      */
     async pageById (obj, args, context, info) {
       let page = await WIKI.db.pages.getPageFromDb(args.id)
@@ -160,6 +161,22 @@ module.exports = {
         throw new WIKI.Error.PageNotFound()
       }
     },
+    /**
+     * FETCH SINGLE PAGE BY PATH
+     */
+    async pageByPath (obj, args, context, info) {
+      const pageArgs = pageHelper.parsePath(args.path)
+      let page = await WIKI.db.pages.getPageFromDb(pageArgs)
+      if (page) {
+        return {
+          ...page,
+          locale: page.localeCode,
+          editor: page.editorKey
+        }
+      } else {
+        throw new Error('ERR_PAGE_NOT_FOUND')
+      }
+    },
     /**
      * FETCH TAGS
      */
@@ -366,7 +383,7 @@ module.exports = {
           user: context.req.user
         })
         return {
-          responseResult: graphHelper.generateSuccess('Page created successfully.'),
+          operation: graphHelper.generateSuccess('Page created successfully.'),
           page
         }
       } catch (err) {
@@ -383,7 +400,7 @@ module.exports = {
           user: context.req.user
         })
         return {
-          responseResult: graphHelper.generateSuccess('Page has been updated.'),
+          operation: graphHelper.generateSuccess('Page has been updated.'),
           page
         }
       } catch (err) {

+ 6 - 2
server/graph/schemas/page.graphql

@@ -34,6 +34,10 @@ extend type Query {
     id: Int!
   ): Page
 
+  pageByPath(
+    path: String!
+  ): Page
+
   tags: [PageTag]!
 
   searchTags(
@@ -80,7 +84,7 @@ extend type Mutation {
   ): PageResponse
 
   updatePage(
-    id: Int!
+    id: UUID!
     content: String
     description: String
     editor: String
@@ -158,7 +162,7 @@ type PageMigrationResponse {
 }
 
 type Page {
-  id: Int
+  id: UUID
   path: String
   hash: String
   title: String

+ 6 - 15
server/models/pageHistory.js

@@ -19,7 +19,7 @@ module.exports = class PageHistory extends Model {
         hash: {type: 'string'},
         title: {type: 'string'},
         description: {type: 'string'},
-        isPublished: {type: 'boolean'},
+        publishState: {type: 'string'},
         publishStartDate: {type: 'string'},
         publishEndDate: {type: 'string'},
         content: {type: 'string'},
@@ -60,14 +60,6 @@ module.exports = class PageHistory extends Model {
           to: 'users.id'
         }
       },
-      editor: {
-        relation: Model.BelongsToOneRelation,
-        modelClass: require('./editors'),
-        join: {
-          from: 'pageHistory.editorKey',
-          to: 'editors.key'
-        }
-      },
       locale: {
         relation: Model.BelongsToOneRelation,
         modelClass: require('./locales'),
@@ -89,18 +81,18 @@ module.exports = class PageHistory extends Model {
   static async addVersion(opts) {
     await WIKI.db.pageHistory.query().insert({
       pageId: opts.id,
+      siteId: opts.siteId,
       authorId: opts.authorId,
       content: opts.content,
       contentType: opts.contentType,
       description: opts.description,
-      editorKey: opts.editorKey,
+      editor: opts.editor,
       hash: opts.hash,
-      isPrivate: (opts.isPrivate === true || opts.isPrivate === 1),
-      isPublished: (opts.isPublished === true || opts.isPublished === 1),
+      publishState: opts.publishState,
       localeCode: opts.localeCode,
       path: opts.path,
-      publishEndDate: opts.publishEndDate || '',
-      publishStartDate: opts.publishStartDate || '',
+      publishEndDate: opts.publishEndDate?.toISO(),
+      publishStartDate: opts.publishStartDate?.toISO(),
       title: opts.title,
       action: opts.action || 'updated',
       versionDate: opts.versionDate
@@ -116,7 +108,6 @@ module.exports = class PageHistory extends Model {
         'pageHistory.path',
         'pageHistory.title',
         'pageHistory.description',
-        'pageHistory.isPrivate',
         'pageHistory.isPublished',
         'pageHistory.publishStartDate',
         'pageHistory.publishEndDate',

+ 12 - 12
server/models/pages.js

@@ -421,8 +421,8 @@ module.exports = class Page extends Model {
       content: opts.content,
       description: opts.description,
       publishState: opts.publishState,
-      publishEndDate: opts.publishEndDate || '',
-      publishStartDate: opts.publishStartDate || '',
+      publishEndDate: opts.publishEndDate?.toISO(),
+      publishStartDate: opts.publishStartDate?.toISO(),
       title: opts.title,
       extra: JSON.stringify({
         ...ogPage.extra,
@@ -439,18 +439,18 @@ module.exports = class Page extends Model {
     await WIKI.db.pages.renderPage(page)
     WIKI.events.outbound.emit('deletePageFromCache', page.hash)
 
-    // -> Update Search Index
-    const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
-    page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
-    await WIKI.data.searchEngine.updated(page)
+    // // -> Update Search Index
+    // const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
+    // page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
+    // await WIKI.data.searchEngine.updated(page)
 
     // -> Update on Storage
-    if (!opts.skipStorage) {
-      await WIKI.db.storage.pageEvent({
-        event: 'updated',
-        page
-      })
-    }
+    // if (!opts.skipStorage) {
+    //   await WIKI.db.storage.pageEvent({
+    //     event: 'updated',
+    //     page
+    //   })
+    // }
 
     // -> Perform move?
     if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {

+ 2 - 10
server/views/base.pug

@@ -40,28 +40,20 @@ html(lang=siteConfig.lang)
 
     //- CSS
     
-      
-    link(
-      type='text/css'
-      rel='stylesheet'
-      href='/_assets-legacy/css/app.629ebe3c082227dbee31.css'
-    )
-      
-    
 
     //- JS
     
       
     script(
       type='text/javascript'
-      src='/_assets-legacy/js/runtime.js?1664769154'
+      src='/_assets-legacy/js/runtime.js'
       )
       
     
       
     script(
       type='text/javascript'
-      src='/_assets-legacy/js/app.js?1664769154'
+      src='/_assets-legacy/js/app.js'
       )
       
     

+ 1 - 1
server/views/editor.pug

@@ -7,7 +7,7 @@ block head
 block body
   #root
     editor(
-      :page-id=page.id
+      page-id=page.id
       locale=page.localeCode
       path=page.path
       title=page.title

+ 0 - 6
ux/.yarnrc.yml

@@ -5,18 +5,12 @@ enableTelemetry: false
 nodeLinker: node-modules
 
 packageExtensions:
-  '@quasar/vite-plugin@*':
-    dependencies:
-      'quasar': '*'
   'rollup-plugin-visualizer@*':
     dependencies:
       'rollup': '*'
   'v-network-graph@*':
     dependencies:
       'd3-force': '*'
-  '@intlify/vite-plugin-vue-i18n@*':
-    dependencies:
-      'vite': '*'
 
 plugins:
   - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs

+ 1 - 0
ux/quasar.config.js

@@ -77,6 +77,7 @@ module.exports = configure(function (/* ctx */) {
 
       extendViteConf (viteConf) {
         viteConf.build.assetsDir = '_assets'
+        // viteConf.resolve.alias.vue = '/workspace/ux/node_modules/vue/dist/vue.esm-bundler.js'
         // viteConf.build.rollupOptions = {
         //   ...viteConf.build.rollupOptions ?? {},
         //   external: [

+ 44 - 44
ux/src/components/BlueprintIcon.vue

@@ -5,7 +5,7 @@ q-item-section(avatar)
     :text-color='avatarTextColor'
     font-size='14px'
     rounded
-    :style='hueRotate !== 0 ? `filter: hue-rotate(` + hueRotate + `deg)` : ``'
+    :style='props.hueRotate !== 0 ? `filter: hue-rotate(` + props.hueRotate + `deg)` : ``'
     )
     q-badge(
       v-if='indicatorDot'
@@ -13,57 +13,57 @@ q-item-section(avatar)
       :color='indicatorDot'
       floating
       )
-      q-tooltip(v-if='indicatorText') {{indicatorText}}
+      q-tooltip(v-if='props.indicatorText') {{props.indicatorText}}
     q-icon(
       v-if='!textMode'
       :name='`img:/_assets/icons/ultraviolet-` + icon + `.svg`'
       size='sm'
     )
-    span.text-uppercase(v-else) {{text}}
+    span.text-uppercase(v-else) {{props.text}}
 </template>
 
-<script>
-export default {
-  name: 'BlueprintIcon',
-  props: {
-    icon: {
-      type: String,
-      default: ''
-    },
-    dark: {
-      type: Boolean,
-      default: false
-    },
-    indicator: {
-      type: String,
-      default: null
-    },
-    indicatorText: {
-      type: String,
-      default: null
-    },
-    hueRotate: {
-      type: Number,
-      default: 0
-    },
-    text: {
-      type: String,
-      default: null
-    }
+<script setup>
+import { computed } from 'vue'
+import { useQuasar } from 'quasar'
+
+const props = defineProps({
+  icon: {
+    type: String,
+    default: ''
+  },
+  dark: {
+    type: Boolean,
+    default: false
+  },
+  indicator: {
+    type: String,
+    default: null
+  },
+  indicatorText: {
+    type: String,
+    default: null
   },
-  data () {
-    return {
-      imgPath: null
-    }
+  hueRotate: {
+    type: Number,
+    default: 0
   },
-  computed: {
-    textMode () { return this.text !== null },
-    avatarBgColor () { return this.$q.dark.isActive || this.dark ? 'dark-4' : 'blue-1' },
-    avatarTextColor () { return this.$q.dark.isActive || this.dark ? 'white' : 'blue-7' },
-    indicatorDot () {
-      if (this.indicator === null) { return null }
-      return (this.indicator === '') ? 'pink' : this.indicator
-    }
+  text: {
+    type: String,
+    default: null
   }
-}
+})
+
+// QUASAR
+
+const $q = useQuasar()
+
+// COMPUTED
+
+const textMode = computed(() => { return props.text !== null })
+const avatarBgColor = computed(() => { return $q.dark.isActive || props.dark ? 'dark-4' : 'blue-1' })
+const avatarTextColor = computed(() => { return $q.dark.isActive || props.dark ? 'white' : 'blue-7' })
+const indicatorDot = computed(() => {
+  if (props.indicator === null) { return null }
+  return (props.indicator === '') ? 'pink' : props.indicator
+})
 </script>

+ 74 - 61
ux/src/components/IconPickerDialog.vue

@@ -1,7 +1,7 @@
 <template lang="pug">
 q-card.icon-picker(flat, style='width: 400px;')
   q-tabs.text-primary(
-    v-model='currentTab'
+    v-model='state.currentTab'
     no-caps
     inline-label
     )
@@ -17,12 +17,12 @@ q-card.icon-picker(flat, style='width: 400px;')
       )
   q-separator
   q-tab-panels(
-    v-model='currentTab'
+    v-model='state.currentTab'
     )
     q-tab-panel(name='icon')
       q-select(
         :options='iconPacks'
-        v-model='selPack'
+        v-model='state.selPack'
         emit-value
         map-options
         outlined
@@ -52,7 +52,7 @@ q-card.icon-picker(flat, style='width: 400px;')
                 size='sm'
               ) {{scope.opt.subset.toUpperCase()}}
       q-input.q-mt-md(
-        v-model='selIcon'
+        v-model='state.selIcon'
         outlined
         label='Icon Name'
         dense
@@ -96,7 +96,7 @@ q-card.icon-picker(flat, style='width: 400px;')
           q-img(
             transition='jump-down'
             :ratio='1'
-            :src='imgPath'
+            :src='state.imgPath'
           )
   q-separator
   q-card-actions
@@ -118,67 +118,80 @@ q-card.icon-picker(flat, style='width: 400px;')
     )
 </template>
 
-<script>
+<script setup>
 import { find } from 'lodash-es'
+import { computed, onMounted, reactive } from 'vue'
 
-export default {
-  props: {
-    value: {
-      type: String,
-      required: true
-    }
-  },
-  data () {
-    return {
-      currentTab: 'icon',
-      selPack: 'las',
-      selIcon: '',
-      imgPath: 'https://placeimg.com/64/64/nature',
-      iconPacks: [
-        { value: 'las', label: 'Line Awesome (solid)', name: 'Line Awesome', subset: 'solid', prefix: 'las la-', reference: 'https://icons8.com/line-awesome' },
-        { value: 'lab', label: 'Line Awesome (brands)', name: 'Line Awesome', subset: 'brands', prefix: 'lab la-', reference: 'https://icons8.com/line-awesome' },
-        { value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' },
-        { value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' },
-        { value: 'far', label: 'Font Awesome (regular)', name: 'Font Awesome', subset: 'regular', prefix: 'far fa-', reference: 'https://fontawesome.com/icons' },
-        { value: 'fal', label: 'Font Awesome (light)', name: 'Font Awesome', subset: 'light', prefix: 'fal fa-', reference: 'https://fontawesome.com/icons' },
-        { value: 'fad', label: 'Font Awesome (duotone)', name: 'Font Awesome', subset: 'duotone', prefix: 'fad fa-', reference: 'https://fontawesome.com/icons' },
-        { value: 'fab', label: 'Font Awesome (brands)', name: 'Font Awesome', subset: 'brands', prefix: 'fab fa-', reference: 'https://fontawesome.com/icons' }
-      ]
-    }
-  },
-  computed: {
-    iconName () {
-      return find(this.iconPacks, ['value', this.selPack]).prefix + this.selIcon
-    },
-    iconPackRefWebsite () {
-      return find(this.iconPacks, ['value', this.selPack]).reference
-    }
-  },
-  mounted () {
-    if (this.value?.startsWith('img:')) {
-      this.currentTab = 'img'
-      this.imgPath = this.value.substring(4)
-    } else {
-      this.currentTab = 'icon'
-      for (const pack of this.iconPacks) {
-        if (this.value?.startsWith(pack.prefix)) {
-          this.selPack = pack.value
-          this.selIcon = this.value.substring(pack.prefix.length)
-          break
-        }
-      }
-    }
-  },
-  methods: {
-    apply () {
-      if (this.currentTab === 'img') {
-        this.$emit('input', `img:${this.imgPath}`)
-      } else {
-        this.$emit('input', this.iconName)
+// PROPS
+
+const props = defineProps({
+  value: {
+    type: String,
+    required: true
+  }
+})
+
+// EMITS
+
+const emit = defineEmits(['input'])
+
+// DATA
+
+const state = reactive({
+  currentTab: 'icon',
+  selPack: 'las',
+  selIcon: '',
+  imgPath: 'https://placeimg.com/64/64/nature'
+})
+
+const iconPacks = [
+  { value: 'las', label: 'Line Awesome (solid)', name: 'Line Awesome', subset: 'solid', prefix: 'las la-', reference: 'https://icons8.com/line-awesome' },
+  { value: 'lab', label: 'Line Awesome (brands)', name: 'Line Awesome', subset: 'brands', prefix: 'lab la-', reference: 'https://icons8.com/line-awesome' },
+  { value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' },
+  { value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' },
+  { value: 'far', label: 'Font Awesome (regular)', name: 'Font Awesome', subset: 'regular', prefix: 'far fa-', reference: 'https://fontawesome.com/icons' },
+  { value: 'fal', label: 'Font Awesome (light)', name: 'Font Awesome', subset: 'light', prefix: 'fal fa-', reference: 'https://fontawesome.com/icons' },
+  { value: 'fad', label: 'Font Awesome (duotone)', name: 'Font Awesome', subset: 'duotone', prefix: 'fad fa-', reference: 'https://fontawesome.com/icons' },
+  { value: 'fab', label: 'Font Awesome (brands)', name: 'Font Awesome', subset: 'brands', prefix: 'fab fa-', reference: 'https://fontawesome.com/icons' }
+]
+
+// COMPUTED
+
+const iconName = computed(() => {
+  return find(iconPacks, ['value', state.selPack]).prefix + state.selIcon
+})
+
+const iconPackRefWebsite = computed(() => {
+  return find(iconPacks, ['value', state.selPack]).reference
+})
+
+// METHODS
+
+function apply () {
+  if (state.currentTab === 'img') {
+    emit('input', `img:${state.imgPath}`)
+  } else {
+    emit('input', state.iconName)
+  }
+}
+
+// MOUNTED
+
+onMounted(() => {
+  if (props.value?.startsWith('img:')) {
+    state.currentTab = 'img'
+    state.imgPath = props.value.substring(4)
+  } else {
+    state.currentTab = 'icon'
+    for (const pack of iconPacks) {
+      if (props.value?.startsWith(pack.prefix)) {
+        state.selPack = pack.value
+        state.selIcon = props.value.substring(pack.prefix.length)
+        break
       }
     }
   }
-}
+})
 </script>
 
 <style lang="scss">

+ 156 - 157
ux/src/components/PagePropertiesDialog.vue

@@ -1,6 +1,6 @@
 <template lang="pug">
 q-card.page-properties-dialog
-  .floating-sidepanel-quickaccess.animated.fadeIn(v-if='showQuickAccess', style='right: 486px;')
+  .floating-sidepanel-quickaccess.animated.fadeIn(v-if='state.showQuickAccess', style='right: 486px;')
     template(v-for='(qa, idx) of quickaccess', :key='`qa-` + qa.key')
       q-btn(
         :icon='qa.icon'
@@ -12,7 +12,7 @@ q-card.page-properties-dialog
         q-tooltip(anchor='center left' self='center right') {{qa.label}}
       q-separator(dark, v-if='idx < quickaccess.length - 1')
   q-toolbar.bg-primary.text-white.flex
-    .text-subtitle2 {{$t('editor.props.pageProperties')}}
+    .text-subtitle2 {{t('editor.props.pageProperties')}}
     q-space
     q-btn(
       icon='las la-times'
@@ -22,61 +22,61 @@ q-card.page-properties-dialog
     )
   q-scroll-area(
     ref='scrollArea'
-    :thumb-style='thumbStyle'
-    :bar-style='barStyle'
+    :thumb-style='siteStore.thumbStyle'
+    :bar-style='siteStore.barStyle'
     style='height: calc(100% - 50px);'
     )
-    q-card-section(ref='card-info')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-info-circle', size='xs')] {{$t('editor.props.info')}}
+    q-card-section(id='refCardInfo')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-info-circle', size='xs')] {{t('editor.props.info')}}
       q-form.q-gutter-sm
         q-input(
-          v-model='title'
-          :label='$t(`editor.props.title`)'
+          v-model='pageStore.title'
+          :label='t(`editor.props.title`)'
           outlined
           dense
         )
         q-input(
-          v-model='description'
-          :label='$t(`editor.props.shortDescription`)'
+          v-model='pageStore.description'
+          :label='t(`editor.props.shortDescription`)'
           outlined
           dense
         )
-    q-card-section.alt-card(ref='card-publishstate')
-      .text-overline.q-pb-xs.items-center.flex #[q-icon.q-mr-sm(name='las la-power-off', size='xs')] {{$t('editor.props.publishState')}}
+    q-card-section.alt-card(id='refCardPublishState')
+      .text-overline.q-pb-xs.items-center.flex #[q-icon.q-mr-sm(name='las la-power-off', size='xs')] {{t('editor.props.publishState')}}
       q-form.q-gutter-md
         div
           q-btn-toggle(
-            v-model='isPublished'
+            v-model='pageStore.isPublished'
             push
             glossy
             no-caps
             toggle-color='primary'
             :options=`[
-              { label: $t('editor.props.draft'), value: false },
-              { label: $t('editor.props.published'), value: true },
-              { label: $t('editor.props.dateRange'), value: null }
+              { label: t('editor.props.draft'), value: false },
+              { label: t('editor.props.published'), value: true },
+              { label: t('editor.props.dateRange'), value: null }
             ]`
           )
-        .text-caption(v-if='isPublished'): em {{$t('editor.props.publishedHint')}}
-        .text-caption(v-else-if='isPublished === false'): em {{$t('editor.props.draftHint')}}
-        template(v-else-if='isPublished === null')
-          .text-caption: em {{$t('editor.props.dateRangeHint')}}
+        .text-caption(v-if='pageStore.isPublished'): em {{t('editor.props.publishedHint')}}
+        .text-caption(v-else-if='pageStore.isPublished === false'): em {{t('editor.props.draftHint')}}
+        template(v-else-if='pageStore.isPublished === null')
+          .text-caption: em {{t('editor.props.dateRangeHint')}}
           q-date(
-            v-model='publishingRange'
+            v-model='pageStore.publishingRange'
             range
             flat
             bordered
             landscape
             minimal
             )
-    q-card-section(ref='card-relations')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-sun', size='xs')] {{$t('editor.props.relations')}}
+    q-card-section(id='refCardRelations')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-sun', size='xs')] {{t('editor.props.relations')}}
       q-list.rounded-borders.q-mb-sm.bg-white(
-        v-if='relations.length > 0'
+        v-if='pageStore.relations.length > 0'
         separator
         bordered
         )
-        q-item(v-for='rel of relations', :key='`rel-id-` + rel.id')
+        q-item(v-for='rel of pageStore.relations', :key='`rel-id-` + rel.id')
           q-item-section(side)
             q-icon(:name='rel.icon')
           q-item-section
@@ -107,130 +107,130 @@ q-card.page-properties-dialog
               @click='removeRelation(rel)'
             )
       q-btn.full-width(
-        :label='$t(`editor.props.relationAdd`)'
+        :label='t(`editor.props.relationAdd`)'
         icon='las la-plus'
         no-caps
         unelevated
         color='secondary'
         @click='newRelation'
         )
-        q-tooltip {{$t('editor.props.relationAddHint')}}
-    q-card-section.alt-card(ref='card-scripts')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-code', size='xs')] {{$t('editor.props.scripts')}}
+        q-tooltip {{t('editor.props.relationAddHint')}}
+    q-card-section.alt-card(id='refCardScripts')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-code', size='xs')] {{t('editor.props.scripts')}}
       q-btn.full-width(
-        :label='$t(`editor.props.jsLoad`)'
+        :label='t(`editor.props.jsLoad`)'
         icon='lab la-js-square'
         no-caps
         unelevated
         color='secondary'
         @click='editScripts(`jsLoad`)'
         )
-        q-tooltip {{$t('editor.props.jsLoadHint')}}
+        q-tooltip {{t('editor.props.jsLoadHint')}}
       q-btn.full-width.q-mt-sm(
-        :label='$t(`editor.props.jsUnload`)'
+        :label='t(`editor.props.jsUnload`)'
         icon='lab la-js-square'
         no-caps
         unelevated
         color='secondary'
         @click='editScripts(`jsUnload`)'
         )
-        q-tooltip {{$t('editor.props.jsUnloadHint')}}
+        q-tooltip {{t('editor.props.jsUnloadHint')}}
       q-btn.full-width.q-mt-sm(
-        :label='$t(`editor.props.styles`)'
+        :label='t(`editor.props.styles`)'
         icon='lab la-css3-alt'
         no-caps
         unelevated
         color='secondary'
         @click='editScripts(`styles`)'
         )
-        q-tooltip {{$t('editor.props.stylesHint')}}
-    q-card-section.q-pb-lg(ref='card-sidebar')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-ruler-vertical', size='xs')] {{$t('editor.props.sidebar')}}
+        q-tooltip {{t('editor.props.stylesHint')}}
+    q-card-section.q-pb-lg(id='refCardSidebar')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-ruler-vertical', size='xs')] {{t('editor.props.sidebar')}}
       q-form.q-gutter-md.q-pt-sm
         div
           q-toggle(
-            v-model='showSidebar'
+            v-model='pageStore.showSidebar'
             dense
-            :label='$t(`editor.props.showSidebar`)'
+            :label='t(`editor.props.showSidebar`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
         div
           q-toggle(
-            v-if='showSidebar'
-            v-model='showToc'
+            v-if='pageStore.showSidebar'
+            v-model='pageStore.showToc'
             dense
-            :label='$t(`editor.props.showToc`)'
+            :label='t(`editor.props.showToc`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
         div(
-          v-if='showSidebar && showToc'
+          v-if='pageStore.showSidebar && pageStore.showToc'
           style='padding-left: 40px;'
           )
-          .text-caption {{$t('editor.props.tocMinMaxDepth')}} #[strong (H{{tocDepth.min}} &rarr; H{{tocDepth.max}})]
+          .text-caption {{t('editor.props.tocMinMaxDepth')}} #[strong (H{{pageStore.tocDepth.min}} &rarr; H{{pageStore.tocDepth.max}})]
           q-range(
-            v-model='tocDepth'
+            v-model='pageStore.tocDepth'
             :min='1'
             :max='6'
             color='primary'
-            :left-label-value='`H` + tocDepth.min'
-            :right-label-value='`H` + tocDepth.max'
+            :left-label-value='`H` + pageStore.tocDepth.min'
+            :right-label-value='`H` + pageStore.tocDepth.max'
             snap
             label
             markers
           )
         div
           q-toggle(
-            v-if='showSidebar'
-            v-model='showTags'
+            v-if='pageStore.showSidebar'
+            v-model='pageStore.showTags'
             dense
-            :label='$t(`editor.props.showTags`)'
+            :label='t(`editor.props.showTags`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
-    q-card-section.alt-card.q-pb-lg(ref='card-social')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-comments', size='xs')] {{$t('editor.props.social')}}
+    q-card-section.alt-card.q-pb-lg(id='refCardSocial')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-comments', size='xs')] {{t('editor.props.social')}}
       q-form.q-gutter-md.q-pt-sm
         div
           q-toggle(
-            v-model='allowComments'
+            v-model='pageStore.allowComments'
             dense
-            :label='$t(`editor.props.allowComments`)'
+            :label='t(`editor.props.allowComments`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
         div
           q-toggle(
-            v-model='allowContributions'
+            v-model='pageStore.allowContributions'
             dense
-            :label='$t(`editor.props.allowContributions`)'
+            :label='t(`editor.props.allowContributions`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
         div
           q-toggle(
-            v-model='allowRatings'
+            v-model='pageStore.allowRatings'
             dense
-            :label='$t(`editor.props.allowRatings`)'
+            :label='t(`editor.props.allowRatings`)'
             color='primary'
             checked-icon='las la-check'
             unchecked-icon='las la-times'
           )
-    q-card-section.q-pb-lg(ref='card-tags')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-tags', size='xs')] {{$t('editor.props.tags')}}
+    q-card-section.q-pb-lg(id='refCardTags')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-tags', size='xs')] {{t('editor.props.tags')}}
       page-tags(edit)
-    q-card-section.alt-card.q-pb-lg(ref='card-visibility')
-      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-eye', size='xs')] {{$t('editor.props.visibility')}}
+    q-card-section.alt-card.q-pb-lg(id='refCardVisibility')
+      .text-overline.items-center.flex #[q-icon.q-mr-sm(name='las la-eye', size='xs')] {{t('editor.props.visibility')}}
       q-form.q-gutter-md.q-pt-sm
         div
           q-toggle(
-            v-model='showInTree'
+            v-model='pageStore.showInTree'
             dense
             :label='$t(`editor.props.showInTree`)'
             color='primary'
@@ -239,7 +239,7 @@ q-card.page-properties-dialog
           )
         div
           q-toggle(
-            v-model='requirePassword'
+            v-model='state.requirePassword'
             dense
             :label='$t(`editor.props.requirePassword`)'
             color='primary'
@@ -247,119 +247,118 @@ q-card.page-properties-dialog
             unchecked-icon='las la-times'
           )
         div(
-          v-if='requirePassword'
+          v-if='state.requirePassword'
           style='padding-left: 40px;'
           )
           q-input(
             ref='iptPagePassword'
-            v-model='password'
-            :label='$t(`editor.props.password`)'
-            :hint='$t(`editor.props.passwordHint`)'
+            v-model='state.password'
+            :label='t(`editor.props.password`)'
+            :hint='t(`editor.props.passwordHint`)'
             outlined
             dense
           )
   q-dialog(
-    v-model='showRelationDialog'
+    v-model='state.showRelationDialog'
     )
-    page-relation-dialog(:edit-id='editRelationId')
+    page-relation-dialog(:edit-id='state.editRelationId')
 
   q-dialog(
-    v-model='showScriptsDialog'
+    v-model='state.showScriptsDialog'
     )
-    page-scripts-dialog(:mode='pageScriptsMode')
+    page-scripts-dialog(:mode='state.pageScriptsMode')
 </template>
 
-<script>
-import { get, sync } from 'vuex-pathify'
+<script setup>
+import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+import { nextTick, onMounted, reactive, ref, watch } from 'vue'
+
 import PageRelationDialog from './PageRelationDialog.vue'
 import PageScriptsDialog from './PageScriptsDialog.vue'
 import PageTags from './PageTags.vue'
 
-export default {
-  components: {
-    PageRelationDialog,
-    PageScriptsDialog,
-    PageTags
-  },
-  data () {
-    return {
-      showRelationDialog: false,
-      showScriptsDialog: false,
-      publishingRange: {},
-      requirePassword: false,
-      password: '',
-      editRelationId: null,
-      pageScriptsMode: 'jsLoad',
-      showQuickAccess: true
-    }
-  },
-  computed: {
-    title: sync('page/title', false),
-    description: sync('page/description', false),
-    showInTree: sync('page/showInTree', false),
-    isPublished: sync('page/isPublished', false),
-    relations: sync('page/relations', false),
-    showSidebar: sync('page/showSidebar', false),
-    showToc: sync('page/showToc', false),
-    showTags: sync('page/showTags', false),
-    tocDepth: sync('page/tocDepth', false),
-    allowComments: sync('page/allowComments', false),
-    allowContributions: sync('page/allowContributions', false),
-    allowRatings: sync('page/allowRatings', false),
-    thumbStyle: get('site/thumbStyle', false),
-    barStyle: get('site/barStyle', false),
-    quickaccess () {
-      return [
-        { key: 'info', icon: 'las la-info-circle', label: this.$t('editor.props.info') },
-        { key: 'publishstate', icon: 'las la-power-off', label: this.$t('editor.props.publishState') },
-        { key: 'relations', icon: 'las la-sun', label: this.$t('editor.props.relations') },
-        { key: 'scripts', icon: 'las la-code', label: this.$t('editor.props.scripts') },
-        { key: 'sidebar', icon: 'las la-ruler-vertical', label: this.$t('editor.props.sidebar') },
-        { key: 'social', icon: 'las la-comments', label: this.$t('editor.props.social') },
-        { key: 'tags', icon: 'las la-tags', label: this.$t('editor.props.tags') },
-        { key: 'visibility', icon: 'las la-eye', label: this.$t('editor.props.visibility') }
-      ]
-    }
-  },
-  watch: {
-    requirePassword (newValue) {
-      if (newValue) {
-        this.$nextTick(() => {
-          this.$refs.iptPagePassword.focus()
-          this.$refs.iptPagePassword.$el.scrollIntoView({
-            behavior: 'smooth'
-          })
-        })
-      }
-    }
-  },
-  mounted () {
-    setTimeout(() => {
-      this.showQuickAccess = true
-    }, 300)
-  },
-  methods: {
-    editScripts (mode) {
-      this.pageScriptsMode = mode
-      this.showScriptsDialog = true
-    },
-    newRelation () {
-      this.editRelationId = null
-      this.showRelationDialog = true
-    },
-    editRelation (rel) {
-      this.editRelationId = rel.id
-      this.showRelationDialog = true
-    },
-    removeRelation (rel) {
-      this.relations = this.$store.get('page/relations').filter(r => r.id !== rel.id)
-    },
-    jumpToSection (id) {
-      this.$refs[`card-${id}`].$el.scrollIntoView({
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  showRelationDialog: false,
+  showScriptsDialog: false,
+  publishingRange: {},
+  requirePassword: false,
+  password: '',
+  editRelationId: null,
+  pageScriptsMode: 'jsLoad',
+  showQuickAccess: true
+})
+
+const quickaccess = [
+  { key: 'refCardInfo', icon: 'las la-info-circle', label: t('editor.props.info') },
+  { key: 'refCardPublishState', icon: 'las la-power-off', label: t('editor.props.publishState') },
+  { key: 'refCardRelations', icon: 'las la-sun', label: t('editor.props.relations') },
+  { key: 'refCardScripts', icon: 'las la-code', label: t('editor.props.scripts') },
+  { key: 'refCardSidebar', icon: 'las la-ruler-vertical', label: t('editor.props.sidebar') },
+  { key: 'refCardSocial', icon: 'las la-comments', label: t('editor.props.social') },
+  { key: 'refCardTags', icon: 'las la-tags', label: t('editor.props.tags') },
+  { key: 'refCardVisibility', icon: 'las la-eye', label: t('editor.props.visibility') }
+]
+
+// WATCHERS
+
+watch(() => state.requirePassword, (newValue) => {
+  if (newValue) {
+    nextTick(() => {
+      this.$refs.iptPagePassword.focus()
+      this.$refs.iptPagePassword.$el.scrollIntoView({
         behavior: 'smooth'
       })
-      // this.$refs.scrollArea.setScrollPosition(offset, 600)
-    }
+    })
   }
+})
+
+// METHODS
+
+function editScripts (mode) {
+  state.pageScriptsMode = mode
+  state.showScriptsDialog = true
+}
+function newRelation () {
+  state.editRelationId = null
+  state.showRelationDialog = true
+}
+function editRelation (rel) {
+  state.editRelationId = rel.id
+  state.showRelationDialog = true
 }
+function removeRelation (rel) {
+  pageStore.relations = pageStore.relations.filter(r => r.id !== rel.id)
+}
+function jumpToSection (id) {
+  document.querySelector(`#${id}`).scrollIntoView({
+    behavior: 'smooth'
+  })
+}
+
+// MOUNTED
+
+onMounted(() => {
+  setTimeout(() => {
+    state.showQuickAccess = true
+  }, 300)
+})
+
 </script>

+ 143 - 115
ux/src/components/PageRelationDialog.vue

@@ -1,88 +1,88 @@
 <template lang="pug">
 q-card.page-relation-dialog(style='width: 500px;')
   q-toolbar.bg-primary.text-white
-    .text-subtitle2(v-if='isEditMode') {{$t('editor.pageRel.titleEdit')}}
-    .text-subtitle2(v-else) {{$t('editor.pageRel.title')}}
+    .text-subtitle2(v-if='isEditMode') {{t('editor.pageRel.titleEdit')}}
+    .text-subtitle2(v-else) {{t('editor.pageRel.title')}}
   q-card-section
-    .text-overline {{$t('editor.pageRel.position')}}
+    .text-overline {{t('editor.pageRel.position')}}
     q-form.q-gutter-md.q-pt-md
       div
         q-btn-toggle(
-          v-model='pos'
+          v-model='state.pos'
           push
           glossy
           no-caps
           toggle-color='primary'
           :options=`[
-            { label: $t('editor.pageRel.left'), value: 'left' },
-            { label: $t('editor.pageRel.center'), value: 'center' },
-            { label: $t('editor.pageRel.right'), value: 'right' }
+            { label: t('editor.pageRel.left'), value: 'left' },
+            { label: t('editor.pageRel.center'), value: 'center' },
+            { label: t('editor.pageRel.right'), value: 'right' }
           ]`
         )
-      .text-overline {{$t('editor.pageRel.button')}}
+      .text-overline {{t('editor.pageRel.button')}}
       q-input(
         ref='iptRelLabel'
         outlined
         dense
-        :label='$t(`editor.pageRel.label`)'
-        v-model='label'
+        :label='t(`editor.pageRel.label`)'
+        v-model='state.label'
         )
-      template(v-if='pos !== `center`')
+      template(v-if='state.pos !== `center`')
         q-input(
           outlined
           dense
-          :label='$t(`editor.pageRel.caption`)'
-          v-model='caption'
+          :label='t(`editor.pageRel.caption`)'
+          v-model='state.caption'
           )
       q-btn.rounded-borders(
-        :label='$t(`editor.pageRel.selectIcon`)'
+        :label='t(`editor.pageRel.selectIcon`)'
         color='primary'
         outline
         )
         q-menu(content-class='shadow-7')
-          icon-picker-dialog(v-model='icon')
-      .text-overline {{$t('editor.pageRel.target')}}
+          icon-picker-dialog(v-model='state.icon')
+      .text-overline {{t('editor.pageRel.target')}}
       q-btn.rounded-borders(
-        :label='$t(`editor.pageRel.selectPage`)'
+        :label='t(`editor.pageRel.selectPage`)'
         color='primary'
         outline
         )
-      .text-overline {{$t('editor.pageRel.preview')}}
+      .text-overline {{t('editor.pageRel.preview')}}
       q-btn(
-        v-if='pos === `left`'
+        v-if='state.pos === `left`'
         padding='sm md'
         outline
-        :icon='icon'
+        :icon='state.icon'
         no-caps
         color='primary'
         )
         .column.text-left.q-pl-md
-          .text-body2: strong {{label}}
-          .text-caption {{caption}}
+          .text-body2: strong {{state.label}}
+          .text-caption {{state.caption}}
       q-btn.full-width(
-        v-else-if='pos === `center`'
-        :label='label'
+        v-else-if='state.pos === `center`'
+        :label='state.label'
         color='primary'
         flat
         no-caps
-        :icon='icon'
+        :icon='state.icon'
       )
       q-btn(
-        v-else-if='pos === `right`'
+        v-else-if='state.pos === `right`'
         padding='sm md'
         outline
-        :icon-right='icon'
+        :icon-right='state.icon'
         no-caps
         color='primary'
         )
         .column.text-left.q-pr-md
-          .text-body2: strong {{label}}
-          .text-caption {{caption}}
+          .text-body2: strong {{state.label}}
+          .text-caption {{state.caption}}
   q-card-actions.card-actions
     q-space
     q-btn.acrylic-btn(
       icon='las la-times'
-      :label='$t(`common.actions.discard`)'
+      :label='t(`common.actions.discard`)'
       color='grey-7'
       padding='xs md'
       v-close-popup
@@ -92,7 +92,7 @@ q-card.page-relation-dialog(style='width: 500px;')
       v-if='isEditMode'
       :disabled='!canSubmit'
       icon='las la-check'
-      :label='$t(`common.actions.save`)'
+      :label='t(`common.actions.save`)'
       unelevated
       color='primary'
       padding='xs md'
@@ -103,7 +103,7 @@ q-card.page-relation-dialog(style='width: 500px;')
       v-else
       :disabled='!canSubmit'
       icon='las la-plus'
-      :label='$t(`common.actions.create`)'
+      :label='t(`common.actions.create`)'
       unelevated
       color='primary'
       padding='xs md'
@@ -112,99 +112,127 @@ q-card.page-relation-dialog(style='width: 500px;')
     )
 </template>
 
-<script>
+<script setup>
 import { v4 as uuid } from 'uuid'
 import { cloneDeep, find } from 'lodash-es'
+import { useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
 
 import IconPickerDialog from './IconPickerDialog.vue'
 
-export default {
-  components: {
-    IconPickerDialog
-  },
-  props: {
-    editId: {
-      type: String,
-      default: null
-    }
-  },
-  data () {
-    return {
-      pos: 'left',
-      label: '',
-      caption: '',
-      icon: 'las la-arrow-left',
-      target: ''
-    }
-  },
-  computed: {
-    canSubmit () {
-      return this.label.length > 0
-    },
-    isEditMode () {
-      return Boolean(this.editId)
+import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
+
+// PROPS
+
+const props = defineProps({
+  editId: {
+    type: String,
+    default: null
+  }
+})
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  pos: 'left',
+  label: '',
+  caption: '',
+  icon: 'las la-arrow-left',
+  target: ''
+})
+
+// REFS
+
+const iptRelLabel = ref(null)
+
+// COMPUTED
+
+const canSubmit = computed(() => state.label.length > 0)
+const isEditMode = computed(() => Boolean(props.editId))
+
+// WATCHERS
+
+watch(() => state.pos, (newValue) => {
+  switch (newValue) {
+    case 'left': {
+      state.icon = 'las la-arrow-left'
+      break
     }
-  },
-  watch: {
-    pos (newValue) {
-      switch (newValue) {
-        case 'left': {
-          this.icon = 'las la-arrow-left'
-          break
-        }
-        case 'center': {
-          this.icon = 'las la-book'
-          break
-        }
-        case 'right': {
-          this.icon = 'las la-arrow-right'
-          break
-        }
-      }
+    case 'center': {
+      state.icon = 'las la-book'
+      break
     }
-  },
-  mounted () {
-    if (this.editId) {
-      const rel = find(this.$store.get('page/relations'), ['id', this.editId])
-      if (rel) {
-        this.pos = rel.position
-        this.label = rel.label
-        this.caption = rel.caption || ''
-        this.icon = rel.icon
-        this.target = rel.target
-      }
+    case 'right': {
+      state.icon = 'las la-arrow-right'
+      break
     }
-    this.$nextTick(() => {
-      this.$refs.iptRelLabel.focus()
-    })
-  },
-  methods: {
-    create () {
-      this.$store.set('page/relations', [
-        ...this.$store.get('page/relations'),
-        {
-          id: uuid(),
-          position: this.pos,
-          label: this.label,
-          ...(this.pos !== 'center' ? { caption: this.caption } : {}),
-          icon: this.icon,
-          target: this.target
-        }
-      ])
-    },
-    persist () {
-      const rels = cloneDeep(this.$store.get('page/relations'))
-      for (const rel of rels) {
-        if (rel.id === this.editId) {
-          rel.position = this.pos
-          rel.label = this.label
-          rel.caption = this.caption
-          rel.icon = this.icon
-          rel.target = this.target
-        }
+  }
+})
+
+// METHODS
+
+function create () {
+  pageStore.$patch({
+    relations: [
+      ...pageStore.relations,
+      {
+        id: uuid(),
+        position: state.pos,
+        label: state.label,
+        ...(state.pos !== 'center' ? { caption: state.caption } : {}),
+        icon: state.icon,
+        target: state.target
       }
-      this.$store.set('page/relations', rels)
+    ]
+  })
+}
+
+function persist () {
+  const rels = cloneDeep(pageStore.relations)
+  for (const rel of rels) {
+    if (rel.id === state.editId) {
+      rel.position = state.pos
+      rel.label = state.label
+      rel.caption = state.caption
+      rel.icon = state.icon
+      rel.target = state.target
     }
   }
+  pageStore.$patch({
+    relations: rels
+  })
 }
+
+// MOUNTED
+
+onMounted(() => {
+  if (props.editId) {
+    const rel = find(pageStore.relations, ['id', props.editId])
+    if (rel) {
+      state.pos = rel.position
+      state.label = rel.label
+      state.caption = rel.caption || ''
+      state.icon = rel.icon
+      state.target = rel.target
+    }
+  }
+  nextTick(() => {
+    iptRelLabel.value.focus()
+  })
+})
 </script>

+ 3 - 1
ux/src/css/app.scss

@@ -222,7 +222,9 @@ body::-webkit-scrollbar-thumb {
 
 @import './animation.scss';
 
-@import 'v-network-graph/lib/style.css'
+@import 'v-network-graph/lib/style.css';
+
+@import './page-contents.scss';
 
 // @import '~codemirror/lib/codemirror.css';
 // @import '~codemirror/theme/elegant.css';

+ 62 - 0
ux/src/css/page-contents.scss

@@ -0,0 +1,62 @@
+.page-contents {
+  color: #424242;
+  font-size: 14px;
+
+  // ---------------------------------
+  // HEADERS
+  // ---------------------------------
+
+  h1, h2, h3, h4, h5, h6 {
+    padding: 0;
+    margin: 0;
+    position: relative;
+    line-height: normal;
+
+    &:first-child {
+      padding-top: 0;
+    }
+
+    &:hover {
+      .toc-anchor {
+        display: block;
+      }
+    }
+  }
+
+  * + h1, * + h2, * + h3 {
+    border-top: 1px solid #DDD;
+  }
+
+  h1 {
+    font-size: 3em;
+    font-weight: 500;
+    padding: 12px 0;
+  }
+  h2 {
+    font-size: 2.4em;
+    padding: 12px 0;
+  }
+  h3 {
+    font-size: 2em;
+    padding: 12px 0;
+  }
+  h4 {
+    font-size: 1.75em;
+  }
+  h5 {
+    font-size: 1.5em;
+  }
+  h6 {
+    font-size: 1.25em;
+  }
+
+  .toc-anchor {
+    display: none;
+    position: absolute;
+    right: 1rem;
+    bottom: .5rem;
+    font-size: 1.25rem;
+    text-decoration: none;
+    color: #666;
+  }
+}

+ 37 - 17
ux/src/pages/Index.vue

@@ -18,13 +18,13 @@ q-page.column
           :icon='brd.icon'
           :label='brd.title'
           :aria-label='brd.title'
-          :to='getFullPath(brd)'
+          :to='brd.path'
           )
     .col-auto.flex.items-center.justify-end
       template(v-if='!pageStore.isPublished')
         .text-caption.text-accent: strong Unpublished
         q-separator.q-mx-sm(vertical)
-      .text-caption.text-grey-6 Last modified on #[strong September 5th, 2020]
+      .text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
   .page-header.row
     //- PAGE ICON
     .col-auto.q-pl-md.flex.items-center
@@ -90,7 +90,7 @@ q-page.column
         style='height: 100%;'
         )
         .q-pa-md
-          div(v-html='pageStore.render')
+          .page-contents(v-html='pageStore.render')
           template(v-if='pageStore.relations && pageStore.relations.length > 0')
             q-separator.q-my-lg
             .row.align-center
@@ -288,14 +288,14 @@ q-page.column
     transition-hide='jump-right'
     class='floating-sidepanel'
     )
-    component(:is='state.sideDialogComponent')
+    component(:is='sideDialogs[state.sideDialogComponent]')
 
   q-dialog(
     v-model='state.showGlobalDialog'
     transition-show='jump-up'
     transition-hide='jump-down'
     )
-    component(:is='state.globalDialogComponent')
+    component(:is='globalDialogs[state.globalDialogComponent]')
 </template>
 
 <script setup>
@@ -303,17 +303,23 @@ import { useMeta, useQuasar, setCssVar } from 'quasar'
 import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
+import { DateTime } from 'luxon'
 
 import { usePageStore } from 'src/stores/page'
-import { useSiteStore } from '../stores/site'
+import { useSiteStore } from 'src/stores/site'
 
 // COMPONENTS
 
 import SocialSharingMenu from '../components/SocialSharingMenu.vue'
-import PageDataDialog from '../components/PageDataDialog.vue'
 import PageTags from '../components/PageTags.vue'
-import PagePropertiesDialog from '../components/PagePropertiesDialog.vue'
-import PageSaveDialog from '../components/PageSaveDialog.vue'
+
+const sideDialogs = {
+  PageDataDialog: defineAsyncComponent(() => import('../components/PageDataDialog.vue')),
+  PagePropertiesDialog: defineAsyncComponent(() => import('../components/PagePropertiesDialog.vue'))
+}
+const globalDialogs = {
+  PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
+}
 
 // QUASAR
 
@@ -441,22 +447,36 @@ const editUrl = computed(() => {
   pagePath += !pageStore.path ? 'home' : pageStore.path
   return `/_edit/${pagePath}`
 })
+const lastModified = computed(() => {
+  return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
+})
 
 // WATCHERS
 
+watch(() => route.path, async (newValue) => {
+  if (newValue.startsWith('/_')) { return }
+  try {
+    await pageStore.pageLoad({ path: newValue })
+  } catch (err) {
+    if (err.message === 'ERR_PAGE_NOT_FOUND') {
+      $q.notify({
+        type: 'negative',
+        message: 'This page does not exist (yet)!'
+      })
+    } else {
+      $q.notify({
+        type: 'negative',
+        message: err.message
+      })
+    }
+  }
+}, { immediate: true })
+
 watch(() => state.toc, refreshTocExpanded)
 watch(() => pageStore.tocDepth, refreshTocExpanded)
 
 // METHODS
 
-function getFullPath ({ locale, path }) {
-  if (siteStore.useLocales) {
-    return `/${locale}/${path}`
-  } else {
-    return `/${path}`
-  }
-}
-
 function togglePageProperties () {
   state.sideDialogComponent = 'PagePropertiesDialog'
   state.showSideDialog = true

+ 76 - 20
ux/src/stores/page.js

@@ -1,24 +1,29 @@
 import { defineStore } from 'pinia'
+import gql from 'graphql-tag'
+import { cloneDeep, last, transform } from 'lodash-es'
+
+import { useSiteStore } from './site'
 
 export const usePageStore = defineStore('page', {
   state: () => ({
+    isLoading: true,
     mode: 'view',
     editor: 'wysiwyg',
     editorMode: 'edit',
     id: 0,
     authorId: 0,
-    authorName: 'Unknown',
+    authorName: '',
     createdAt: '',
-    description: 'How to install Wiki.js on Ubuntu 18.04 / 20.04',
+    description: '',
     isPublished: true,
     showInTree: true,
     locale: 'en',
     path: '',
     publishEndDate: '',
     publishStartDate: '',
-    tags: ['cities', 'canada'],
-    title: 'Ubuntu',
-    icon: 'lab la-empire',
+    tags: [],
+    title: '',
+    icon: 'las la-file-alt',
     updatedAt: '',
     relations: [],
     scriptJsLoad: '',
@@ -35,20 +40,20 @@ export const usePageStore = defineStore('page', {
       max: 2
     },
     breadcrumbs: [
-      {
-        id: 1,
-        title: 'Installation',
-        icon: 'las la-file-alt',
-        locale: 'en',
-        path: 'installation'
-      },
-      {
-        id: 2,
-        title: 'Ubuntu',
-        icon: 'lab la-ubuntu',
-        locale: 'en',
-        path: 'installation/ubuntu'
-      }
+      // {
+      //   id: 1,
+      //   title: 'Installation',
+      //   icon: 'las la-file-alt',
+      //   locale: 'en',
+      //   path: 'installation'
+      // },
+      // {
+      //   id: 2,
+      //   title: 'Ubuntu',
+      //   icon: 'lab la-ubuntu',
+      //   locale: 'en',
+      //   path: 'installation/ubuntu'
+      // }
     ],
     effectivePermissions: {
       comments: {
@@ -75,10 +80,61 @@ export const usePageStore = defineStore('page', {
     },
     commentsCount: 0,
     content: '',
-    render: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
+    render: ''
   }),
   getters: {},
   actions: {
+    /**
+     * PAGE - LOAD
+     */
+    async pageLoad ({ path, id }) {
+      const siteStore = useSiteStore()
+      try {
+        const resp = await APOLLO_CLIENT.query({
+          query: gql`
+            query loadPage (
+              $path: String!
+            ) {
+              pageByPath(
+                path: $path
+              ) {
+                id
+                title
+                description
+                path
+                locale
+                updatedAt
+                render
+              }
+            }
+          `,
+          variables: {
+            path
+          },
+          fetchPolicy: 'network-only'
+        })
+        const pageData = cloneDeep(resp?.data?.pageByPath ?? {})
+        if (!pageData?.id) {
+          throw new Error('ERR_PAGE_NOT_FOUND')
+        }
+        const pathPrefix = siteStore.useLocales ? `/${pageData.locale}` : ''
+        this.$patch({
+          ...pageData,
+          breadcrumbs: transform(pageData.path.split('/'), (result, value, key) => {
+            result.push({
+              id: key,
+              title: value,
+              icon: 'las la-file-alt',
+              locale: 'en',
+              path: (last(result)?.path || pathPrefix) + `/${value}`
+            })
+          }, [])
+        })
+      } catch (err) {
+        console.warn(err)
+        throw err
+      }
+    },
     /**
      * PAGE - CREATE
      */

+ 4 - 54
ux/yarn.lock

@@ -5899,7 +5899,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss@npm:^8.1.10, postcss@npm:^8.4.12, postcss@npm:^8.4.4":
+"postcss@npm:^8.1.10, postcss@npm:^8.4.4":
   version: 8.4.12
   resolution: "postcss@npm:8.4.12"
   dependencies:
@@ -6240,13 +6240,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"quasar@npm:*":
-  version: 2.6.6
-  resolution: "quasar@npm:2.6.6"
-  checksum: 0ef62a7e916f88cea06d3e5320cec9a0eddebd829891b7bc0ee11a4ec47653b1ae08d414241c046a8c65657e9f12cf2ead4fc214f6dc43d57816535f5c216160
-  languageName: node
-  linkType: hard
-
 "quasar@npm:2.7.7":
   version: 2.7.7
   resolution: "quasar@npm:2.7.7"
@@ -6460,8 +6453,8 @@ __metadata:
   linkType: hard
 
 "rollup@npm:*":
-  version: 2.71.1
-  resolution: "rollup@npm:2.71.1"
+  version: 3.2.3
+  resolution: "rollup@npm:3.2.3"
   dependencies:
     fsevents: ~2.3.2
   dependenciesMeta:
@@ -6469,7 +6462,7 @@ __metadata:
       optional: true
   bin:
     rollup: dist/bin/rollup
-  checksum: fe2b2fda7bf53c86e970f3b026b784c00e2237089b802755b3e43725db88f5d1869c1f81f8c5257e9b68b0fd1840dcbd3897d2f19768cce97a37c70e1a563dce
+  checksum: e4b4f3b70fad4b8f7dabc579fb8bbe14399d5aed0cc1fcee39f15ae81804d6acd0e1063b653e6cf5ef50e8e954801689e2c822e99ed31ca18f1b1fbbea8075e5
   languageName: node
   linkType: hard
 
@@ -6487,20 +6480,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"rollup@npm:^2.59.0":
-  version: 2.70.1
-  resolution: "rollup@npm:2.70.1"
-  dependencies:
-    fsevents: ~2.3.2
-  dependenciesMeta:
-    fsevents:
-      optional: true
-  bin:
-    rollup: dist/bin/rollup
-  checksum: 06c62933e6e81a1c8c684d7d576e507081aabdb63cc0c91bca86b7348b66df03b77827068e4990b8b6c738bd3ef66dcc8c7ed7e0ea40b736068e7618f693133e
-  languageName: node
-  linkType: hard
-
 "rope-sequence@npm:^1.3.0":
   version: 1.3.2
   resolution: "rope-sequence@npm:1.3.2"
@@ -7326,35 +7305,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"vite@npm:*":
-  version: 2.9.6
-  resolution: "vite@npm:2.9.6"
-  dependencies:
-    esbuild: ^0.14.27
-    fsevents: ~2.3.2
-    postcss: ^8.4.12
-    resolve: ^1.22.0
-    rollup: ^2.59.0
-  peerDependencies:
-    less: "*"
-    sass: "*"
-    stylus: "*"
-  dependenciesMeta:
-    fsevents:
-      optional: true
-  peerDependenciesMeta:
-    less:
-      optional: true
-    sass:
-      optional: true
-    stylus:
-      optional: true
-  bin:
-    vite: bin/vite.js
-  checksum: 79bbf516547f4adb1a297ac8648f8818b3e2a7bb113a7e12acc2355498d247556f58fc50d39eec4891c07250e7420e72773e358eeb8dc62af62fc1ff32c70877
-  languageName: node
-  linkType: hard
-
 "vite@npm:^2.9.13":
   version: 2.9.15
   resolution: "vite@npm:2.9.15"