Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F12741197
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/apps/orbeon-forms/composer.json b/apps/orbeon-forms/composer.json
new file mode 100644
index 0000000..1f3e8b6
--- /dev/null
+++ b/apps/orbeon-forms/composer.json
@@ -0,0 +1,32 @@
+{
+ "name": "waystone/orbeon-forms",
+ "description": "Read and allows to annotate forms filled with Orbeon",
+ "keywords": [
+ "orbeon",
+ "waystone",
+ "obsidian"
+ ],
+ "minimum-stability": "stable",
+ "license": "BSD-2-Clause",
+ "authors": [
+ {
+ "name": "Sébastien Santoro",
+ "email": "dereckson@espace-win.org"
+ }
+ ],
+ "require-dev": {
+ "nasqueron/codestyle": "^0.1.2",
+ "phpunit/phpunit": "^12.5",
+ "squizlabs/php_codesniffer": "^4.0"
+ },
+ "require": {
+ "keruald/database": "0.6.1",
+ "waystone/workspaces": "^1.0.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Waystone\\Apps\\OrbeonForms\\": "src/",
+ "Waystone\\Apps\\OrbeonForms\\Tests\\": "tests/"
+ }
+ }
+}
diff --git a/apps/orbeon-forms/src/Forms/Entry.php b/apps/orbeon-forms/src/Forms/Entry.php
new file mode 100644
index 0000000..b274ee3
--- /dev/null
+++ b/apps/orbeon-forms/src/Forms/Entry.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Waystone\Apps\OrbeonForms\Forms;
+
+use Waystone\Workspaces\Engines\Exceptions\DocumentNotFoundException;
+use Waystone\Workspaces\Engines\Exceptions\WorkspaceException;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\OmniTools\Collections\HashMap;
+use Keruald\OmniTools\Collections\Vector;
+
+use DateTime;
+
+class Entry {
+
+ ///
+ /// Members
+ ///
+
+ private Form $form;
+
+ private string $document_id;
+
+ private HashMap $content;
+
+ private DateTime $created;
+
+ private PDOEngine $db;
+
+ ///
+ /// Constructors
+ ///
+
+ public function __construct (PDOEngine $db, Form $form, string $document_id) {
+ $this->form = $form;
+ $this->document_id = $document_id;
+ $this->db = $db;
+
+ $this->loadFromDatabase();
+ }
+
+ ///
+ /// Getters
+ ///
+
+ public function getContent () : HashMap {
+ return $this->content;
+ }
+
+ public function getDate () : string {
+ return $this->created->format("Y-m-d");
+ }
+
+ ///
+ /// Helper methods
+ ///
+
+ /**
+ * Guess the title of the entry, based on the first field.
+ * If specific fields are declared for the index, the first of those is used.
+ *
+ * @return string
+ */
+ public function guessTitle () : string {
+ $field = $this->form
+ ->getIndexKeys()
+ ->firstOr($this->form->getRawFields()->first());
+
+ return $this->content[$field] ?? "(untitled entry)";
+ }
+
+ public function countAttachments () : int {
+ $this->validate();
+
+ $sql = "SELECT count(*)
+ FROM orbeon_form_data_attach
+ WHERE document_id = :document_id";
+
+ return $this->db
+ ->prepare($sql)
+ ->with("document_id", $this->document_id)
+ ->query()
+ ->fetchScalar();
+ }
+
+ public function hasAttachments () : bool {
+ return $this->countAttachments() > 0;
+ }
+
+ ///
+ /// Load from database
+ ///
+
+ private function loadFromDatabase () : void {
+ $sql = $this->buildSelectQuery();
+
+ $result = $this->db
+ ->prepare($sql)
+ ->with("document_id", $this->document_id)
+ ->query();
+
+ if (!$result || $result->numRows() === 0) {
+ throw new DocumentNotFoundException(
+ "Document ID not found: $this->document_id"
+ );
+ }
+
+ $row = $result->fetchRow();
+ $this->created = new DateTime($row["metadata_created"]);
+ $this->content = $this->form->getFields()
+ ->mapValuesAndKeys(function ($fieldColumn, $fieldName) use ($row) {
+ return [$fieldName, $row[$fieldColumn]];
+ });
+ }
+
+ ///
+ /// SQL helper methods
+ ///
+
+ private function buildSelectQuery () : string {
+ $sql = "SELECT ";
+ $sql .= $this->getQueryFields()->implode(", ");
+ $sql .= " FROM ";
+ $sql .= $this->form->getView();
+ $sql .= " WHERE metadata_document_id = :document_id";
+
+ return $sql;
+ }
+
+ private function getQueryFields () : Vector {
+ return $this->form->getRawFields()
+ ->push("metadata_document_id")
+ ->push("metadata_created");
+ }
+
+ ///
+ /// Validate configuration syntax
+ ///
+
+ public function validate () : void {
+ $this->form->validate();
+
+ if (!ctype_xdigit($this->document_id)) {
+ throw new WorkspaceException(
+ "Invalid document ID: $this->document_id"
+ );
+ }
+ }
+
+}
diff --git a/apps/orbeon-forms/src/Forms/Form.php b/apps/orbeon-forms/src/Forms/Form.php
new file mode 100644
index 0000000..742e6e2
--- /dev/null
+++ b/apps/orbeon-forms/src/Forms/Form.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Waystone\Apps\OrbeonForms\Forms;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\OmniTools\Collections\HashMap;
+use Keruald\OmniTools\Collections\Vector;
+use Waystone\Workspaces\Engines\Exceptions\WorkspaceException;
+
+class Form {
+
+ ///
+ /// Private members
+ ///
+
+ private string $name;
+
+ private string $slug;
+
+ private string $view;
+
+ private string $orbeon_base_url;
+
+ private array $fields;
+
+ /**
+ * @var string[]
+ */
+ private array $index;
+
+ private PDOEngine $db;
+
+ ///
+ /// Constructor
+ ///
+
+ public function __construct (PDOEngine $db, array $config) {
+ $this->db = $db;
+
+ $this->name = $config["name"];
+ $this->slug = $config["slug"];
+ $this->view = $config["view"];
+ $this->orbeon_base_url = $config["orbeon_base_url"];
+ $this->fields = $config["fields"];
+ $this->index = $config["index"] ?? [];
+
+ $this->validate();
+ }
+
+ ///
+ /// Getters
+ ///
+
+ public function getName () : string {
+ return $this->name;
+ }
+
+ public function getSlug () : string {
+ return $this->slug;
+ }
+
+ public function getView () : string {
+ return $this->view;
+ }
+
+ public function getOrbeonBaseUrl () : string {
+ return $this->orbeon_base_url;
+ }
+
+ public function getRawFields () : Vector {
+ return Vector::from(array_keys($this->fields));
+ }
+
+ public function getFields () : HashMap {
+ return HashMap::from($this->fields);
+ }
+
+ /**
+ * @return Vector<string>
+ */
+ public function getIndexKeys () : Vector {
+ return Vector::from($this->index)
+ ->map(fn ($key) => $this->fields[$key]);
+ }
+
+ ///
+ /// Interact with the form entries
+ ///
+
+ /**
+ * @return iterable<array<string, array<string, string>>>
+ */
+ public function getAllEntries () : iterable {
+ $this->validate();
+
+ $sql = $this->buildSelectQuery();
+
+ $result = $this->db->query($sql);
+ foreach ($result as $row) {
+ yield $this->entryFromRow($row);
+ }
+
+ return [];
+ }
+
+ public function getEntry (string $documentId) : Entry {
+ return new Entry($this->db, $this, $documentId);
+ }
+
+ ///
+ /// Enrich SQL content
+ ///
+
+ private function entryFromRow (array $row) : array {
+ $entry = [
+ "data" => [],
+ "metadata" => [],
+ ];
+
+ foreach ($row as $key => $value) {
+ if (str_starts_with($key, "metadata_")) {
+ $key = substr($key, 9);
+ $entry["metadata"][$key] = $value;
+ } else {
+ $key = $this->fields[$key];
+ $entry["data"][$key] = $value;
+ }
+ }
+
+ return $entry;
+ }
+
+ ///
+ /// SQL helper methods
+ ///
+
+ private function buildSelectQuery () : string {
+ $sql = "SELECT ";
+ $sql .= $this->getQueryFields()->implode(", ");
+ $sql .= " FROM ";
+ $sql .= $this->view;
+ $sql .= " ORDER BY metadata_created DESC";
+
+ return $sql;
+ }
+
+ private function getIndexFields() : array {
+ if ($this->index) {
+ return $this->index;
+ }
+
+ // If omitted, use all fields
+ return array_keys($this->fields);
+ }
+
+ private function getQueryFields () : Vector {
+ $fields = $this->getIndexFields();
+ $fields[] = "metadata_document_id";
+ $fields[] = "metadata_created";
+
+ return Vector::from($fields);
+ }
+
+ ///
+ /// Validate configuration syntax
+ ///
+
+ const string RE_DB_ITEM_EXPRESSION = "/^[a-z0-9_]+$/";
+
+ public function validate () : void {
+ $fields = array_keys($this->fields);
+
+ foreach ($fields as $field) {
+ if (!$this->validateDatabaseItem($field)) {
+ throw new WorkspaceException("Invalid database item expression: $field");
+ }
+ }
+
+ foreach ($this->index as $field) {
+ if (!in_array($field, $fields)) {
+ throw new WorkspaceException("Invalid index field: $field (must be declared into fields too)");
+ }
+ }
+
+ if (!$this->validateDatabaseItem($this->view)) {
+ throw new WorkspaceException("Invalid view name: $this->view");
+ }
+ }
+
+ private function validateDatabaseItem ($expression) : bool {
+ // Expression can only contain [a-z0-9_]
+ return preg_match(self::RE_DB_ITEM_EXPRESSION, $expression);
+ }
+
+}
diff --git a/apps/orbeon-forms/src/Reader/Application.php b/apps/orbeon-forms/src/Reader/Application.php
new file mode 100644
index 0000000..eec54c0
--- /dev/null
+++ b/apps/orbeon-forms/src/Reader/Application.php
@@ -0,0 +1,231 @@
+<?php
+
+namespace Waystone\Apps\OrbeonForms\Reader;
+
+use Waystone\Apps\OrbeonForms\Forms\Form;
+use Waystone\Workspaces\Engines\Apps\Application as BaseApplication;
+use Waystone\Workspaces\Engines\Errors\ErrorHandling;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\OmniTools\Collections\Vector;
+use Keruald\OmniTools\DataTypes\Option\None;
+use Keruald\OmniTools\DataTypes\Option\Option;
+use Keruald\OmniTools\DataTypes\Option\Some;
+
+use ErrorPageController;
+use FooterController;
+use HeaderController;
+
+class Application extends BaseApplication {
+
+ private PDOEngine $db;
+
+ protected function onAfterInitialize () : void {
+ /** @var ApplicationConfiguration $config */
+ $config = $this->context->configuration;
+
+ $this->db = PDOEngine::initialize($config->orbeonDatabase);
+ }
+
+ ///
+ /// Controller methods
+ ///
+
+ /**
+ * Serves the application index page
+ * @param Vector<Form> $forms
+ */
+ private function index (Vector $forms) : void {
+ $count = $forms->count();
+
+ if ($count == 0) {
+ ErrorHandling::messageAndDie(GENERAL_ERROR, "No form set in workspace configuration");
+ }
+
+ if ($count == 1) {
+ $slug = $forms[0]["slug"];
+ $this->redirectTo($slug);
+ }
+
+ ///
+ /// View: List of forms
+ ///
+
+ $smarty = $this->context->templateEngine;
+
+ // List of forms, with their URL
+ $forms = $forms->map(function ($form) {
+ return [
+ "name" => $form["name"],
+ "url" => $this->buildUrl($form["slug"]),
+ ];
+ });
+
+ // Header
+ $smarty->assign("PAGE_TITLE", "Forms");
+ HeaderController::run($this->context);
+
+ // Body
+ $smarty->display("apps/_blocks/page_header.tpl");
+
+ $smarty->assign("items", $forms);
+ $smarty->display("apps/_blocks/menu_items.tpl");
+
+ // Footer
+ FooterController::run($this->context);
+ }
+
+ private function formIndex (array $formConfig) : void {
+ $form = new Form($this->db, $formConfig);
+ $entries = $form->getAllEntries();
+
+ ///
+ /// View: List of entries
+ ///
+
+ $smarty = $this->context->templateEngine;
+
+ $keys = $form->getIndexKeys();
+
+ $url_base = $form->getSlug();
+ $items = Vector::from($entries)
+ ->map(function ($entry) use ($url_base) {
+ return $entry["data"] + [
+ "url" => $this->buildUrl(
+ $url_base . "/" . $entry["metadata"]["document_id"]
+ ),
+ ];
+ });
+
+ // Header
+ $smarty->assign("PAGE_TITLE", $form->getName());
+ HeaderController::run($this->context);
+
+ // Body
+ $smarty->display("apps/_blocks/page_header.tpl");
+
+ $smarty->assign("keys", $keys);
+ $smarty->assign("items", $items);
+ $smarty->display("apps/_crud/list.tpl");
+
+ // Footer
+ FooterController::run($this->context);
+ }
+
+ private function formEntry (array $formConfig, string $document_id) {
+ $form = new Form($this->db, $formConfig);
+ $entry = $form->getEntry($document_id);
+ $title = $entry->guessTitle();
+
+ $content = $entry->getContent()
+ ->set("🗓️ Filing date", $entry->getDate());
+
+ ///
+ /// View: Form entry
+ ///
+
+ $smarty = $this->context->templateEngine;
+
+ // Header
+ $smarty->assign('PAGE_TITLE', $form->getName());
+ $smarty->assign('custom_css', "dd {white-space: pre-line;}");
+ HeaderController::run($this->context);
+
+ // Body
+ $smarty->display("apps/_blocks/page_header.tpl");
+
+ echo '<div class="row"><h2>', $title, '</h2></div>';
+ $smarty->assign('items', $content);
+ $smarty->display("apps/_blocks/dl.tpl");
+
+ if ($entry->hasAttachments()) {
+ $smarty->assign("alert_level", "info");
+ $smarty->assign("alert_note", "📎 This form has documents attached.");
+ $smarty->display("apps/_blocks/alert.tpl");
+ }
+
+ $view_url = $form->getOrbeonBaseUrl() . "/view/" . $document_id;
+ echo '<div class="row">';
+ echo '<p>➕ <a href="' . $view_url . '">View full form on Orbeon</a></p>';
+ echo '<p>↩ <a href="' . $this->buildUrl($form->getSlug()) . '">Back to list</a></p>';
+ echo '</div>';
+
+ // Footer
+ FooterController::run($this->context);
+ }
+
+ ///
+ /// Controller handler
+ ///
+
+ public function handleRequest () {
+ /** @var ApplicationConfiguration $config */
+ $config = $this->context->configuration;
+ $forms = Vector::from($config->forms);
+
+ $argc = count($this->context->url);
+
+ if ($argc == 1) {
+ $this->index($forms);
+ return;
+ }
+
+ $form = $this->get_form_config($this->context->url[1]);
+ if ($form->isNone()) {
+ // URL points to a non-existing form
+ ErrorPageController::show($this->context, 404);
+ exit;
+ }
+
+ if ($argc == 2) {
+ $this->formIndex($form->getValue());
+ exit;
+ }
+
+ if ($argc == 3) {
+ $document_id = $this->context->url[2];
+
+ if (ctype_xdigit($document_id)) {
+ // URL points to a form entry
+ $this->formEntry($form->getValue(), $this->context->url[2]);
+ exit;
+ }
+ }
+
+ // Unknown URL
+ ErrorPageController::show($this->context, 404);
+ exit;
+ }
+
+ private function buildUrl (string $slug) : string {
+ return "/" . Vector::from([
+ $this->context->workspace->code,
+ $this->context->configuration->bind,
+ $slug,
+ ])->implode("/");
+ }
+
+ private function redirectTo (mixed $slug) : never {
+ $url = $this->buildUrl($slug);
+
+ header("Location: $url");
+ exit;
+ }
+
+ /**
+ * @return Option<array<string, mixed>>
+ */
+ private function get_form_config (string $slug) : Option {
+ /** @var ApplicationConfiguration $config */
+ $config = $this->context->configuration;
+
+ foreach ($config->forms as $form) {
+ if ($form["slug"] == $slug) {
+ return new Some($form);
+ }
+ }
+
+ return new None;
+ }
+
+}
diff --git a/apps/orbeon-forms/src/Reader/ApplicationConfiguration.php b/apps/orbeon-forms/src/Reader/ApplicationConfiguration.php
new file mode 100644
index 0000000..3d37b55
--- /dev/null
+++ b/apps/orbeon-forms/src/Reader/ApplicationConfiguration.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Waystone\Apps\OrbeonForms\Reader;
+
+use Waystone\Workspaces\Engines\Apps\ApplicationConfiguration as BaseApplicationConfiguration;
+
+class ApplicationConfiguration extends BaseApplicationConfiguration {
+
+ /**
+ * @var array Configuration for Orbeon Forms
+ */
+ public array $orbeonDatabase = [];
+
+ /**
+ * @var array Configuration for Orbeon forms
+ */
+ public array $forms = [];
+
+}
diff --git a/composer.json b/composer.json
index 3f2b33e..8cf9fbc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,48 +1,51 @@
{
"name": "waystone/waystone",
"type": "library",
"description": "Modular libraries to build applications with Obsidian Workspaces",
"keywords": [
"framework",
"keruald",
"waystone",
"obsidian"
],
"license": "BSD-2-Clause",
"homepage": "https://waystone.nasqueron.org",
"authors": [
{
"name": "Sébastien Santoro",
"email": "dereckson@espace-win.org"
}
],
"require": {
- "keruald/database": "0.5.2",
+ "keruald/database": "0.6.1",
"keruald/omnitools": "0.16.0",
"keruald/yaml": "0.1.1",
"netresearch/jsonmapper": "^v5.0.0",
"smarty/smarty": "^5.6.0",
"vlucas/phpdotenv": "^v5.6.2",
"ext-mysqli": "*"
},
"require-dev": {
"nasqueron/codestyle": "^0.1.2",
- "phpunit/phpunit": "^12.4",
+ "phpunit/phpunit": "^12.5",
"squizlabs/php_codesniffer": "^4.0"
},
"replace": {
+ "waystone/orbeon-forms": "0.1.0",
"waystone/workspaces": "1.0.0"
},
"autoload": {
"psr-4": {
+ "Waystone\\Apps\\OrbeonForms\\": "apps/orbeon-forms/src/",
+ "Waystone\\Apps\\OrbeonForms\\Tests\\": "apps/orbeon-forms/tests/",
"Waystone\\Workspaces\\": "workspaces/src/",
"Waystone\\Workspaces\\Tests\\": "workspaces/tests/"
}
},
"scripts": {
"lint-src": "find */src -type f -name '*.php' | xargs -I {} php -l {} 1> /dev/null",
"lint-tests": "find */tests -type f -name '*.php' | xargs -n1 php -l",
"test": "vendor/bin/phpunit"
},
"minimum-stability": "dev"
}
diff --git a/workspaces/src/Engines/Exceptions/DocumentNotFoundException.php b/workspaces/src/Engines/Exceptions/DocumentNotFoundException.php
new file mode 100644
index 0000000..270cb99
--- /dev/null
+++ b/workspaces/src/Engines/Exceptions/DocumentNotFoundException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Waystone\Workspaces\Engines\Exceptions;
+
+use RuntimeException;
+
+class DocumentNotFoundException extends RuntimeException {
+
+}
diff --git a/workspaces/src/skins/bluegray/apps/_blocks/dl.tpl b/workspaces/src/skins/bluegray/apps/_blocks/dl.tpl
new file mode 100644
index 0000000..0dd61f4
--- /dev/null
+++ b/workspaces/src/skins/bluegray/apps/_blocks/dl.tpl
@@ -0,0 +1,8 @@
+<div class="row">
+ <dl>
+ {foreach from=$items key=key item=item}
+ <dt>{$key}</dt>
+ <dd>{$item}</dd>
+ {/foreach}
+ </dl>
+</div>
diff --git a/workspaces/src/skins/bluegray/apps/_blocks/menu_items.tpl b/workspaces/src/skins/bluegray/apps/_blocks/menu_items.tpl
new file mode 100644
index 0000000..43bd9a8
--- /dev/null
+++ b/workspaces/src/skins/bluegray/apps/_blocks/menu_items.tpl
@@ -0,0 +1,11 @@
+<div class="row">
+ {if $items}
+ <ul id="documents">
+ {foreach from=$items item=item}
+ <li class="document"><a href="{$item.url}">{$item.name}</a></li>
+ {/foreach}
+ </ul>
+ {else}
+ <p>No element currently available.</p>
+ {/if}
+</div>
diff --git a/workspaces/src/skins/bluegray/apps/_blocks/page_header.tpl b/workspaces/src/skins/bluegray/apps/_blocks/page_header.tpl
new file mode 100644
index 0000000..ea1185c
--- /dev/null
+++ b/workspaces/src/skins/bluegray/apps/_blocks/page_header.tpl
@@ -0,0 +1,3 @@
+<header class="row">
+ <h1 class="page-header">{$PAGE_TITLE}</h1>
+</header>
diff --git a/workspaces/src/skins/bluegray/apps/_crud/list.tpl b/workspaces/src/skins/bluegray/apps/_crud/list.tpl
new file mode 100644
index 0000000..a010a59
--- /dev/null
+++ b/workspaces/src/skins/bluegray/apps/_crud/list.tpl
@@ -0,0 +1,30 @@
+<div class="row">
+ {if $items}
+ <table class="table">
+ <thead>
+ <tr>
+ {foreach from=$keys item=key}
+ <th scope="col">{$key}</th>
+ {/foreach}
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {foreach from=$items item=item}
+ <tr>
+ {foreach from=$keys item=key name=loop}
+ {if $smarty.foreach.loop.index == 0}
+ <td><a href="{$item.url}">{$item[$key]}</a></td>
+ {else}
+ <td>{$item[$key]}</td>
+ {/if}
+ {/foreach}
+ <td><a href="{$item.url}">🔎</a></td>
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ {else}
+ <p>No item.</p>
+ {/if}
+</div>
diff --git a/workspaces/src/skins/bluegray/header.tpl b/workspaces/src/skins/bluegray/header.tpl
index 199e082..fce78b4 100644
--- a/workspaces/src/skins/bluegray/header.tpl
+++ b/workspaces/src/skins/bluegray/header.tpl
@@ -1,87 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{#SiteTitle#}{if $PAGE_TITLE} :: {$PAGE_TITLE}{/if}</title>
<link href="{#StaticContentURL#}/css/bootstrap.css" rel="stylesheet">
<link href="{#StaticContentURL#}/css/font-awesome.min.css" rel="stylesheet">
<link href="{#StaticContentURL#}/css/bluegray.css" rel="stylesheet">
<link href="{#StaticContentURL#}/favicon.ico" rel="shorcut icon" type="image/x-icon">
<link href="{#StaticContentURL#}/favicon.png" rel="icon" type="image/png" />
+{if isset($custom_css)}
+ <style>
+ /* Custom CSS specific for this page */
+ {$custom_css}
+ </style>
+{/if}
</head>
<body>
<a href="#content" class="sr-only">{#SkipNavigation#}</a>
{if isset($custom_workspace_header)}
<!-- Workspace header-->
{$custom_workspace_header}
{/if}
<div id="wrapper">
<nav class="navbar navbar-default navbar-static-top" role="navigation" style="margin-bottom: 0">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".sidebar-collapse">
<span class="sr-only">{#ToggleNavigation#}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{if isset($current_workspace)}
<a class="navbar-brand" href="{$current_workspace_url}">{$current_workspace->name}</a>
{else}
<a class="navbar-brand" href="{$root_url}">{#SiteTitle#}</a>
{/if}
</div>
<ul class="nav navbar-top-links navbar-pushed-to-right">
{if $workspaces_count > 1}
<!-- Workspaces -->
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-book fa-fw"></i> <i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-messages">
{foreach from=$workspaces item=workspace}
<li>
<a href="/{$workspace->code}">
<div>
<strong class="workspace-name">{$workspace->name}</strong>
<span class="pull-right text-muted">
<em class="workspace-counter">{counter}</em>
</span>
</div>
<div class="workspace-description">{$workspace->description}</div>
</a>
</li>
<li class="divider"></li>
{/foreach}
<li>
<a class="text-center" href="#">
<strong>{#WorkspacesManagement#}</strong>
<i class="fa fa-angle-right"></i>
</a>
</li>
</ul>
</li>
{/if}
<!-- Other right navigation actions -->
<li>
{if isset($current_workspace)}
<a href="{$current_workspace_url}?action=user.logout">
{else}
<a href="{$root_url}?action=user.logout">
{/if}
<i class="fa fa-sign-out fa-fw"></i> {#Logout#}
</a>
</li>
</ul>
</nav>
{if isset($controller_custom_nav) }
{include file=$controller_custom_nav}
{else}
{include file='nav_main.tpl'}
{/if}
- <div id="content">
\ No newline at end of file
+ <div id="content">
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 16, 13:22 (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3155501
Default Alt Text
(25 KB)
Attached To
Mode
rOBSIDIAN Obsidian Workspaces
Attached
Detach File
Event Timeline
Log In to Comment