Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0725366dd5 | |||
| a2bcd10607 | |||
| 37ed966ac3 | |||
| 085c307137 | |||
| d961d910d8 | |||
| 1dc6d541cc | |||
| 7fef89b778 | |||
| 6b713e4ffb | |||
| b52a91bf52 | |||
| e8d8340448 | |||
| 81371c41a7 | |||
| 3cf5f0173b | |||
| 65844e030d | |||
| ba7e4ab151 | |||
| ec4e578aee | |||
| 1a5f300a78 | |||
| a04cdd753d | |||
| daa4b268a6 | |||
| a5161cb6b4 | |||
| 48e32bfdcb | |||
| 639ee895cd | |||
| 8cf1dd9bfd | |||
| fcadc8d7a1 | |||
| 1e2f941f47 | |||
| eeee2afc05 | |||
| 98efb76f7b | |||
| dff3748623 | |||
| 93a16ee916 | |||
| 8b3a3964a5 | |||
| 43dd36e20e | |||
| 5587e8b4b6 | |||
| 16e50e2d49 | |||
| bd01e5c32c | |||
| e058ceadee | |||
| 4a7eb217a1 | |||
| 57b0727788 | |||
| 58b047edd2 | |||
| a78b699a5e | |||
| e910ae9878 | |||
| 362c649d36 | |||
| 852c4d608d | |||
| aa59e55a41 | |||
| 348bad80e0 | |||
| f0d4686141 | |||
| a482ab3c7e | |||
| c580882ec0 | |||
| 2cf8379cbe | |||
| c39ec469df | |||
| 232fcc8845 | |||
| 9d5d4dd930 | |||
| c507dc6031 |
@@ -6,3 +6,4 @@ front/node_modules
|
|||||||
front/npm-debug.log
|
front/npm-debug.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/.claude
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "/phpactor.schema.json",
|
||||||
|
"language_server_phpstan.enabled": false
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Upgrade from 1.1.* to 1.2.0
|
||||||
|
|
||||||
|
## lucent.php config file
|
||||||
|
|
||||||
|
There is now an array of commands, accepting more than one.
|
||||||
|
|
||||||
|
from
|
||||||
|
```php
|
||||||
|
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
|
||||||
|
```
|
||||||
|
|
||||||
|
to
|
||||||
|
```php
|
||||||
|
"commands" => [
|
||||||
|
"generate:static" => "Build Website",
|
||||||
|
],
|
||||||
|
```
|
||||||
|
## config/filesystems.php
|
||||||
|
|
||||||
|
Lucent has its own filesystem.
|
||||||
|
|
||||||
|
You should now add:
|
||||||
|
|
||||||
|
```
|
||||||
|
'lucent' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
```
|
||||||
+5
-8
@@ -6,19 +6,17 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"ext-sqlite3": "*",
|
|
||||||
"ext-imagick": "*",
|
"ext-imagick": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"php": "^8.3",
|
"php": "^8.4",
|
||||||
|
"intervention/image": "^2.7",
|
||||||
"phpoption/phpoption": "^1.9",
|
"phpoption/phpoption": "^1.9",
|
||||||
"spatie/image-optimizer": "^1.6",
|
"spatie/image-optimizer": "^1.6",
|
||||||
"staudenmeir/laravel-cte": "^1.10",
|
"staudenmeir/laravel-cte": "^1.0"
|
||||||
"intervention/image": "^3.8",
|
|
||||||
"guzzlehttp/guzzle": "^7.9"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^10.48",
|
"phpstan/phpstan": "^1.8",
|
||||||
"phpstan/phpstan": "^1.12"
|
"laravel/framework": "^10.10"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -38,5 +36,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+631
-916
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
"env" => env("LUCENT_ENV", "production"),
|
|
||||||
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "resources/lucent/schemas"),
|
|
||||||
"image_filters_path" => "app/Filters",
|
|
||||||
"database" => env('LUCENT_DB_CONNECTION', env('DB_CONNECTION', "sqlite")),
|
|
||||||
"name" => env("LUCENT_NAME", "Stoic"),
|
|
||||||
"url" => env("LUCENT_URL", env('APP_URL')),
|
|
||||||
"preview_target" => env("LUCENT_PREVIEW_TARGET", "previewTarget"),
|
|
||||||
/*
|
|
||||||
* Make available laravel artisan commands for admin users
|
|
||||||
* example:
|
|
||||||
* [
|
|
||||||
* "command1:signature" => "Description 1"
|
|
||||||
* "command2:signature" => "Description 2"
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* */
|
|
||||||
"commands" => [],
|
|
||||||
"can_invite" => ["admin"],
|
|
||||||
"can_run_commands" => ["admin"],
|
|
||||||
"system_user_id" => ""
|
|
||||||
];
|
|
||||||
+145
-298
@@ -5,374 +5,221 @@ include_toc: true
|
|||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
|
|
||||||
Fields are similar to a table's columns in a relational databases.
|
Fields define the columns of a schema. Each field has a `ui` type that controls both storage and the admin UI component rendered.
|
||||||
|
|
||||||
## Available fields for Collections and Files
|
## Common Optional Properties
|
||||||
|
|
||||||
|
Most fields share these optional properties:
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `required` | Whether the field must have a value to save as `published` |
|
||||||
|
| `nullable` | Allow saving as `null` |
|
||||||
|
| `help` | Help text shown below the input |
|
||||||
|
| `default` | Default value when creating a new record |
|
||||||
|
| `readonly` | Prevent editing from the UI |
|
||||||
|
| `group` | Tab group this field belongs to |
|
||||||
|
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
### text
|
### text
|
||||||
One-line text input
|
|
||||||
|
|
||||||
required
|
One-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
| `selectOptions` | Array of options. Strings or `[{value, label}]` objects |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### textarea
|
### textarea
|
||||||
textarea input
|
|
||||||
|
|
||||||
required
|
Multi-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### slug
|
### slug
|
||||||
Slug input. Generates automatically if left empty
|
|
||||||
|
|
||||||
required
|
Slug input. Auto-generates from a source field if left empty.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `source`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **source**: The source field from which it generates
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `source` | Field name to generate the slug from |
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### rich
|
### rich
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
WYSIWYG rich text editor.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
### markdown
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
Markdown editor.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### number
|
### number
|
||||||
Any numeric value
|
|
||||||
|
|
||||||
required
|
Numeric input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `decimals` | Number of decimal places. Default: `0` |
|
||||||
|
| `min` | Minimum value |
|
||||||
|
| `max` | Maximum value |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **decimals**: default is 0
|
---
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### checkbox
|
### checkbox
|
||||||
True or false
|
|
||||||
|
|
||||||
required
|
Boolean true/false toggle.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### color
|
### color
|
||||||
Color picker
|
|
||||||
|
|
||||||
required
|
Color picker.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `selectOptions` | Restrict to a predefined palette |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### date
|
### date
|
||||||
Date select
|
|
||||||
|
|
||||||
required
|
Date selector. Stores as a date string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum date |
|
||||||
|
| `max` | Maximum date |
|
||||||
|
| `selectOptions` | Predefined date options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### datetime
|
### datetime
|
||||||
Date and time selector
|
|
||||||
|
|
||||||
required
|
Date and time selector. Stores as an ISO 8601 string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum datetime |
|
||||||
|
| `max` | Maximum datetime |
|
||||||
|
| `selectOptions` | Predefined datetime options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
### uuid
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
---
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### json
|
### json
|
||||||
Json data
|
|
||||||
|
|
||||||
required
|
Raw JSON data field. Accepts either a JSON string or a plain array — both are stored as JSON.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### file
|
### file
|
||||||
Upload or select files
|
|
||||||
|
|
||||||
required
|
Upload or select files from a files schema. Files can be uploaded directly or browsed from previously uploaded files. Image files (jpeg, png, webp, gif, tiff) automatically get a 300×300 thumbnail generated on upload.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
- **mime**: The mime types allowed to select
|
| `collections` | Array of file schema names to choose from |
|
||||||
- **nullable**: Can the field be saved as null
|
| `mime` | Allowed MIME types e.g. `["image/*"]` |
|
||||||
- **min**: Minimum files
|
| `min` | Minimum number of files |
|
||||||
- **max**: Maximum files
|
| `max` | Maximum number of files |
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### reference
|
### reference
|
||||||
Reference other records
|
|
||||||
|
|
||||||
required
|
Reference records from another collection.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `collections` | Array of collection schema names to reference |
|
||||||
|
| `min` | Minimum number of references |
|
||||||
|
| `max` | Maximum number of references |
|
||||||
|
|
||||||
- **nullable**: Can the field be saved as null
|
---
|
||||||
- **min**: Minimum files
|
|
||||||
- **max**: Maximum files
|
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
## Example Field
|
||||||
|
|
||||||
### block
|
```json
|
||||||
The block editor
|
{
|
||||||
|
"ui": "text",
|
||||||
required
|
"name": "title",
|
||||||
|
"label": "Title",
|
||||||
- **name**: The id of the field
|
"required": true,
|
||||||
- **label**: The friendly name of the field
|
"min": 3,
|
||||||
- **schema**: The block schema name
|
"max": 200,
|
||||||
|
"help": "The main title of the post"
|
||||||
optional
|
}
|
||||||
|
```
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
)
|
|
||||||
## Available fields for the Block Editor
|
|
||||||
|
|
||||||
### heading
|
|
||||||
Single-line text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### textarea
|
|
||||||
Multiline text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### rich
|
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### file
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **mime**: The mime types allowed to select
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### reference
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|||||||
+133
-52
@@ -5,88 +5,169 @@ include_toc: true
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
|
|
||||||
## Graph or Tree
|
The `Query` class is the main way to fetch records. Inject it via the Laravel container.
|
||||||
|
|
||||||
Queries can return results in 2 formats. A graph or a tree.
|
|
||||||
|
|
||||||
Graphs results are a collection of records (nodes) and a collection of edges. This format is more useful for network visualization.
|
|
||||||
|
|
||||||
The tree format is more straightforward as it returns a collection of records. Each record has a **_children** and a **_parents** field and the tree can continue upwards or downwards according to the depth you have requested.
|
|
||||||
|
|
||||||
For example to request records with their children and their children's children:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->childrenDepth(2);
|
public function __construct(private Query $query) {}
|
||||||
```
|
```
|
||||||
Maybe you only want to get a specific type of relationship:
|
|
||||||
|
|
||||||
```php
|
## Return Formats
|
||||||
$query->childrenDepth(2)->childrenFields(["categories"]);
|
|
||||||
```
|
Queries return results in two formats:
|
||||||
|
|
||||||
|
**Graph** — via `->run()`. Returns a `Graph` object with `records` and `edges` collections. Useful for network-style data.
|
||||||
|
|
||||||
|
**Tree** — via `->tree()`. Returns a flat `Collection` of records where each record has `_children` and `_parents` arrays populated based on the requested depth. This is the most common format.
|
||||||
|
|
||||||
|
## Chaining Methods
|
||||||
|
|
||||||
|
All methods return `$this` and can be chained:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `->filter(array)` | Add an AND filter |
|
||||||
|
| `->orFilter(array)` | Add an OR filter (grouped) |
|
||||||
|
| `->limit(int)` | Max number of root records to return |
|
||||||
|
| `->skip(int)` | Offset for pagination |
|
||||||
|
| `->sort(string)` | Sort by field. Prefix with `-` for descending e.g. `"-_sys.updatedAt"` |
|
||||||
|
| `->status(array)` | Filter by status array e.g. `["published", "draft"]` |
|
||||||
|
| `->onlyPublished()` | Shorthand for `->status(["published"])` |
|
||||||
|
| `->childrenDepth(int)` | How many levels of children to load |
|
||||||
|
| `->childrenLimit(int)` | Max children per record |
|
||||||
|
| `->childrenFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->parentsDepth(int)` | How many levels of parents to load |
|
||||||
|
| `->parentsLimit(int)` | Max parents per record |
|
||||||
|
| `->parentFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->notLinked(string)` | Return only records with no parents |
|
||||||
|
| `->run()` | Execute and return a `Graph` |
|
||||||
|
| `->tree()` | Execute and return a `Collection` (tree format) |
|
||||||
|
| `->runWithCount()` | Execute and return a `Graph` with a `total` count |
|
||||||
|
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
You can filter your query with the following format:
|
Filter keys use the format `field_operator`. When no operator suffix is given, `eq` is assumed.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->filter(["field_operator" => "value"]);
|
$query->filter(["field_operator" => "value"]);
|
||||||
|
|
||||||
// example:
|
// No operator = eq
|
||||||
|
$query->filter(["schema" => "blogPosts"]);
|
||||||
|
|
||||||
$query->filter(["date_lt" => "2020-09-15"]);
|
// With operator
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Or filters are also available:
|
### Operator Reference
|
||||||
|
|
||||||
|
| Operator | Description |
|
||||||
|
|---|---|
|
||||||
|
| _(none)_ or `eq` | Equals (string) |
|
||||||
|
| `ne` | Not equals (string) |
|
||||||
|
| `eqnum` | Equals (numeric) |
|
||||||
|
| `neqnum` | Not equals (numeric) |
|
||||||
|
| `eqtrue` | Equals `true` |
|
||||||
|
| `eqfalse` | Equals `false` |
|
||||||
|
| `netrue` | Not equals `true` |
|
||||||
|
| `nefalse` | Not equals `false` |
|
||||||
|
| `regex` | Regular expression match |
|
||||||
|
| `in` | Value is in array (strings) |
|
||||||
|
| `nin` | Value is not in array (strings) |
|
||||||
|
| `innum` | Value is in array (numeric) |
|
||||||
|
| `ninnum` | Value is not in array (numeric) |
|
||||||
|
| `lt` | Less than |
|
||||||
|
| `lte` | Less than or equal |
|
||||||
|
| `gt` | Greater than |
|
||||||
|
| `gte` | Greater than or equal |
|
||||||
|
| `null` | Field is null |
|
||||||
|
| `nnull` | Field is not null |
|
||||||
|
| `exists` | Field key exists |
|
||||||
|
| `nexists` | Field key does not exist |
|
||||||
|
| `filter` | Raw filter passthrough |
|
||||||
|
|
||||||
|
### OR Filters
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query
|
$query
|
||||||
->filter(["price_eqn" => 10])
|
->filter(["schema" => "blogPosts"])
|
||||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"])
|
->orFilter(["title_regex" => "search", "slug_regex" => "search"]);
|
||||||
;
|
|
||||||
```
|
```
|
||||||
## Operator List
|
|
||||||
|
|
||||||
- regex
|
Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together.
|
||||||
- eq
|
|
||||||
- ne
|
|
||||||
- eqnum
|
|
||||||
- neqnum
|
|
||||||
- filter
|
|
||||||
- eqtrue
|
|
||||||
- eqfalse
|
|
||||||
- netrue
|
|
||||||
- nefalse
|
|
||||||
- in:
|
|
||||||
- innum
|
|
||||||
- nin
|
|
||||||
- ninnum
|
|
||||||
- lt
|
|
||||||
- lte
|
|
||||||
- gt
|
|
||||||
- gte
|
|
||||||
- null
|
|
||||||
- nnull
|
|
||||||
- exists
|
|
||||||
- nexists
|
|
||||||
|
|
||||||
## Example
|
### Cross-Schema (Children) Filters
|
||||||
|
|
||||||
Get 10 posts from the sports category as a tree
|
You can filter records by properties of their related records using the `children.` prefix:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query->filter([
|
||||||
|
"schema" => "blogPosts",
|
||||||
|
"children.categories.data.slug" => "sports",
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns `blogPosts` that have a child in `categories` where `slug` equals `sports`.
|
||||||
|
|
||||||
|
|
||||||
|
## Nested Data Fields
|
||||||
|
|
||||||
|
Use dot notation to filter on JSON data fields and system fields:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Filter on a data field
|
||||||
|
$query->filter(["data.status_eq" => "draft"]);
|
||||||
|
|
||||||
|
// Filter on a system field
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Get published blog posts, newest first:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$posts = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->onlyPublished()
|
||||||
|
->sort("-_sys.updatedAt")
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get posts from a specific category with children loaded:**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$posts = $query
|
$posts = $query
|
||||||
->filter([
|
->filter([
|
||||||
"schema" => "posts",
|
"schema" => "blogPosts",
|
||||||
"children.categories.data.slug" => "sports",
|
"children.categories.data.slug" => "sports",
|
||||||
]
|
])
|
||||||
)
|
|
||||||
->childrenDepth(1)
|
->childrenDepth(1)
|
||||||
|
->childrenFields(["categories"])
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->tree();
|
->tree();
|
||||||
|
|
||||||
$posts->map(...)->toArray();
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Paginate records:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$graph = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->limit(20)
|
||||||
|
->skip(40)
|
||||||
|
->runWithCount();
|
||||||
|
|
||||||
|
$posts = $graph->tree();
|
||||||
|
$total = $graph->total;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Search across fields:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$results = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->orFilter(["title_regex" => $term, "slug_regex" => $term])
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|||||||
+77
-38
@@ -5,70 +5,106 @@ include_toc: true
|
|||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
||||||
Schemas define both the shape of your data and how the UI on the admin will behave.
|
Schemas define both the shape of your data and how the admin UI behaves.
|
||||||
|
|
||||||
There are 3 types of schemas
|
There are 2 types of schemas:
|
||||||
|
|
||||||
- Collections: Normal data
|
- **collection** — Regular data records
|
||||||
- Files: Images and files
|
- **files** — Images and file uploads
|
||||||
- Block: Used in the block editor
|
|
||||||
|
|
||||||
|
|
||||||
## Collection Reference
|
## Collection Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "collection"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` |
|
||||||
- **visible**: An array of field id to show on the content browser _optional_
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"collection"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
|
| `visible` | no | Field IDs to show in the content browser list |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **cardImage**: Field name of image you want to use as a preview image _optional_
|
| `sortBy` | no | Default sort in browser. Prefix with `-` for descending e.g. `-_sys.updatedAt` |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `cardTitle` | no | Mustache template for the record preview title e.g. `{{name}} - {{slug}}` |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `revisions` | no | Number of revisions to keep per record. Default: `0` (disabled) |
|
||||||
|
| `read` | no | Roles with read access. Empty means all roles can read |
|
||||||
|
| `write` | no | Roles with write access. Empty means all roles can write |
|
||||||
|
|
||||||
|
|
||||||
## Files Reference
|
## Files Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "files"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` |
|
||||||
- **path**: The relative directory that these files will be stored.
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"files"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
| `sortBy` | no | Default sort in browser |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `cardTitle` | no | Mustache template for the record preview title |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `revisions` | no | Number of revisions to keep per record |
|
||||||
|
| `read` | no | Roles with read access |
|
||||||
|
| `write` | no | Roles with write access |
|
||||||
|
|
||||||
|
### File Metadata
|
||||||
|
|
||||||
|
Every uploaded file is automatically stored with the following metadata (stored in `lucent_files`):
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `id` | Unique file ID |
|
||||||
|
| `recordId` | ID of the record this file belongs to |
|
||||||
|
| `originalName` | Original filename as uploaded |
|
||||||
|
| `mime` | MIME type e.g. `image/webp` |
|
||||||
|
| `path` | Storage path e.g. `files/{recordId}/{filename}` |
|
||||||
|
| `size` | File size in bytes |
|
||||||
|
| `width` | Image width in pixels (0 for non-images) |
|
||||||
|
| `height` | Image height in pixels (0 for non-images) |
|
||||||
|
| `checksum` | SHA-1 hash of the file contents |
|
||||||
|
|
||||||
|
Image files (jpeg, png, webp, gif, tiff) also get a 300×300 thumbnail generated automatically at `thumbs/{path}`, plus any image filter presets configured in `lucent.imageFilters`.
|
||||||
|
|
||||||
|
|
||||||
A full Collection example without the fields:
|
## System Fields
|
||||||
|
|
||||||
|
Every record automatically has these read-only system fields available in queries:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `_sys.createdAt` | ISO 8601 creation timestamp |
|
||||||
|
| `_sys.updatedAt` | ISO 8601 last update timestamp |
|
||||||
|
| `_sys.createdBy` | User ID who created the record |
|
||||||
|
| `_sys.updatedBy` | User ID who last updated the record |
|
||||||
|
| `_sys.version` | Revision version number |
|
||||||
|
| `status` | `draft` or `published` |
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"label": "Departments",
|
"schemas": [
|
||||||
"name": "departments",
|
{
|
||||||
|
"label": "Blog Posts",
|
||||||
|
"name": "blogPosts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"visible": [
|
"visible": [
|
||||||
|
"title",
|
||||||
"slug",
|
"slug",
|
||||||
"cover",
|
|
||||||
"_sys.updatedAt",
|
"_sys.updatedAt",
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
"Extra Info",
|
"Content",
|
||||||
"Metadata",
|
|
||||||
"SEO"
|
"SEO"
|
||||||
],
|
],
|
||||||
"sortBy": "-_sys.createdAt",
|
"sortBy": "-_sys.createdAt",
|
||||||
"schemaTitle": "{{name}} {{slug}}",
|
"cardTitle": "{{title}}",
|
||||||
"schemaImage": "cover",
|
"cardImage": "cover",
|
||||||
"revisions": 15,
|
"revisions": 15,
|
||||||
"read": [
|
"read": [
|
||||||
"admin",
|
"admin",
|
||||||
@@ -80,5 +116,8 @@ A full Collection example without the fields:
|
|||||||
"editors"
|
"editors"
|
||||||
],
|
],
|
||||||
"fields": []
|
"fields": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": ["admin", "editors", "reviewers"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
+82
-23
@@ -5,51 +5,94 @@ To generate the static content of the website, lucent provides a helper class.
|
|||||||
|
|
||||||
## Laravel Command
|
## Laravel Command
|
||||||
|
|
||||||
Just create a command and name it as you like. Make sure to inject the StaticGenerator class.
|
Create an Artisan command and inject `StaticGenerator`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function __construct(
|
class GenerateStatic extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'generate:static';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
public StaticGenerator $staticGenerator,
|
public StaticGenerator $staticGenerator,
|
||||||
) {
|
public Context $ctx,
|
||||||
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$this->staticGenerator->run('generate:static', function ($writer) {
|
||||||
|
$writer->save("/", $this->ctx->render("homepage"));
|
||||||
|
$writer->save("/about", $this->ctx->render("about"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Redirect command
|
`run(string $signature, callable $callback)` — the first argument must match the command's artisan signature. This is used to stream live build logs in the Lucent UI. The callback receives a `Writer` instance.
|
||||||
|
|
||||||
There are cases which is useful to create a redirect like when you have a multilingual website:
|
|
||||||
|
## Writer: save
|
||||||
|
|
||||||
|
Writes an HTML file at the given path:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$this->staticGenerator->run(function ($writer) {
|
$writer->save("/blog/my-post", $html);
|
||||||
$writer->createRedirect("/", "/el");
|
// writes to: storage/lucent/build/blog/my-post/index.html
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## The Writer save command
|
An optional third argument changes the file extension (default `"html"`).
|
||||||
|
|
||||||
In order to create an html file, you have to use the writer's save command
|
|
||||||
|
|
||||||
The first argument is the url and the second is the rendered HTML. That's where the page classes do come handy.
|
## Writer: createRedirect
|
||||||
|
|
||||||
|
Generates an HTML meta-refresh redirect. Useful for root-level locale redirects:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$this->staticGenerator->run(function ($writer) {
|
$writer->createRedirect("/", "/el");
|
||||||
$writer->save("/", $this->ctx->render("homepage"));
|
$writer->createRedirect("/", "/el", "Redirecting", "Please wait...");
|
||||||
$writer->save("/about", $this->ctx->render("about"));
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Arguments: `from`, `to`, `title` (default `"Redirecting"`), `message` (default `"Redirecting Soon..."`).
|
||||||
|
|
||||||
|
|
||||||
|
## Writer: recordIterator
|
||||||
|
|
||||||
|
Iterates over all records in paginated batches — useful when there are too many records to load at once:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$writer->recordIterator(
|
||||||
|
query: fn(int $limit, int $skip) => $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->onlyPublished()
|
||||||
|
->limit($limit)
|
||||||
|
->skip($skip)
|
||||||
|
->tree(),
|
||||||
|
parser: function ($records) use ($writer) {
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$writer->save(
|
||||||
|
"/blog/" . $record->data->slug,
|
||||||
|
$this->ctx->render("blogPost", $record),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Signature: `recordIterator(callable $query, callable $parser, int $skip = 0, int $limit = 100): int`
|
||||||
|
|
||||||
|
The `$query` callable receives `($limit, $skip)` and must return a collection. Iteration stops automatically when a batch returns zero records.
|
||||||
|
|
||||||
|
|
||||||
## Storage and Permissions
|
## Storage and Permissions
|
||||||
|
|
||||||
All the generated html is stored on `storage/lucent/live`
|
All generated HTML is written to `storage/lucent/build` during the run, then atomically swapped to `storage/lucent/live` on completion. Create the public symlink with:
|
||||||
In order to make it accessible you have to create symlink:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan lucent:livelink
|
php artisan lucent:livelink
|
||||||
```
|
```
|
||||||
|
|
||||||
Now your static website is accessible in http://localhost:8000/live
|
The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost:
|
||||||
|
|
||||||
But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost
|
|
||||||
|
|
||||||
```nginxconf
|
```nginxconf
|
||||||
location / {
|
location / {
|
||||||
@@ -57,14 +100,30 @@ location / {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Artisan Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent:setup-db` | Create all Lucent database tables |
|
||||||
|
| `lucent:schemas` | Compile schema JSON files |
|
||||||
|
| `lucent:livelink` | Create the `public/live` symlink |
|
||||||
|
| `lucent:rebuild:thumbnails` | Regenerate thumbnails for all uploaded images |
|
||||||
|
| `lucent:removeOrphanEdges` | Remove edges pointing to deleted records |
|
||||||
|
| `lucent:generate:collection {name}` | Scaffold a new collection schema JSON file |
|
||||||
|
|
||||||
|
|
||||||
## Build from the Lucent UI
|
## Build from the Lucent UI
|
||||||
|
|
||||||
In your lucent.php config file you can define your command that is used to generate the static files:
|
Register your generate command in `config/lucent.php` so admin users can trigger it from the UI:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
"generateCommand" => "generate:static"
|
"commands" => [
|
||||||
|
"generate:static" => "Generate Static Site",
|
||||||
|
],
|
||||||
```
|
```
|
||||||
That way, the users will be able to initiate the build command through the user interface.
|
|
||||||
|
Only roles listed in `canBuild` can trigger commands from the UI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,3 +99,113 @@ class Context
|
|||||||
```
|
```
|
||||||
|
|
||||||
Add the middleware inside the HTTP Kernel file.
|
Add the middleware inside the HTTP Kernel file.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
After publishing the config (`php artisan vendor:publish --tag=lucent-config`), configure `config/lucent.php` via `.env`:
|
||||||
|
|
||||||
|
| Key | Env var | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `env` | `LUCENT_ENV` | `production` | `production` or `development` |
|
||||||
|
| `auth` | `LUCENT_AUTH` | `lucent` | Auth driver: `lucent` or `lunar` |
|
||||||
|
| `disk` | `LUCENT_DISK` | `public` | Laravel filesystem disk for uploads |
|
||||||
|
| `schemas_path` | `LUCENT_SCHEMAS_PATH` | `resources/lucent/schemas` | Where schema JSON files live |
|
||||||
|
| `database` | `LUCENT_DB_CONNECTION` | `DB_CONNECTION` | Database connection name |
|
||||||
|
| `name` | `LUCENT_NAME` | `Lucent` | CMS display name |
|
||||||
|
| `url` | `LUCENT_URL` | `APP_URL` | Base URL of the CMS |
|
||||||
|
| `previewTarget` | `LUCENT_PREVIEW_TARGET` | `previewTarget` | Preview route parameter |
|
||||||
|
| `commands` | — | `[]` | Artisan commands exposed to admin users (see [Static Generator](Static%20Generator.md)) |
|
||||||
|
| `imageFilters` | — | `[]` | Image filter presets applied to uploads (see below) |
|
||||||
|
| `canInvite` | — | `["admin"]` | Roles that can invite new users |
|
||||||
|
| `canBuild` | — | `["admin"]` | Roles that can trigger static builds |
|
||||||
|
| `systemUserId` | — | `""` | User ID used for console-initiated record writes |
|
||||||
|
|
||||||
|
|
||||||
|
## Image Filters
|
||||||
|
|
||||||
|
Image filters are Intervention Image filter classes applied to every uploaded image, producing additional versions (e.g. a cropped or watermarked variant). Each preset is stored at `templates/{name}/{original-path}` on the configured disk.
|
||||||
|
|
||||||
|
Define filters in `config/lucent.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
"imageFilters" => [
|
||||||
|
"hero" => \App\ImageFilters\HeroFilter::class,
|
||||||
|
"thumb" => \App\ImageFilters\ThumbFilter::class,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
A filter class must implement `Intervention\Image\Filters\FilterInterface`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\ImageFilters;
|
||||||
|
|
||||||
|
use Intervention\Image\Filters\FilterInterface;
|
||||||
|
use Intervention\Image\Image;
|
||||||
|
|
||||||
|
class HeroFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function applyFilter(Image $image): Image
|
||||||
|
{
|
||||||
|
return $image->fit(1200, 600)->encode('webp', 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every uploaded image automatically gets a 300×300 WebP thumbnail at `thumbs/{path}` in addition to any configured presets.
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Lucent uses **email-link (magic link) login** — no passwords. Users receive a time-limited link by email.
|
||||||
|
|
||||||
|
### Auth modes
|
||||||
|
|
||||||
|
Set `LUCENT_AUTH` to choose how users are stored:
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent` | Users stored in the `lucent_users` table. Supports roles. |
|
||||||
|
| `lunar` | Delegates to Lunar's `lunar_staff` table. Roles not supported. |
|
||||||
|
|
||||||
|
### Login flow
|
||||||
|
|
||||||
|
1. User submits their email at `/lucent/login`
|
||||||
|
2. A 32-character token is stored against the user with a timestamp
|
||||||
|
3. Lucent emails a login link containing the token
|
||||||
|
4. User clicks the link → token is validated (must be used within **1 hour**)
|
||||||
|
5. Session is established; token is cleared
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
|
||||||
|
The `/lucent/register` route is only available when **no users exist** in the system. Once the first admin registers, the route returns a redirect to `/lucent/login`. Registration automatically assigns the `admin` role and sends a login link.
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
Two middleware aliases are available for your routes:
|
||||||
|
|
||||||
|
| Alias | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent.auth` | Requires an active Lucent session |
|
||||||
|
| `lucent.guest` | Redirects authenticated users away |
|
||||||
|
|
||||||
|
### Roles
|
||||||
|
|
||||||
|
Roles are defined in your channel config. Only roles listed there are valid — anything else is silently stripped on assignment. The `canInvite` and `canBuild` config keys control which roles can invite users and trigger builds respectively.
|
||||||
|
|
||||||
|
On console commands and non-`/lucent` routes, `currentUserId()` returns `config("lucent.systemUserId")` instead of the session user.
|
||||||
|
|
||||||
|
|
||||||
|
## Database Tables
|
||||||
|
|
||||||
|
All Lucent-managed tables use the `lucent_` prefix:
|
||||||
|
|
||||||
|
| Table | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent_records` | All collection and file schema records |
|
||||||
|
| `lucent_files` | Uploaded file metadata |
|
||||||
|
| `lucent_edges` | Relationships between records |
|
||||||
|
| `lucent_revisions` | Record revision history |
|
||||||
|
| `lucent_users` | Users (when using `lucent` auth mode) |
|
||||||
|
| `lucent_command_logs` | Background command execution logs |
|
||||||
|
|
||||||
|
These are created automatically by running `php artisan lucent:setup`.
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-225
File diff suppressed because one or more lines are too long
Vendored
+212
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"main.js": {
|
"main.js": {
|
||||||
"file": "assets/main-C4XTQmaY.js",
|
"file": "assets/main-DH0OAeUr.js",
|
||||||
"name": "main",
|
"name": "main",
|
||||||
"src": "main.js",
|
"src": "main.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main-BJijircB.css"
|
"assets/main-BVNnoznq.css"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
-51
@@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
|
||||||
* to our Laravel back-end. This library automatically handles sending the
|
|
||||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
|
|
||||||
|
|
||||||
loadHtmxFormsBehaviour();
|
|
||||||
window.axios = axios;
|
|
||||||
export const axiosInstance = axios;
|
|
||||||
|
|
||||||
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
|
|
||||||
window.axios.interceptors.request.use(
|
|
||||||
function (config) {
|
|
||||||
let list;
|
|
||||||
list = document.querySelectorAll(".btn-spinner");
|
|
||||||
for (let i = 0; i < list.length; ++i) {
|
|
||||||
list[i].classList.add("spinner-on");
|
|
||||||
list[i].disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
window.axios.interceptors.response.use(
|
|
||||||
function (response) {
|
|
||||||
let list;
|
|
||||||
list = document.querySelectorAll(".btn-spinner");
|
|
||||||
for (let i = 0; i < list.length; ++i) {
|
|
||||||
list[i].classList.remove("spinner-on");
|
|
||||||
list[i].disabled = false;
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
let list;
|
|
||||||
list = document.querySelectorAll(".btn-spinner");
|
|
||||||
for (let i = 0; i < list.length; ++i) {
|
|
||||||
list[i].classList.remove("spinner-on");
|
|
||||||
list[i].disabled = false;
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export function debounce(callback, wait) {
|
|
||||||
let timeoutId = null;
|
|
||||||
|
|
||||||
return (...args) => {
|
|
||||||
window.clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
timeoutId = window.setTimeout(() => {
|
|
||||||
callback.apply(null, args);
|
|
||||||
}, wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+110
-27
@@ -1,7 +1,7 @@
|
|||||||
import {format, formatDistanceToNow, parseJSON} from "date-fns";
|
import { formatDistanceToNow, parseJSON, format, parse } from "date-fns";
|
||||||
|
|
||||||
export function friendlyDate(date) {
|
export function friendlyDate(date) {
|
||||||
return formatDistanceToNow(parseJSON(date), {addSuffix: true});
|
return formatDistanceToNow(parseJSON(date), { addSuffix: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readableDate(date) {
|
export function readableDate(date) {
|
||||||
@@ -19,50 +19,133 @@ export function readableDatetime(date) {
|
|||||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stripHtml(html = "") {
|
export function stripHtml(html = "") {
|
||||||
let tmp = document.createElement("div");
|
let tmp = document.createElement("div");
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
return tmp.textContent || tmp.innerText || "";
|
return tmp.textContent || tmp.innerText || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function randomId(length = 10) {
|
export function randomId(length = 10) {
|
||||||
return Math.random().toString(36).substring(2, length + 2);
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, length + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clickOutside(node) {
|
export function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
const handleClick = event => {
|
|
||||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||||
node.dispatchEvent(
|
node.dispatchEvent(new CustomEvent("click_outside", node));
|
||||||
new CustomEvent('click_outside', node)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('click', handleClick, true);
|
document.addEventListener("click", handleClick, true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
document.removeEventListener('click', handleClick, true);
|
document.removeEventListener("click", handleClick, true);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uniqueBy(list, callback) {
|
export function apiFetch(url, options = {}) {
|
||||||
const itemMap = list.reduce((c, item) => {
|
return fetch(url, {
|
||||||
c[callback(item)] = item;
|
...options,
|
||||||
return c;
|
headers: {
|
||||||
}, {});
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
return Object.values(itemMap);
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function range(start, end) {
|
export function apiPost(url, body, options = {}) {
|
||||||
var ans = [];
|
return fetch(url, {
|
||||||
for (let i = start; i <= end; i++) {
|
...options,
|
||||||
ans.push(i);
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
}).then((r) => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiGet(url, options = {}) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
}).then((r) => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEqual(db, ed) {
|
||||||
|
let isObject = (x) =>
|
||||||
|
typeof x === "object" && !Array.isArray(x) && x !== null;
|
||||||
|
let isArray = (x) => x?.constructor === Array;
|
||||||
|
let isEmpty = (x) => x === null || x === undefined;
|
||||||
|
const db_value = db ?? null;
|
||||||
|
const ed_value = ed ?? null;
|
||||||
|
|
||||||
|
if (isObject(db_value)) {
|
||||||
|
let keys = Object.keys(db_value);
|
||||||
|
return keys.reduce((acc, k) => {
|
||||||
|
if (acc === false) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return ans;
|
return isEqual(db_value?.[k], ed_value?.[k]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
if (isArray(db_value)) {
|
||||||
|
if (!isArray(ed_value) || db_value.length !== ed_value.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return db_value.reduce((c, v, i) => {
|
||||||
|
if (c === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(v, ed_value[i]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(db_value) && isEmpty(ed_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_value == ed_value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(fn, delay) {
|
||||||
|
let timer;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUnique(array) {
|
||||||
|
return array.filter((value, index) => array.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUniqueBy(items, uniqueBy) {
|
||||||
|
const ids = new Set(items.map((item) => item[uniqueBy]));
|
||||||
|
return [...ids].map((id) => items.find((i) => i[uniqueBy] === id));
|
||||||
|
}
|
||||||
|
export function arrayUniqueCb(items, aFilter) {
|
||||||
|
const cache = new Set();
|
||||||
|
return items.filter((item) => {
|
||||||
|
const cacheValue = aFilter(item);
|
||||||
|
if (cache.has(cacheValue)) return false;
|
||||||
|
cache.add(cacheValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
export function loadHtmxFormsBehaviour(){
|
|
||||||
document.querySelectorAll(".form").forEach(el => {
|
|
||||||
initHtmxForm(el);
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function initHtmxForm(el){
|
|
||||||
el.addEventListener("htmx:responseError", (e) => {
|
|
||||||
el.querySelector(".form-errors").innerHTML = e.detail.xhr.response;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formEl = el.querySelector("form");
|
|
||||||
|
|
||||||
if(!formEl.getAttribute("hx-redirect")){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.addEventListener("htmx:afterOnLoad", (e) => {
|
|
||||||
if(e.detail.successful){
|
|
||||||
return window.location.href = formEl.getAttribute("hx-redirect");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
+7
-12
@@ -1,13 +1,11 @@
|
|||||||
import {axiosInstance} from "./bootstrap";
|
import "../sass/app.scss";
|
||||||
import Account from "./svelte/Account.svelte";
|
import Account from "./svelte/Account.svelte";
|
||||||
import Channel from "./svelte/Channel.svelte";
|
import Channel from "./svelte/Channel.svelte";
|
||||||
import Mustache from "mustache";
|
// import Mustache from "mustache";
|
||||||
import 'htmx.org';
|
|
||||||
|
|
||||||
Mustache.escape = function (value) {
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Mustache.escape = function (value) {
|
||||||
|
// return value;
|
||||||
|
// };
|
||||||
|
|
||||||
// Define all components
|
// Define all components
|
||||||
const entryComponents = {
|
const entryComponents = {
|
||||||
@@ -28,17 +26,14 @@ let loadSvelte = function () {
|
|||||||
const loadElement = function (element) {
|
const loadElement = function (element) {
|
||||||
const componentId = element.attributes["data-layout"].value;
|
const componentId = element.attributes["data-layout"].value;
|
||||||
const [_, component] = Object.entries(entryComponents).find(
|
const [_, component] = Object.entries(entryComponents).find(
|
||||||
([key, _]) => componentId === key
|
([key, _]) => componentId === key,
|
||||||
);
|
);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonData = document.getElementById(
|
const jsonData = document.getElementById("json-" + componentId).innerHTML;
|
||||||
"json-" + componentId
|
|
||||||
).innerHTML;
|
|
||||||
const props = JSON.parse(jsonData);
|
const props = JSON.parse(jsonData);
|
||||||
props.axios = axiosInstance;
|
|
||||||
const compOptions = {
|
const compOptions = {
|
||||||
target: element,
|
target: element,
|
||||||
props: props,
|
props: props,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Login from "./account/Login.svelte";
|
import Login from "./account/Login.svelte";
|
||||||
import Verify from "./account/Verify.svelte";
|
import Verify from "./account/Verify.svelte";
|
||||||
import Profile from "./account/Profile.svelte";
|
import Profile from "./account/Profile.svelte";
|
||||||
import {setContext} from "svelte";
|
import { setContext } from "svelte";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
register: Register,
|
register: Register,
|
||||||
@@ -21,10 +21,7 @@
|
|||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
</script>
|
</script>
|
||||||
<div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)">
|
|
||||||
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name ?? "Lucent Setup"}</a></h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<svelte:component this={components[view]} {title} {...data}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<svelte:component this={components[view]} {channel} {title} {...data} />
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import RecordNotFound from "./records/NotFound.svelte";
|
import RecordNotFound from "./records/NotFound.svelte";
|
||||||
import RecordEdit from "./records/Edit.svelte";
|
import RecordEdit from "./records/Edit.svelte";
|
||||||
import ContentIndex from "./content/Index.svelte";
|
import ContentIndex from "./content/Index.svelte";
|
||||||
import {setContext} from "svelte";
|
import { setContext } from "svelte";
|
||||||
import Navbar from "./layout/Navbar.svelte";
|
import Navbar from "./layout/Navbar.svelte";
|
||||||
import HomeIndex from "./home/Index.svelte";
|
import HomeIndex from "./home/Index.svelte";
|
||||||
import BuildReport from "./build/Report.svelte";
|
import BuildReport from "./build/Report.svelte";
|
||||||
@@ -25,27 +25,22 @@
|
|||||||
// export let layout;
|
// export let layout;
|
||||||
export let channel;
|
export let channel;
|
||||||
|
|
||||||
export let axios;
|
|
||||||
export let readableSchemas;
|
export let readableSchemas;
|
||||||
|
|
||||||
|
|
||||||
setContext("axios", axios);
|
|
||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("readableSchemas", channel.schemas.filter((s) => readableSchemas.includes(s.name)));
|
setContext(
|
||||||
|
"readableSchemas",
|
||||||
|
channel.schemas.filter((s) => readableSchemas.includes(s.name)),
|
||||||
|
);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<Navbar schema={data.schema}/>
|
<Navbar schema={data.schema} />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<Header />
|
<Header />
|
||||||
<svelte:component this={components[view]} {title} {...data}/>
|
<svelte:component this={components[view]} {title} {...data} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let email = "";
|
let email = "";
|
||||||
let message = "";
|
let submitted = false;
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/login", { email: email })
|
||||||
.post(channel.lucentUrl + "/login", {
|
.then(() => {
|
||||||
email: email,
|
submitted = true;
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.catch(() => {});
|
||||||
message = "You will receive an email with a login link"
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="scope-login">
|
||||||
{#if message}
|
<div class="bg-image"></div>
|
||||||
|
<div class="login-form">
|
||||||
|
{#if submitted}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
{message}
|
<p>
|
||||||
|
You will receive an email with a login link at <b>{email}</b
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p>Check your spam folder</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="form">
|
||||||
|
<h2 class="mb-5">Enter Lucent</h2>
|
||||||
|
|
||||||
<form on:submit={login}>
|
<form on:submit={login}>
|
||||||
<div class="mb-3">
|
<p>
|
||||||
<label for="emailaddress" class="form-label">Email address</label>
|
Submit your email address and you will receive a <b
|
||||||
|
>login link</b
|
||||||
|
> to your email
|
||||||
|
</p>
|
||||||
|
<p>Don't forget to check your spam folder</p>
|
||||||
|
<div class="mt-5 mb-3">
|
||||||
|
<label for="emailaddress" class="form-label"
|
||||||
|
>Email address</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
@@ -43,12 +52,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="bt bt-primary">
|
||||||
<div class="text-center mt-5 d-block">
|
Send email
|
||||||
<SpinnerButton label="Login"/>
|
<img
|
||||||
</div>
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
|
src="/img/spinner.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import Avatar from "./Avatar.svelte";
|
import Avatar from "./Avatar.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -17,8 +17,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-name", {
|
||||||
.post(channel.lucentUrl + "/account/update-name", {
|
|
||||||
name: name,
|
name: name,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -26,6 +25,7 @@
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data.error;
|
errorMessage = error.response?.data.error;
|
||||||
|
console.log({ errorMessage });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +33,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-email", {
|
||||||
.post(channel.lucentUrl + "/account/update-email", {
|
|
||||||
email: email,
|
email: email,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -42,16 +41,16 @@
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data.error;
|
errorMessage = error.response?.data.error;
|
||||||
|
console.log({ errorMessage });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="wrapper-tiny">
|
||||||
<ErrorAlert message={errorMessage}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
<SuccessAlert bind:this={successAlert} />
|
<SuccessAlert bind:this={successAlert} />
|
||||||
<h3 class="header-small mb-5">
|
<h3 class="header-small mb-5">
|
||||||
<Avatar name={user.name}/>
|
<Avatar name={user.name} />
|
||||||
</h3>
|
</h3>
|
||||||
<form on:submit={saveName}>
|
<form on:submit={saveName}>
|
||||||
<div class="input-group mb-5">
|
<div class="input-group mb-5">
|
||||||
@@ -62,7 +61,7 @@
|
|||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update Name"/>
|
<SpinnerButton label="Update Name" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -75,12 +74,14 @@
|
|||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update Email"/>
|
<SpinnerButton label="Update Email" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this
|
<a
|
||||||
device</a>
|
class="list-group-item list-group-item-action"
|
||||||
|
href="{channel.lucentUrl}/logout">Logout from this device</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let name = "";
|
let name = "";
|
||||||
@@ -12,8 +13,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/register", {
|
||||||
.post(channel.lucentUrl + "/register", {
|
|
||||||
name: name,
|
name: name,
|
||||||
email: email,
|
email: email,
|
||||||
})
|
})
|
||||||
@@ -22,13 +22,13 @@
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data.error;
|
errorMessage = error.response?.data.error;
|
||||||
console.log({errorMessage});
|
console.log({ errorMessage });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="wrapper-tiny">
|
||||||
<ErrorAlert message={errorMessage}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
|
|
||||||
<form on:submit={register}>
|
<form on:submit={register}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -50,10 +50,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="text-center mt-5 d-block">
|
<div class="text-center mt-5 d-block">
|
||||||
<SpinnerButton label="Register"/>
|
<SpinnerButton label="Register" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let email;
|
export let email;
|
||||||
export let token;
|
export let token;
|
||||||
let successAlert;
|
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/verify", {
|
||||||
.post(channel.lucentUrl + "/verify", {
|
|
||||||
email: email,
|
email: email,
|
||||||
token: token,
|
token: token,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessAlert bind:this={successAlert}/>
|
<div class="scope-login">
|
||||||
<div class="wrapper-tiny">
|
<div class="bg-image"></div>
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="form">
|
||||||
|
<h2 class="mb-5">Welcome to Lucent</h2>
|
||||||
<form on:submit={login}>
|
<form on:submit={login}>
|
||||||
<div class="mb-3 text-center">
|
<button class="bt bt-primary">
|
||||||
<h3>Login as {email}</h3>
|
Enter as {email}
|
||||||
|
<img
|
||||||
</div>
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
<div class="text-center mt-5 d-block">
|
src="/img/spinner.svg"
|
||||||
<SpinnerButton label="Enter"/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="form-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle execution of a function. Especially useful for rate limiting
|
||||||
|
* execution of handlers on events like resize and scroll.
|
||||||
|
*
|
||||||
|
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
|
||||||
|
* are most useful.
|
||||||
|
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
|
||||||
|
* as-is, to `callback` when the throttled-function is executed.
|
||||||
|
* @param {object} [options] - An object to configure options.
|
||||||
|
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
|
||||||
|
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
|
||||||
|
* one final time after the last throttled-function call. (After the throttled-function has not been called for
|
||||||
|
* `delay` milliseconds, the internal counter is reset).
|
||||||
|
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
|
||||||
|
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
|
||||||
|
* callback will never executed if both noLeading = true and noTrailing = true.
|
||||||
|
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
|
||||||
|
* false (at end), schedule `callback` to execute after `delay` ms.
|
||||||
|
*
|
||||||
|
* @returns {Function} A new, throttled, function.
|
||||||
|
*/
|
||||||
|
export function throttle(delay, callback, options) {
|
||||||
|
const {
|
||||||
|
noTrailing = false,
|
||||||
|
noLeading = false,
|
||||||
|
debounceMode = undefined,
|
||||||
|
} = options || {};
|
||||||
|
/*
|
||||||
|
* After wrapper has stopped being called, this timeout ensures that
|
||||||
|
* `callback` is executed at the proper times in `throttle` and `end`
|
||||||
|
* debounce modes.
|
||||||
|
*/
|
||||||
|
let timeoutID;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Keep track of the last time `callback` was executed.
|
||||||
|
let lastExec = 0;
|
||||||
|
|
||||||
|
// Function to clear existing timeout
|
||||||
|
function clearExistingTimeout() {
|
||||||
|
if (timeoutID) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel next exec
|
||||||
|
function cancel(options) {
|
||||||
|
const { upcomingOnly = false } = options || {};
|
||||||
|
clearExistingTimeout();
|
||||||
|
cancelled = !upcomingOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The `wrapper` function encapsulates all of the throttling / debouncing
|
||||||
|
* functionality and when executed will limit the rate at which `callback`
|
||||||
|
* is executed.
|
||||||
|
*/
|
||||||
|
function wrapper(...arguments_) {
|
||||||
|
let self = this;
|
||||||
|
let elapsed = Date.now() - lastExec;
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute `callback` and update the `lastExec` timestamp.
|
||||||
|
function exec() {
|
||||||
|
lastExec = Date.now();
|
||||||
|
callback.apply(self, arguments_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `debounceMode` is true (at begin) this is used to clear the flag
|
||||||
|
* to allow future `callback` executions.
|
||||||
|
*/
|
||||||
|
function clear() {
|
||||||
|
timeoutID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noLeading && debounceMode && !timeoutID) {
|
||||||
|
/*
|
||||||
|
* Since `wrapper` is being called for the first time and
|
||||||
|
* `debounceMode` is true (at begin), execute `callback`
|
||||||
|
* and noLeading != true.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExistingTimeout();
|
||||||
|
|
||||||
|
if (debounceMode === undefined && elapsed > delay) {
|
||||||
|
if (noLeading) {
|
||||||
|
/*
|
||||||
|
* In throttle mode with noLeading, if `delay` time has
|
||||||
|
* been exceeded, update `lastExec` and schedule `callback`
|
||||||
|
* to execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
lastExec = Date.now();
|
||||||
|
if (!noTrailing) {
|
||||||
|
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
|
||||||
|
* `callback`.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
} else if (noTrailing !== true) {
|
||||||
|
/*
|
||||||
|
* In trailing throttle mode, since `delay` time has not been
|
||||||
|
* exceeded, schedule `callback` to execute `delay` ms after most
|
||||||
|
* recent execution.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is true (at begin), schedule `clear` to execute
|
||||||
|
* after `delay` ms.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is false (at end), schedule `callback` to
|
||||||
|
* execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
timeoutID = setTimeout(
|
||||||
|
debounceMode ? clear : exec,
|
||||||
|
debounceMode === undefined ? delay - elapsed : delay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.cancel = cancel;
|
||||||
|
|
||||||
|
// Return the wrapper function.
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(delay, callback, options) {
|
||||||
|
const { atBegin = false } = options || {};
|
||||||
|
return throttle(delay, callback, { debounceMode: atBegin !== false });
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext, onMount} from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let title;
|
export let title;
|
||||||
export let command;
|
export let command;
|
||||||
@@ -12,58 +11,57 @@
|
|||||||
let inProgress = false;
|
let inProgress = false;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature );
|
const eventSource = new EventSource(
|
||||||
|
channel.lucentUrl + "/command-report-source/" + command.signature,
|
||||||
|
);
|
||||||
|
|
||||||
eventSource.onmessage = function (event) {
|
eventSource.onmessage = function (event) {
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
date = data.date;
|
date = data.date;
|
||||||
logs = data.logs;
|
logs = data.logs;
|
||||||
anchorEl.scrollIntoView()
|
anchorEl.scrollIntoView();
|
||||||
}
|
};
|
||||||
eventSource.onerror = (e) => {
|
eventSource.onerror = (e) => {
|
||||||
|
console.log(e);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWebsite(e) {
|
function buildWebsite(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
|
apiPost(channel.lucentUrl + "/command/" + command.signature).then(
|
||||||
connect()
|
(response) => {
|
||||||
})
|
connect();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
connect()
|
connect();
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="common-wrapper">
|
<div class="common-wrapper">
|
||||||
<div class="lx-card mt-5">
|
<div class="lx-card mt-5">
|
||||||
|
|
||||||
<h3 class="header-small mb-5">{title}</h3>
|
<h3 class="header-small mb-5">{title}</h3>
|
||||||
|
|
||||||
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start
|
<button
|
||||||
|
on:click={buildWebsite}
|
||||||
|
class="button primary mb-3"
|
||||||
|
disabled={inProgress}
|
||||||
|
>Start
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#if inProgress}
|
{#if inProgress}
|
||||||
<span class="badge text-bg-warning">
|
<span class="badge text-bg-warning"> Action in progress </span>
|
||||||
Action in progress
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !inProgress && logs}
|
{#if !inProgress && logs}
|
||||||
<span class="badge text-bg-info">
|
<span class="badge text-bg-info"> Action completed </span>
|
||||||
Action completed
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre class="logs">{logs}
|
<pre class="logs">{logs}
|
||||||
@@ -71,8 +69,9 @@
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.logs{
|
.logs {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
background: var(--p90);
|
background: var(--p90);
|
||||||
|
|||||||
@@ -124,17 +124,10 @@
|
|||||||
"italic": {
|
"italic": {
|
||||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8.874 19 6.143-14M6 19h6.33m-.66-14H18"/>',
|
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8.874 19 6.143-14M6 19h6.33m-.66-14H18"/>',
|
||||||
viewBox: "0 0 24 24",
|
viewBox: "0 0 24 24",
|
||||||
},
|
|
||||||
"undo": {
|
|
||||||
path: '<path fill-rule="evenodd" clip-rule="evenodd" d="M7.53033 3.46967C7.82322 3.76256 7.82322 4.23744 7.53033 4.53033L5.81066 6.25H15C18.1756 6.25 20.75 8.82436 20.75 12C20.75 15.1756 18.1756 17.75 15 17.75H8.00001C7.58579 17.75 7.25001 17.4142 7.25001 17C7.25001 16.5858 7.58579 16.25 8.00001 16.25H15C17.3472 16.25 19.25 14.3472 19.25 12C19.25 9.65279 17.3472 7.75 15 7.75H5.81066L7.53033 9.46967C7.82322 9.76256 7.82322 10.2374 7.53033 10.5303C7.23744 10.8232 6.76256 10.8232 6.46967 10.5303L3.46967 7.53033C3.17678 7.23744 3.17678 6.76256 3.46967 6.46967L6.46967 3.46967C6.76256 3.17678 7.23744 3.17678 7.53033 3.46967Z" fill="#1C274C"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
},
|
|
||||||
"destroy": {
|
|
||||||
path: '<path d="M17 7L15 9" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path d="M19.5 7.5L20.5 8" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path d="M16 3.5L16.5 4.5" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path d="M19 5L20 4" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path\n'+' d="M5.75 8.00337C6.85315 7.36523 8.13392 7 9.5 7C13.6421 7 17 10.3579 17 14.5C17 18.6421 13.6421 22 9.5 22C5.35786 22 2 18.6421 2 14.5C2 13.1339 2.36523 11.8532 3.00337 10.75" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export let width = 16;
|
export let width = 16;
|
||||||
export let height = 16;
|
export let height = 16;
|
||||||
export let icon = "";
|
export let icon = "";
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button type="submit" class="button secondary btn-spinner" {disabled}>
|
<button type="submit" class="button secondary btn-spinner" {disabled}>
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let selected;
|
export let selected;
|
||||||
@@ -9,29 +9,27 @@
|
|||||||
|
|
||||||
function deleteRecords(e) {
|
function deleteRecords(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/delete", {
|
||||||
.post(channel.lucentUrl + "/records/delete", {
|
|
||||||
ids: selected.map((s) => s.id),
|
ids: selected.map((s) => s.id),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeStatus(e, status) {
|
function changeStatus(e, status) {
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/status/" + status, {
|
||||||
.post(channel.lucentUrl + "/records/status/" + status, {
|
|
||||||
schemaName: schema.name,
|
schemaName: schema.name,
|
||||||
records: selected.map((s) => s.id),
|
records: selected,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -41,28 +39,42 @@
|
|||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Publish
|
class="button"
|
||||||
</button
|
>Publish
|
||||||
>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Make Draft
|
class="button"
|
||||||
</button
|
>Make Draft
|
||||||
>
|
</button>
|
||||||
{#if filter["status_in"] === "trashed"}
|
{#if filter["status_in"] === "trashed"}
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
>Publish
|
||||||
|
</button>
|
||||||
|
{#if schema.hasDrafts}
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
>Make Draft
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={deleteRecords}
|
on:click|preventDefault={deleteRecords}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Delete forever
|
class="button"
|
||||||
</button
|
>Delete forever
|
||||||
>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
||||||
class="button">Move to trash
|
class="button"
|
||||||
</button
|
>Move to trash
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {getContext} from "svelte";
|
|
||||||
import Preview from "../files/Preview.svelte";
|
|
||||||
import {selectRecord} from "./functions/recordSelect.js";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
|
|
||||||
export let schema;
|
|
||||||
export let records;
|
|
||||||
export let isWritable;
|
|
||||||
export let selected = [];
|
|
||||||
|
|
||||||
function select(record) {
|
|
||||||
selected = selectRecord(record, selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<div class="row" style="max-width:1000px">
|
|
||||||
{#each records as record (record.id)}
|
|
||||||
<div class="col-6 col-md-4">
|
|
||||||
<div
|
|
||||||
class="file-wrapper rounded p-2 mb-4 bg-light"
|
|
||||||
class:selected={selected.includes(record)}
|
|
||||||
>
|
|
||||||
{#if isWritable}
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
on:change={() => select(record)}
|
|
||||||
class="form-check-input "
|
|
||||||
type="checkbox"
|
|
||||||
checked={selected.find(
|
|
||||||
(r) => r.id === record.id
|
|
||||||
)}
|
|
||||||
value={record}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<Preview {record} size="medium"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
title={record._file.path}
|
|
||||||
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
|
||||||
style="
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #333;
|
|
||||||
">{record._file.path}</a
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="lx-small-text text-muted d-block text-center"
|
|
||||||
>{record._file.mime}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-check {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
import Pagination from "./pagination/Pagination.svelte";
|
import Pagination from "./pagination/Pagination.svelte";
|
||||||
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
||||||
import Table from "./Table.svelte";
|
import Table from "./Table.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
const axios = getContext("axios");
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let users;
|
export let users;
|
||||||
export let records;
|
export let records;
|
||||||
@@ -26,18 +26,17 @@
|
|||||||
|
|
||||||
function refresh(e) {
|
function refresh(e) {
|
||||||
const newUrl = e.detail;
|
const newUrl = e.detail;
|
||||||
axios
|
apiGet(newUrl)
|
||||||
.get(newUrl)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
records = response.data.records;
|
records = response.records;
|
||||||
sortParam = response.data.sortParam;
|
sortParam = response.sortParam;
|
||||||
sortField = response.data.sortField;
|
sortField = response.sortField;
|
||||||
operators = response.data.operators;
|
operators = response.operators;
|
||||||
filter = response.data.filter;
|
filter = response.filter;
|
||||||
skip = response.data.skip;
|
skip = response.skip;
|
||||||
limit = response.data.limit;
|
limit = response.limit;
|
||||||
total = response.data.total;
|
total = response.total;
|
||||||
modalUrl = response.data.modalUrl;
|
modalUrl = response.modalUrl;
|
||||||
document.querySelector("dialog h3").scrollIntoView();
|
document.querySelector("dialog h3").scrollIntoView();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -47,12 +46,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="{inModal ? 'mt-0' : 'mt-5'}">
|
<div class={inModal ? "mt-0" : "mt-5"}>
|
||||||
<h3 class="header-normal mb-5 ">
|
<h3 class="header-normal mb-5">
|
||||||
{schema.label}
|
{schema.label}
|
||||||
</h3>
|
</h3>
|
||||||
{#if selected.length > 0 && !inModal && isWritable}
|
{#if selected.length > 0 && !inModal && isWritable}
|
||||||
<ActionsOnSelected {schema} {selected} {filter}/>
|
<ActionsOnSelected {schema} {selected} {filter} />
|
||||||
{:else}
|
{:else}
|
||||||
<Tools
|
<Tools
|
||||||
bind:schema
|
bind:schema
|
||||||
@@ -82,7 +81,6 @@
|
|||||||
{isWritable}
|
{isWritable}
|
||||||
bind:selected
|
bind:selected
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -94,4 +92,3 @@
|
|||||||
{modalUrl}
|
{modalUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import RenderField from "./RenderField.svelte";
|
import RenderField from "./RenderField.svelte";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import Status from "../records/Status.svelte";
|
import Status from "../records/Status.svelte";
|
||||||
import {usernameById} from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import {friendlyDate} from "../../helpers";
|
import { friendlyDate } from "../../helpers";
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let users;
|
export let users;
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
export let sortParam;
|
export let sortParam;
|
||||||
export let sortField;
|
export let sortField;
|
||||||
export let visibleColumns;
|
export let visibleColumns;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each visibleColumns as field, index}
|
{#each visibleColumns as field, index}
|
||||||
@@ -20,40 +19,48 @@
|
|||||||
class="field-ui-{field.info.name}"
|
class="field-ui-{field.info.name}"
|
||||||
class:is-sort={field.name === sortField.name}
|
class:is-sort={field.name === sortField.name}
|
||||||
>
|
>
|
||||||
<RenderField {record} {schema} {graph} {field}/>
|
<RenderField {record} {schema} {graph} {field} />
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
{#if schema.visible?.includes("status")}
|
{#if schema.visible?.includes("_sys.status")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
||||||
>
|
>
|
||||||
<Status status={record.status}/>
|
<Status status={record.status} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.createdBy")}
|
{#if schema.visible?.includes("_sys.createdBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
|
class:is-sort={"-_sys.createdBy" == sortParam ||
|
||||||
|
"_sys.createdBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/>
|
<Avatar name={usernameById(users, record.createdBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.updatedBy")}
|
{#if schema.visible?.includes("_sys.updatedBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
|
class:is-sort={"-_sys.updatedBy" == sortParam ||
|
||||||
|
"_sys.updatedBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.createdAt")}
|
{#if schema.visible?.includes("_sys.createdAt")}
|
||||||
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(record._sys.createdAt)}
|
class:is-sort={"-_sys.createdAt" == sortParam ||
|
||||||
|
"_sys.createdAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.updatedAt")}
|
{#if schema.visible?.includes("_sys.updatedAt")}
|
||||||
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(record._sys.updatedAt)}
|
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
||||||
|
"_sys.updatedAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.updatedAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
import Number from "./elements/Number.svelte";
|
import Number from "./elements/Number.svelte";
|
||||||
|
|
||||||
import Text from "./elements/Text.svelte";
|
import Text from "./elements/Text.svelte";
|
||||||
|
import Url from "./elements/Url.svelte";
|
||||||
import Date from "./elements/Date.svelte";
|
import Date from "./elements/Date.svelte";
|
||||||
import Datetime from "./elements/Datetime.svelte";
|
import Datetime from "./elements/Datetime.svelte";
|
||||||
import File from "./elements/File.svelte";
|
import File from "./elements/File.svelte";
|
||||||
|
import Uuid from "./elements/UUID.svelte";
|
||||||
import Rich from "./elements/Rich.svelte";
|
import Rich from "./elements/Rich.svelte";
|
||||||
|
|
||||||
const renderElements = {
|
const renderElements = {
|
||||||
@@ -20,8 +22,10 @@
|
|||||||
checkbox: Checkbox,
|
checkbox: Checkbox,
|
||||||
reference: Reference,
|
reference: Reference,
|
||||||
number: Number,
|
number: Number,
|
||||||
|
url: Url,
|
||||||
date: Date,
|
date: Date,
|
||||||
datetime: Datetime,
|
datetime: Datetime,
|
||||||
|
uuid: Uuid,
|
||||||
file: File,
|
file: File,
|
||||||
};
|
};
|
||||||
export let field;
|
export let field;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import RecordRow from "./RecordRow.svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import {previewTitle} from "../records/Preview";
|
import { usernameById } from "../account/users";
|
||||||
import {usernameById} from "../account/users";
|
import { getContext } from "svelte";
|
||||||
import {getContext} from "svelte";
|
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {selectRecord, toggleAll} from "./functions/recordSelect.js";
|
import { selectRecord, toggleAll } from "./functions/recordSelect.js";
|
||||||
import Checkbox from "../common/Checkbox.svelte";
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
import Preview from "../files/Preview.svelte";
|
|
||||||
import {fileurl} from "../files/imageserver.js";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -23,17 +20,19 @@
|
|||||||
export let selected = [];
|
export let selected = [];
|
||||||
|
|
||||||
function eventToggleAll(e) {
|
function eventToggleAll(e) {
|
||||||
selected = toggleAll(e, records, selected)
|
selected = toggleAll(e, records, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(record) {
|
function select(record) {
|
||||||
selected = selectRecord(record, selected)
|
selected = selectRecord(record, selected);
|
||||||
}
|
}
|
||||||
|
console.log(schema);
|
||||||
$: visibleColumns = schema.fields.filter(c => schema.visible?.includes(c.name) ?? [])
|
$: visibleColumns = schema.fields.filter(
|
||||||
|
(c) => schema.visible?.includes(c.name) ?? [],
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table mt-5 ">
|
<div class="table mt-5">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -42,10 +41,10 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value=""
|
value=""
|
||||||
on:change={eventToggleAll}
|
on:change={eventToggleAll}
|
||||||
indeterminate={selected.length > 0 && selected.length < records.length}
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < records.length}
|
||||||
checked={selected.length === records.length}
|
checked={selected.length === records.length}
|
||||||
>
|
></Checkbox>
|
||||||
</Checkbox>
|
|
||||||
</th>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -54,12 +53,13 @@
|
|||||||
class="field-ui-{field.info.name ?? field.ui}"
|
class="field-ui-{field.info.name ?? field.ui}"
|
||||||
class:is-sort={field.name === sortField.name}
|
class:is-sort={field.name === sortField.name}
|
||||||
scope="col"
|
scope="col"
|
||||||
title={field.help}
|
title={field.help}>{field.label}</th
|
||||||
>{field.label}</th
|
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField}
|
{#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
|
||||||
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th>
|
<th class:is-sort={sysField.name === sortField.name}
|
||||||
|
>{sysField.label}</th
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -68,59 +68,29 @@
|
|||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title-td">
|
<td class="title-td">
|
||||||
<div
|
<div class="title-td-contents">
|
||||||
class="title-td-contents"
|
|
||||||
>
|
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
on:change={() => select(record)}
|
on:change={() => select(record)}
|
||||||
checked={selected.find((r) => r.id === record.id)}
|
checked={selected.find(
|
||||||
|
(r) => r.id === record.id,
|
||||||
|
)}
|
||||||
value={record}
|
value={record}
|
||||||
>
|
></Checkbox>
|
||||||
</Checkbox>
|
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if record._file?.path}
|
|
||||||
<div class="file-table-row">
|
|
||||||
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if record.status === "draft"}
|
|
||||||
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
target={inModal ? "_blank" : "_self"}
|
|
||||||
>
|
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
|
||||||
</a>
|
|
||||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
|
||||||
|
|
||||||
{#if record._file.width > 0}
|
|
||||||
<span>{record._file.width + "x" + record._file.height}</span>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
href="{fileurl(channel,record)}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
target={inModal ? "_blank" : "_self"}
|
target={inModal ? "_blank" : "_self"}
|
||||||
>
|
>
|
||||||
{#if record.status === "draft"}
|
{#if record.status === "draft"}
|
||||||
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
|
<span
|
||||||
|
style="text-transform: uppercase;font-size:10px"
|
||||||
|
>{record.status}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<RecordRow
|
<RecordRow
|
||||||
@@ -134,10 +104,7 @@
|
|||||||
/>
|
/>
|
||||||
<td>
|
<td>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(
|
name={usernameById(users, record.updatedBy)}
|
||||||
users,
|
|
||||||
record._sys.updatedBy
|
|
||||||
)}
|
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -146,4 +113,3 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
<span
|
||||||
|
class="badge rounded-pill bg-primary bg-opacity-75"
|
||||||
|
style="max-width:64px; overflow:hidden; white-space: nowrap; text-overflow: ellipsis;"
|
||||||
|
title={value}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>{value}</span
|
||||||
|
>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={value} target="_blank">{value}</a>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
import { range } from "../../../helpers.js";
|
|
||||||
import NavItem from "./NavItem.svelte";
|
import NavItem from "./NavItem.svelte";
|
||||||
export let inModal;
|
export let inModal;
|
||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
@@ -11,7 +11,11 @@
|
|||||||
|
|
||||||
$: totalPages = Math.ceil(total / limit);
|
$: totalPages = Math.ceil(total / limit);
|
||||||
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
||||||
|
const range = (start, end, step = 1) =>
|
||||||
|
Array.from(
|
||||||
|
{ length: Math.ceil((end - start) / step) },
|
||||||
|
(_, i) => start + i * step,
|
||||||
|
);
|
||||||
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
||||||
return i > 0 && i <= totalPages;
|
return i > 0 && i <= totalPages;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {previewTitle} from "../../records/Preview";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
export let operators;
|
export let operators;
|
||||||
@@ -19,13 +17,10 @@
|
|||||||
isReference: key.startsWith("children"),
|
isReference: key.startsWith("children"),
|
||||||
};
|
};
|
||||||
|
|
||||||
filter = [
|
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
|
||||||
extractOperator(key),
|
(mem, fn) => fn(mem),
|
||||||
extractLabel(schema, key),
|
filter,
|
||||||
].reduce((mem, fn) => fn(mem), filter);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function extractOperator(key) {
|
function extractOperator(key) {
|
||||||
return (filter) => {
|
return (filter) => {
|
||||||
@@ -50,18 +45,16 @@
|
|||||||
const filterField = schema.fields.find((f) => f.name === fieldName);
|
const filterField = schema.fields.find((f) => f.name === fieldName);
|
||||||
filter.label = filterField?.label ?? fieldName;
|
filter.label = filterField?.label ?? fieldName;
|
||||||
return filter;
|
return filter;
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterRecord = extractFilterRecord(graph, value);
|
const filterRecord = extractFilterRecord(graph, value);
|
||||||
|
|
||||||
function extractFilterRecord(graph, value) {
|
function extractFilterRecord(graph, value) {
|
||||||
|
|
||||||
if (!filter.isReference) {
|
if (!filter.isReference) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return graph.records.find(r => r.id === value);
|
return graph.records.find((r) => r.id === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFilter(k) {
|
function removeFilter(k) {
|
||||||
@@ -78,11 +71,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="applied-filter">
|
<span class="applied-filter">
|
||||||
|
|
||||||
{#if filter.isReference && filterRecord}
|
{#if filter.isReference && filterRecord}
|
||||||
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
|
{filter.label} is {filterRecord.data.name}
|
||||||
{:else}
|
{:else}
|
||||||
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""}
|
{filter.label}
|
||||||
|
{operators.find((o) => o.name === filter.operator)?.symbol ?? ""}
|
||||||
|
{operators.find((o) => o.name === filter.operator)?.hasValue
|
||||||
|
? value
|
||||||
|
: ""}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -90,7 +86,6 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="button-text"
|
class="button-text"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
><Icon width={12} height={12} icon="close"></Icon></button>
|
><Icon width={12} height={12} icon="close"></Icon></button
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {debounce} from "../../../debounce.js";
|
import { apiGet, debounce } from "../../../helpers";
|
||||||
import {previewTitle} from "../../records/Preview";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -10,13 +8,11 @@
|
|||||||
export let value = "";
|
export let value = "";
|
||||||
export let field;
|
export let field;
|
||||||
|
|
||||||
let search = ""
|
let search = "";
|
||||||
$: searchOptions = []
|
$: searchOptions = [];
|
||||||
|
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/records/suggestions", {
|
||||||
.get(channel.lucentUrl + "/records/suggestions", {
|
|
||||||
params: {
|
params: {
|
||||||
schema: field.collections[0],
|
schema: field.collections[0],
|
||||||
field: "search",
|
field: "search",
|
||||||
@@ -25,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = response.data;
|
searchOptions = response;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
@@ -35,24 +31,22 @@
|
|||||||
|
|
||||||
function apply(e, newOption) {
|
function apply(e, newOption) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
value = newOption.id
|
value = newOption.id;
|
||||||
dispatch("addFilter");
|
dispatch("addFilter");
|
||||||
value = ""
|
value = "";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="reference-tags">
|
<div class="reference-tags">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
on:keyup={updateResults}
|
on:keyup={updateResults}
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
placeholder={"Search for "+field.label}
|
placeholder={"Search for " + field.label}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reference-tags-results">
|
<div class="reference-tags-results">
|
||||||
|
|
||||||
{#if searchOptions}
|
{#if searchOptions}
|
||||||
{#each searchOptions as option (option.id)}
|
{#each searchOptions as option (option.id)}
|
||||||
<div
|
<div
|
||||||
@@ -62,19 +56,11 @@
|
|||||||
on:click={(e) => apply(e, option)}
|
on:click={(e) => apply(e, option)}
|
||||||
on:keypress={(e) => apply(e, option)}
|
on:keypress={(e) => apply(e, option)}
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, option)}
|
{option.data.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="start-typing">Start typing...</div>
|
||||||
class="start-typing">
|
|
||||||
Start typing...
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import FilterFields from "./FilterFields.svelte";
|
import FilterFields from "./FilterFields.svelte";
|
||||||
import Uploader from "../../files/Uploader.svelte";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import SortFields from "./SortFields.svelte";
|
import SortFields from "./SortFields.svelte";
|
||||||
import AppliedFilter from "./AppliedFilter.svelte";
|
import AppliedFilter from "./AppliedFilter.svelte";
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
|
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
|
||||||
|
|
||||||
@@ -41,17 +40,11 @@
|
|||||||
} else {
|
} else {
|
||||||
window.location = url;
|
window.location = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadComplete(e) {
|
|
||||||
records = e.detail;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
|
|
||||||
<SortFields
|
<SortFields
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
@@ -62,7 +55,6 @@
|
|||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FilterFields
|
<FilterFields
|
||||||
bind:schema
|
bind:schema
|
||||||
{systemFields}
|
{systemFields}
|
||||||
@@ -74,15 +66,17 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<form method="GET" on:submit={search}>
|
<form method="GET" on:submit={search}>
|
||||||
<input type="search" name="filter[search_regex]" placeholder="Search"
|
<input
|
||||||
class="search" required>
|
type="search"
|
||||||
|
name="filter[search_regex]"
|
||||||
|
placeholder="Search"
|
||||||
|
class="search"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items: center;gap:4px">
|
<div style="display:flex;align-items: center;gap:4px">
|
||||||
{#if schema.type === "collection"}
|
|
||||||
{#if !inModal && isWritable}
|
{#if !inModal && isWritable}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||||
@@ -91,15 +85,11 @@
|
|||||||
New Record
|
New Record
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else }
|
|
||||||
<div>
|
|
||||||
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if !inModal}
|
{#if !inModal}
|
||||||
<Dropdown orientation="right">
|
<Dropdown orientation="right">
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
<Icon icon="ellipsis-vertical"/>
|
<Icon icon="ellipsis-vertical" />
|
||||||
</div>
|
</div>
|
||||||
{#if filter["status_in"] === "trashed"}
|
{#if filter["status_in"] === "trashed"}
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
@@ -111,12 +101,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href={csvUrl}
|
|
||||||
>Export to CSV</a
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
||||||
@@ -129,17 +114,12 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="applied-filters">
|
<div class="applied-filters">
|
||||||
<AppliedFilterNotLinked
|
<AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
|
||||||
{inModal}
|
|
||||||
{modalUrl}
|
|
||||||
on:refresh
|
|
||||||
></AppliedFilterNotLinked>
|
></AppliedFilterNotLinked>
|
||||||
{#if Object.entries(filter).length > 0}
|
{#if Object.entries(filter).length > 0}
|
||||||
{#each Object.entries(filter) as [k, v]}
|
{#each Object.entries(filter) as [k, v]}
|
||||||
@@ -156,5 +136,3 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import Index from "../content/Index.svelte";
|
import Index from "../content/Index.svelte";
|
||||||
import axios from "axios";
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
let dialogEl;
|
let dialogEl;
|
||||||
|
|
||||||
@@ -15,21 +14,19 @@
|
|||||||
// load();
|
// load();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
export function close(e) {
|
export function close(e) {
|
||||||
if(e){
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogEl.close()
|
dialogEl.close();
|
||||||
selectedRecords = [];
|
selectedRecords = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(schema) {
|
function load(schema) {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/content/" + schema)
|
||||||
.get(channel.lucentUrl + "/content/" + schema)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
data = response.data;
|
data = response;
|
||||||
})
|
})
|
||||||
.catch((error) => console.log(error));
|
.catch((error) => console.log(error));
|
||||||
}
|
}
|
||||||
@@ -52,12 +49,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function open(schema) {
|
export function open(schema) {
|
||||||
dialogEl.showModal()
|
dialogEl.showModal();
|
||||||
load(schema);
|
load(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog bind:this={dialogEl}>
|
<dialog bind:this={dialogEl}>
|
||||||
{#if data.schema}
|
{#if data.schema}
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
@@ -96,6 +92,5 @@
|
|||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<Index {...data} bind:selected={selectedRecords}></Index>
|
<Index {...data} bind:selected={selectedRecords}></Index>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import FileIndex from "./FileIndex.svelte";
|
||||||
|
|
||||||
|
let dialogEl;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const channel = getContext("channel");
|
||||||
|
$: files = [];
|
||||||
|
$: selectedRecords = [];
|
||||||
|
// onMount(() => {
|
||||||
|
// load();
|
||||||
|
// });
|
||||||
|
|
||||||
|
export function close(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogEl.close();
|
||||||
|
selectedRecords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(recordId) {
|
||||||
|
fetch(channel.lucentUrl + "/records/files/?recordId=" + recordId)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
files = json;
|
||||||
|
})
|
||||||
|
.catch((error) => console.log(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("insert_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("replace_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function open(recordId) {
|
||||||
|
dialogEl.showModal();
|
||||||
|
load(recordId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog bind:this={dialogEl}>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={insert}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Insert
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={replace}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
|
{#if selectedRecords.length > 0}
|
||||||
|
<span class="">
|
||||||
|
{selectedRecords.length} records selected
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={close}
|
||||||
|
type="button"
|
||||||
|
class="button close"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<Icon icon="close"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<FileIndex {files} bind:selected={selectedRecords}></FileIndex>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import { fileurl } from "../files/imageserver";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
export let files = [];
|
||||||
|
export let selected = [];
|
||||||
|
export let isWritable = true;
|
||||||
|
|
||||||
|
function eventToggleAll(e) {
|
||||||
|
selected = toggleAll(e, files, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(file) {
|
||||||
|
selected = selectFile(file, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleAll = (e, files, selected) => {
|
||||||
|
if (selected.length === files.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
e.currentTarget.checked = selected.length > 0;
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectFile = (file, selected) => {
|
||||||
|
let fileExists = selected.find((r) => r.id === file.id);
|
||||||
|
if (fileExists) {
|
||||||
|
return selected.filter((r) => r.id !== file.id);
|
||||||
|
}
|
||||||
|
return [...selected, file];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table mt-5">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#if isWritable}
|
||||||
|
<th>
|
||||||
|
<Checkbox
|
||||||
|
value=""
|
||||||
|
on:change={eventToggleAll}
|
||||||
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < files.length}
|
||||||
|
checked={selected.length === files.length}
|
||||||
|
></Checkbox>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each files as file (file.id)}
|
||||||
|
<tr>
|
||||||
|
<td class="title-td">
|
||||||
|
<div class="title-td-contents">
|
||||||
|
{#if isWritable}
|
||||||
|
<Checkbox
|
||||||
|
on:change={() => select(file)}
|
||||||
|
checked={selected.find(
|
||||||
|
(s) => s.id === file.id,
|
||||||
|
)}
|
||||||
|
value={file}
|
||||||
|
></Checkbox>
|
||||||
|
{/if}
|
||||||
|
<div class="file-table-row">
|
||||||
|
<Preview
|
||||||
|
{file}
|
||||||
|
size={file.width > 0 ? "medium" : "small"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{file.filename}
|
||||||
|
<span
|
||||||
|
>{(file.size / 1024).toFixed(1)}kB</span
|
||||||
|
>
|
||||||
|
|
||||||
|
{#if file.width > 0}
|
||||||
|
<span
|
||||||
|
>{file.width +
|
||||||
|
"x" +
|
||||||
|
file.height}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<a
|
||||||
|
href={fileurl(channel, file)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function sortByField(from, to, edges, fieldName, references) {
|
export function sortByField(from, to, edges, fieldName, references) {
|
||||||
if (from === to) {
|
if (from === to) {
|
||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
let referenceIds = references.map(r => r.record.id);
|
let referenceIds = references.map((r) => r.id);
|
||||||
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? [];
|
let edgesTosort =
|
||||||
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
edges?.filter(
|
||||||
|
(ed) =>
|
||||||
|
ed.field === fieldName &&
|
||||||
|
ed.depth === 1 &&
|
||||||
|
referenceIds.includes(ed.target),
|
||||||
|
) ?? [];
|
||||||
|
let remainingEdge =
|
||||||
|
edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
||||||
|
|
||||||
edgesTosort = array_move(edgesTosort,from, to);
|
edgesTosort = array_move(edgesTosort, from, to);
|
||||||
return [...remainingEdge, ...edgesTosort];
|
return [...remainingEdge, ...edgesTosort];
|
||||||
}
|
}
|
||||||
|
|
||||||
function array_move(arr, old_index, new_index) {
|
export function array_move(arr, old_index, new_index) {
|
||||||
if (new_index >= arr.length) {
|
if (new_index >= arr.length) {
|
||||||
var k = new_index - arr.length + 1;
|
var k = new_index - arr.length + 1;
|
||||||
while (k--) {
|
while (k--) {
|
||||||
@@ -22,4 +26,4 @@ function array_move(arr, old_index, new_index) {
|
|||||||
}
|
}
|
||||||
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
||||||
return arr; // for testing
|
return arr; // for testing
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import {imgurl} from "./imageserver.js";
|
import { imgurl } from "./imageserver.js";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export let record;
|
export let file;
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let size = "small";
|
export let size = "small";
|
||||||
export let showFilename = false;
|
export let showFilename = false;
|
||||||
let imageSide;
|
let imageSide;
|
||||||
let fileSide;
|
let fileSide;
|
||||||
let fontSize;
|
let fontSize;
|
||||||
|
|
||||||
|
console.log({ channel });
|
||||||
if (size == "large") {
|
if (size == "large") {
|
||||||
imageSide = 256;
|
imageSide = 256;
|
||||||
fileSide = 32;
|
fileSide = 32;
|
||||||
@@ -28,50 +30,49 @@
|
|||||||
fontSize = "13";
|
fontSize = "13";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<div style="display: flex;align-items: center;gap: 5px;">
|
|
||||||
{#if record}
|
|
||||||
|
|
||||||
{#if record._file.mime.startsWith("image")}
|
<div style="display: flex;align-items: center;gap: 5px;">
|
||||||
|
{#if file}
|
||||||
|
{#if file.mime.startsWith("image")}
|
||||||
<!-- href={imgurl(record)} -->
|
<!-- href={imgurl(record)} -->
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.originalName}
|
title={file.filename}
|
||||||
style="width:{imageSide}px;height:{imageSide}px"
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="rounded w-100"
|
class="rounded w-100"
|
||||||
src={imgurl(channel,record)}
|
src={imgurl(channel, file)}
|
||||||
alt={record._file.path}
|
alt={file.path}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.path}
|
title={file.path}
|
||||||
class="file-preview-small"
|
class="file-preview-small"
|
||||||
style="width:{imageSide}px;height:{imageSide}px"
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
>
|
>
|
||||||
<Icon icon="file" width={fileSide} height={fileSide}/>
|
<Icon icon="file" width={fileSide} height={fileSide} />
|
||||||
<span class="ms-2"
|
<span class="ms-2"
|
||||||
>.{record._file.path.split(".").pop().toLowerCase()}</span
|
>.{file.path.split(".").pop().toLowerCase()}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if showFilename}
|
{#if showFilename}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.path}
|
title={file.path}
|
||||||
class="preview-file-filename lx-small-text text-decoration-none"
|
class="preview-file-filename lx-small-text text-decoration-none"
|
||||||
>{record._file.path} </a
|
>{file.path}
|
||||||
>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
img{
|
img {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let recordId;
|
||||||
let mimeTypes = "";
|
let mimeTypes = "";
|
||||||
let files = [];
|
let files = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
@@ -17,34 +17,34 @@
|
|||||||
files = e.target.files ? [...e.target.files] : [];
|
files = e.target.files ? [...e.target.files] : [];
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
formData.append("schema", schema.name);
|
formData.append("recordId", recordId);
|
||||||
Array.from(files).forEach(function (file) {
|
Array.from(files).forEach(function (file) {
|
||||||
formData.append("files[]", file);
|
formData.append("files[]", file);
|
||||||
});
|
});
|
||||||
dispatch("beforeUpload", files);
|
dispatch("beforeUpload", files);
|
||||||
axios
|
fetch(channel.lucentUrl + "/files/upload", {
|
||||||
.post(channel.lucentUrl + "/files/upload", formData, {
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"X-CSRF-TOKEN": document.querySelector(
|
||||||
|
'meta[name="csrf-token"]',
|
||||||
|
).content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response.json())
|
||||||
if (response.data.error) {
|
.then((data) => {
|
||||||
dispatch("uploadError", response.data.error);
|
if (data.error) {
|
||||||
|
dispatch("uploadError", data.error);
|
||||||
} else {
|
} else {
|
||||||
dispatch("uploadComplete", response.data);
|
dispatch("uploadComplete", data);
|
||||||
}
|
}
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
isLoading = false;
|
|
||||||
console.log(error.response.data);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset class="upload-button" disabled={isLoading}>
|
<fieldset class="upload-button" disabled={isLoading}>
|
||||||
<label class="button primary btn-spinner ">
|
<label class="button primary btn-spinner">
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
export function imgurl(channel, record) {
|
export function imgurl(channel, file) {
|
||||||
if (record._file.mime === "image/svg+xml") {
|
if (file.mime === "image/svg+xml") {
|
||||||
return fileurl(channel, record);
|
return fileurl(channel, file);
|
||||||
}
|
}
|
||||||
const pathAr = record._file.path.split("/");
|
return channel.filesUrl + `/thumbs/${file.path}`;
|
||||||
return channel.disks[record._file.disk] + `/${pathAr[0]}/thumbs/${pathAr[1]}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileurl(channel, record) {
|
export function fileurl(channel, file) {
|
||||||
return channel.disks[record._file.disk] + `/${record._file.path}`;
|
return channel.filesUrl + `/${file.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlurl(channel, record, preset) {
|
export function htmlurl(channel, file, preset) {
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let url = fileurl(channel, record)
|
let url = fileurl(channel, file);
|
||||||
|
|
||||||
if (record._file.width > 0) {
|
if (file.width > 0) {
|
||||||
let presetUrl = url;
|
let presetUrl = url;
|
||||||
if (preset) {
|
if (preset) {
|
||||||
const pathAr = record._file.path.split("/");
|
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
|
||||||
presetUrl = channel.disks[record._file.disk] + `/${pathAr[0]}/templates/${preset}/${pathAr[1]}`;
|
|
||||||
}
|
}
|
||||||
html = `<img src="${presetUrl}" alt="${record._file.path}" />`
|
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
||||||
} else if (record._file.mime === "image/svg+xml") {
|
} else if (file.mime === "image/svg+xml") {
|
||||||
html = `<img src="${url}" alt="${record._file.path}"/>`
|
html = `<img src="${url}" alt="${file.path}"/>`;
|
||||||
} else {
|
} else {
|
||||||
html = `<a href="${url}">${record._file.originalName}</a>`
|
html = `<a href="${url}">${file.originalName}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { getContext, onMount } from "svelte";
|
||||||
import {getContext, onMount} from "svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import RecordRow from "./RecordRow.svelte"
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let records = [];
|
let records = [];
|
||||||
let graph = null;
|
let graph = null;
|
||||||
let users = [];
|
let users = [];
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/home/records")
|
||||||
.get(channel.lucentUrl + "/home/records")
|
.then((data) => {
|
||||||
.then((response) => {
|
records = data.records;
|
||||||
records = response.data.records;
|
users = data.users;
|
||||||
graph = response.data.graph;
|
|
||||||
users = response.data.users;
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -21,21 +19,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
||||||
{#if records.length > 0}
|
{#if records.length > 0}
|
||||||
|
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<table class="">
|
<table class="">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<RecordRow {graph} {record} {users}/>
|
<RecordRow {record} {users} />
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import {formatDistanceToNow, parseJSON} from "date-fns";
|
import { formatDistanceToNow, parseJSON } from "date-fns";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {previewTitle} from "../records/Preview";
|
|
||||||
import Preview from "../files/Preview.svelte";
|
import Preview from "../files/Preview.svelte";
|
||||||
import {usernameById} from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let users;
|
export let users;
|
||||||
export let graph;
|
|
||||||
export let record;
|
export let record;
|
||||||
let schema = channel.schemas.find((s) => s.name === record.schema);
|
let schema = channel.schemas.find((s) => s.name === record.schema);
|
||||||
let frieldlyUpdatedAt = formatDistanceToNow(
|
let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.updatedAt), {
|
||||||
parseJSON(record._sys.updatedAt),
|
addSuffix: true,
|
||||||
{addSuffix: true}
|
});
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@@ -23,26 +20,19 @@
|
|||||||
<span class="status">DRAFT</span>
|
<span class="status">DRAFT</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.type === "files"}
|
{#if schema.type === "files"}
|
||||||
<Preview {record} size="tiny" showFilename={true}/>
|
<Preview {record} size="tiny" showFilename={true} />
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a href="{channel.lucentUrl}/records/{record.id}">
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
{record.data.name}
|
||||||
|
|
||||||
>
|
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><a
|
<td><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
|
||||||
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex;gap: 14px">
|
<div style="display: flex;gap: 14px">
|
||||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
{frieldlyUpdatedAt}
|
{frieldlyUpdatedAt}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {getContext} from "svelte";
|
|
||||||
import Icon from "../common/Icon.svelte";
|
|
||||||
import Folder from "./Folder.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
export let folder;
|
|
||||||
export let schema;
|
|
||||||
export let expanded = folder.shouldExpand;
|
|
||||||
|
|
||||||
function toggleExpand() {
|
|
||||||
expanded = !expanded;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<div class="sidebar-folder">
|
|
||||||
{#if folder.name !== ""}
|
|
||||||
<button class="sidebar-header" tabindex="0" on:click={toggleExpand}>
|
|
||||||
{folder.name.replaceAll("_", " ") ?? "Main"}
|
|
||||||
{#if expanded}
|
|
||||||
<Icon icon="circle-chevron-up"></Icon>
|
|
||||||
{:else}
|
|
||||||
<Icon icon="circle-chevron-down"></Icon>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{#if expanded}
|
|
||||||
{#each folder.folders as aFolder}
|
|
||||||
<Folder folder={aFolder} schema={schema}></Folder>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#each folder.files as aSchema}
|
|
||||||
<a class="sidebar-item" class:active={aSchema.name === schema?.name}
|
|
||||||
aria-current="page"
|
|
||||||
href="{channel.lucentUrl}/content/{aSchema.name}">{aSchema.label}</a>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,28 +1,38 @@
|
|||||||
<script>
|
<script>
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Dropdown from "../common/Dropdown.svelte";
|
import Dropdown from "../common/Dropdown.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="top-nav">
|
||||||
<div class="top-nav ">
|
{#if channel.auth == "lucent"}
|
||||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||||
|
{/if}
|
||||||
{#if channel.commands.length > 0}
|
{#if channel.commands.length > 0}
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">Actions</div>
|
<div slot="button">Actions</div>
|
||||||
{#each channel.commands as command}
|
{#each channel.commands as command}
|
||||||
<a href="{channel.lucentUrl}/command-report/{command.signature}" class="top-nav-item">{command.name}</a>
|
<a
|
||||||
|
href="{channel.lucentUrl}/command-report/{command.signature}"
|
||||||
|
class="top-nav-item">{command.name}</a
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- <div>-->
|
||||||
|
<!-- <form method="GET">-->
|
||||||
|
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
|
||||||
|
<!-- class="form-control" required/>-->
|
||||||
|
<!-- </form>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
{#if channel.auth == "lucent"}
|
||||||
<a href="{channel.lucentUrl}/profile">
|
<a href="{channel.lucentUrl}/profile">
|
||||||
<Avatar side="28" name={user.name}/>
|
<Avatar side="28" name={user.name} />
|
||||||
</a>
|
</a>
|
||||||
|
{:else}
|
||||||
|
<Avatar side="28" name={user.name} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Folder from "./Folder.svelte";
|
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const readableSchemas = getContext("readableSchemas");
|
const readableSchemas = getContext("readableSchemas");
|
||||||
|
|
||||||
function addToFolder(tree, folderPath, aSchema) {
|
|
||||||
let shouldExpand = aSchema.name === schema?.name;
|
|
||||||
if (folderPath === "") {
|
|
||||||
tree.files.push(aSchema)
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
const folderNames = folderPath.split(".");
|
|
||||||
folderNames.forEach(folderName => {
|
|
||||||
let queriedFolder = tree.folders.find(folder => folder.name === folderName)
|
|
||||||
if (!queriedFolder) {
|
|
||||||
queriedFolder = {name: folderName, files: [], folders: [], shouldExpand: shouldExpand};
|
|
||||||
}
|
|
||||||
folderNames.shift()
|
|
||||||
let remainingFolderPath = folderNames.join(".");
|
|
||||||
queriedFolder = addToFolder(queriedFolder, remainingFolderPath, aSchema)
|
|
||||||
|
|
||||||
tree.folders = tree.folders.filter(f => f.name !== queriedFolder.name)
|
|
||||||
tree.folders.push(queriedFolder);
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return tree;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaTree = readableSchemas.reduce((carry, schema) => {
|
|
||||||
carry = addToFolder(carry, schema.folder,schema)
|
|
||||||
return carry;
|
|
||||||
}, {name: "", files: [], folders: [], shouldExpand:true});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sidebar-top">
|
<div class="sidebar-top">
|
||||||
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
|
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
|
||||||
<a class="nav-item" href="{channel.lucentUrl}/profile">
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<Folder folder={schemaTree} {schema} ></Folder>
|
{#each readableSchemas as aschema}
|
||||||
|
<a
|
||||||
|
class="sidebar-item"
|
||||||
|
class:active={aschema.name === schema?.name}
|
||||||
|
aria-current="page"
|
||||||
|
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
||||||
import {onDestroy, onMount} from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import {basicSetup, EditorView} from "codemirror";
|
import { basicSetup, EditorView } from "codemirror";
|
||||||
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
|
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||||
import {Compartment, EditorState} from "@codemirror/state";
|
import { Compartment, EditorState } from "@codemirror/state";
|
||||||
import {keymap} from "@codemirror/view";
|
import { keymap } from "@codemirror/view";
|
||||||
import {indentWithTab} from "@codemirror/commands";
|
import { indentWithTab } from "@codemirror/commands";
|
||||||
import {markdown} from "@codemirror/lang-markdown";
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
import {lintKeymap} from "@codemirror/lint";
|
import { lintKeymap } from "@codemirror/lint";
|
||||||
|
|
||||||
let parentElement;
|
let parentElement;
|
||||||
let codeMirrorView;
|
let codeMirrorView;
|
||||||
@@ -17,10 +16,10 @@
|
|||||||
|
|
||||||
export function insertMedia(info) {
|
export function insertMedia(info) {
|
||||||
let insertText = "";
|
let insertText = "";
|
||||||
if (info.record._file.width > 0) {
|
if (info.file.width > 0) {
|
||||||
insertText = ``;
|
insertText = ``;
|
||||||
} else {
|
} else {
|
||||||
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
|
insertText = `[${info.file.filename}](${info.originalUrl})`;
|
||||||
}
|
}
|
||||||
const cursor = codeMirrorView.state.selection.main.head;
|
const cursor = codeMirrorView.state.selection.main.head;
|
||||||
const transaction = codeMirrorView.state.update({
|
const transaction = codeMirrorView.state.update({
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
insert: insertText,
|
insert: insertText,
|
||||||
},
|
},
|
||||||
// the next 2 lines will set the appropriate cursor position after inserting the new text.
|
// the next 2 lines will set the appropriate cursor position after inserting the new text.
|
||||||
selection: {anchor: cursor + 1},
|
selection: { anchor: cursor + 1 },
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,11 +45,7 @@
|
|||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
keymap.of([
|
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
||||||
indentWithTab,
|
|
||||||
...lintKeymap,
|
|
||||||
...completionKeymap
|
|
||||||
]),
|
|
||||||
language.of(markdown()),
|
language.of(markdown()),
|
||||||
markdown(),
|
markdown(),
|
||||||
autocompletion(),
|
autocompletion(),
|
||||||
@@ -63,17 +58,14 @@
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.contentAttributes.of({spellcheck: "true"})
|
EditorView.contentAttributes.of({ spellcheck: "true" }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
codeMirrorView = new EditorView({
|
codeMirrorView = new EditorView({
|
||||||
state,
|
state,
|
||||||
parent: parentElement,
|
parent: parentElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -83,4 +75,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="is-editable-{editable}" bind:this={parentElement}/>
|
<div class="is-editable-{editable}" bind:this={parentElement} />
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {onDestroy, onMount} from "svelte";
|
|
||||||
|
|
||||||
import tinymce from "tinymce/tinymce";
|
|
||||||
import "tinymce/models/dom";
|
|
||||||
import "tinymce/icons/default";
|
|
||||||
import "tinymce/themes/silver";
|
|
||||||
import "tinymce/skins/ui/oxide/skin.css";
|
|
||||||
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css?inline";
|
|
||||||
import customcss from "./tinymce.css?inline";
|
|
||||||
|
|
||||||
import "tinymce/plugins/link";
|
|
||||||
import "tinymce/plugins/code";
|
|
||||||
import "tinymce/plugins/image";
|
|
||||||
import "tinymce/plugins/table";
|
|
||||||
import "tinymce/plugins/codesample";
|
|
||||||
import "tinymce/plugins/media";
|
|
||||||
|
|
||||||
import "tinymce/plugins/lists";
|
|
||||||
import "tinymce/plugins/autoresize";
|
|
||||||
import "tinymce/plugins/wordcount";
|
|
||||||
|
|
||||||
export let value = "";
|
|
||||||
export let additionalConfig = {};
|
|
||||||
let lastVal = "";
|
|
||||||
let textareaEl;
|
|
||||||
let activeEditor;
|
|
||||||
let editorWrapper;
|
|
||||||
const plugins = [
|
|
||||||
"autoresize",
|
|
||||||
"code",
|
|
||||||
"image",
|
|
||||||
"table",
|
|
||||||
"codesample",
|
|
||||||
"link",
|
|
||||||
"lists",
|
|
||||||
"media",
|
|
||||||
"wordcount",
|
|
||||||
];
|
|
||||||
const toolbar =
|
|
||||||
"bold italic underline strikethrough removeformat | link | subscript superscript bullist numlist media image codesample table code wordcount blockquote indent outdent blocks";
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (activeEditor) {
|
|
||||||
activeEditor.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const config = {
|
|
||||||
target: textareaEl,
|
|
||||||
toolbar_mode: "sliding",
|
|
||||||
toolbar_sticky: true,
|
|
||||||
skin: false,
|
|
||||||
content_css: false,
|
|
||||||
content_style: contentUiSkinCss.toString() + customcss.toString(),
|
|
||||||
branding: false,
|
|
||||||
inline: false,
|
|
||||||
plugins: plugins,
|
|
||||||
contextmenu: false,
|
|
||||||
menubar: false,
|
|
||||||
statusbar: false,
|
|
||||||
entity_encoding: "raw",
|
|
||||||
convert_urls: false,
|
|
||||||
toolbar: toolbar,
|
|
||||||
image_caption: true,
|
|
||||||
relative_urls: false,
|
|
||||||
browser_spellcheck: true,
|
|
||||||
max_height: 600,
|
|
||||||
// media_poster: false,
|
|
||||||
|
|
||||||
setup: function (editor) {
|
|
||||||
activeEditor = editor;
|
|
||||||
|
|
||||||
editor.on("init", function (e) {
|
|
||||||
editor.setContent(value ?? "");
|
|
||||||
});
|
|
||||||
|
|
||||||
// editor.on("blur", function (e) {
|
|
||||||
// let content = setImageDimensions(editor.getContent());
|
|
||||||
// dispatch("editorBlur", content);
|
|
||||||
// editorWrapper.classList.remove("editorFocus");
|
|
||||||
// // return false;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// editor.on("focus", function (e) {
|
|
||||||
// editorWrapper.classList.add("editorFocus");
|
|
||||||
// // return false;
|
|
||||||
// });
|
|
||||||
|
|
||||||
editor.on("change input undo redo", function (e) {
|
|
||||||
lastVal = editor.getContent();
|
|
||||||
if (lastVal !== value) {
|
|
||||||
value = lastVal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
tinymce.init({...config, ...additionalConfig});
|
|
||||||
});
|
|
||||||
|
|
||||||
export function insertMedia(info){
|
|
||||||
activeEditor.execCommand('InsertHTML', false, info.html);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={editorWrapper} class="tox-wrapper">
|
|
||||||
<div class="form-control" bind:this={textareaEl}>
|
|
||||||
{@html value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(.tox:not(.tox-tinymce-inline) .tox-editor-header) {
|
|
||||||
background-color: #fff;
|
|
||||||
border-bottom: 1px solid #ced4da;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 4px 0;
|
|
||||||
transition: box-shadow 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.tox-tinymce) {
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<script>
|
||||||
|
import {onDestroy, onMount} from 'svelte';
|
||||||
|
import {Editor} from '@tiptap/core'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
|
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import Heading from '@tiptap/extension-heading'
|
||||||
|
import HardBreak from '@tiptap/extension-hard-break'
|
||||||
|
import Blockquote from '@tiptap/extension-blockquote';
|
||||||
|
import CodeBlock from '@tiptap/extension-code-block';
|
||||||
|
import Bold from '@tiptap/extension-bold';
|
||||||
|
import BulletList from '@tiptap/extension-bullet-list';
|
||||||
|
import Code from '@tiptap/extension-code';
|
||||||
|
import History from '@tiptap/extension-history';
|
||||||
|
import Italic from '@tiptap/extension-italic';
|
||||||
|
import ListItem from '@tiptap/extension-list-item';
|
||||||
|
import OrderedList from '@tiptap/extension-ordered-list';
|
||||||
|
import Strike from '@tiptap/extension-strike';
|
||||||
|
import Table from '@tiptap/extension-table';
|
||||||
|
import TableRow from '@tiptap/extension-table-row';
|
||||||
|
import TableCell from '@tiptap/extension-table-cell';
|
||||||
|
import TableHeader from '@tiptap/extension-table-header';
|
||||||
|
import Underline from '@tiptap/extension-underline';
|
||||||
|
import Image from '@tiptap/extension-image';
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
let element;
|
||||||
|
let editor;
|
||||||
|
export let value = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editor = new Editor({
|
||||||
|
element: element,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Bold,
|
||||||
|
ListItem,
|
||||||
|
BulletList,
|
||||||
|
Code,
|
||||||
|
CodeBlock,
|
||||||
|
History,
|
||||||
|
Italic,
|
||||||
|
HardBreak,
|
||||||
|
OrderedList,
|
||||||
|
Strike,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
Underline,
|
||||||
|
Dropcursor,
|
||||||
|
Image,
|
||||||
|
Heading.configure({
|
||||||
|
levels: [1, 2, 3],
|
||||||
|
}),
|
||||||
|
Blockquote
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
editable: true,
|
||||||
|
onTransaction: () => {
|
||||||
|
// force re-render so `editor.isActive` works as expected
|
||||||
|
editor = editor;
|
||||||
|
},
|
||||||
|
onUpdate: ({editor}) => {
|
||||||
|
value = editor.getHTML()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function insertMedia(info){
|
||||||
|
editor.chain().focus().setImage({ src: info.url }).run()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if editor}
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
class:active={editor.isActive('heading', { level: 1 })}
|
||||||
|
>
|
||||||
|
H1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
class:active={editor.isActive('heading', { level: 2 })}
|
||||||
|
>
|
||||||
|
H2
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
class:active={editor.isActive('bold')}
|
||||||
|
>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
class:active={editor.isActive('italic')}
|
||||||
|
>
|
||||||
|
<em>IT</em>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
class:active={editor.isActive('underline')}
|
||||||
|
>
|
||||||
|
<u>U</u>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
class:active={editor.isActive('strike')}
|
||||||
|
>
|
||||||
|
<s>S</s>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.commands.unsetAllMarks()}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
class:active={editor.isActive('code')}
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
class:active={editor.isActive('bulletList')}
|
||||||
|
>
|
||||||
|
<Icon icon="list"></Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
class:active={editor.isActive('orderedList')}
|
||||||
|
>
|
||||||
|
<Icon icon="ordered-list"></Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
class:active={editor.isActive('blockquote')}
|
||||||
|
>
|
||||||
|
""
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
class:active={editor.isActive('codeBlock')}
|
||||||
|
>
|
||||||
|
cb
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div bind:this={element} class="content"/>
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onDestroy, onMount} from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import Trix from "trix"
|
import Trix from "trix";
|
||||||
|
import "trix/dist/trix.css";
|
||||||
|
|
||||||
export let value = "";
|
export let value = "";
|
||||||
export let field;
|
export let field;
|
||||||
let editor;
|
let editor;
|
||||||
|
|
||||||
|
|
||||||
function updateValue(e) {
|
function updateValue(e) {
|
||||||
value = e.target.value;
|
value = e.target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertMedia(info){
|
export function insertMedia(info) {
|
||||||
if(info.record._file.width > 0){
|
if (info.file.width > 0) {
|
||||||
var attachment = new Trix.Attachment({ content: info.html })
|
var attachment = new Trix.Attachment({ content: info.html });
|
||||||
editor.editor.insertAttachment(attachment)
|
editor.editor.insertAttachment(attachment);
|
||||||
}else{
|
} else {
|
||||||
editor.editor.insertHTML(`<a href="${info.originalUrl}">${info.record._file.originalName}</a>`)
|
editor.editor.insertHTML(
|
||||||
|
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor.addEventListener("trix-file-accept", (e) => {
|
editor.addEventListener("trix-file-accept", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
})
|
});
|
||||||
|
|
||||||
editor.addEventListener("trix-before-initialize", (e) => {
|
editor.addEventListener("trix-before-initialize", (e) => {
|
||||||
Trix.config.blockAttributes.heading1.tagName = 'h2';
|
Trix.config.blockAttributes.heading1.tagName = "h2";
|
||||||
const { toolbarElement } = e.target
|
const { toolbarElement } = e.target;
|
||||||
const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]")
|
const h1Button = toolbarElement.querySelector(
|
||||||
h1Button.insertAdjacentHTML("afterend", `<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`)
|
"[data-trix-attribute=heading1]",
|
||||||
})
|
);
|
||||||
|
h1Button.insertAdjacentHTML(
|
||||||
|
"afterend",
|
||||||
})
|
`<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
// onDestroy(() => {
|
// onDestroy(() => {
|
||||||
// editor.removeEventListener("trix-before-initialize")
|
// editor.removeEventListener("trix-before-initialize")
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
Trix.config.blockAttributes.default.breakOnReturn = false;
|
||||||
Trix.config.blockAttributes.default.breakOnReturn = false
|
|
||||||
Trix.config.blockAttributes.heading3 = {
|
Trix.config.blockAttributes.heading3 = {
|
||||||
tagName: 'h3',
|
tagName: "h3",
|
||||||
terminal: true,
|
terminal: true,
|
||||||
breakOnReturn: true,
|
breakOnReturn: true,
|
||||||
group: false
|
group: false,
|
||||||
}
|
};
|
||||||
// console.log(Trix.config)
|
// console.log(Trix.config)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="tox-wrapper">
|
<div class="tox-wrapper">
|
||||||
<input id="x-{field.name}" {value} type="hidden">
|
<input id="x-{field.name}" {value} type="hidden" />
|
||||||
<trix-editor
|
<trix-editor
|
||||||
bind:this={editor}
|
bind:this={editor}
|
||||||
class=" content"
|
class=" content"
|
||||||
@@ -62,6 +62,5 @@
|
|||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:trix-change={updateValue}
|
on:trix-change={updateValue}
|
||||||
|
|
||||||
></trix-editor>
|
></trix-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
.mce-content-body .img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body{
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.mce-content-body p{
|
|
||||||
|
|
||||||
margin-bottom: 14px;
|
|
||||||
&:last-child{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body ul {
|
|
||||||
padding: 0 0 0 16px;
|
|
||||||
list-style: none outside none;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body li::before {
|
|
||||||
content: "—";
|
|
||||||
opacity: .5;
|
|
||||||
font-size: 12px;
|
|
||||||
padding-right: 6px;
|
|
||||||
vertical-align: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body li {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let newRoles = [...member.roles, aRole];
|
let newRoles = [...member.roles, aRole];
|
||||||
|
console.log(member.roles)
|
||||||
|
console.log(aRole)
|
||||||
|
console.log(newRoles)
|
||||||
dispatch("update", {
|
dispatch("update", {
|
||||||
user: member.id,
|
user: member.id,
|
||||||
roles: newRoles,
|
roles: newRoles,
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let users;
|
export let users;
|
||||||
@@ -23,15 +22,14 @@
|
|||||||
function invite(newName, newEmail, newRole) {
|
function invite(newName, newEmail, newRole) {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/invite", {
|
||||||
.post(channel.lucentUrl + "/members/invite", {
|
|
||||||
name: newName,
|
name: newName,
|
||||||
email: newEmail,
|
email: newEmail,
|
||||||
roles: [newRole],
|
roles: [newRole],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("User was invited");
|
successAlert.show("User was invited");
|
||||||
users = [...users, response.data.user];
|
users = [...users, response.user];
|
||||||
name = null;
|
name = null;
|
||||||
email = null;
|
email = null;
|
||||||
role = null;
|
role = null;
|
||||||
@@ -45,14 +43,13 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/update", {
|
||||||
.post(channel.lucentUrl + "/members/update", {
|
|
||||||
id: e.detail.user,
|
id: e.detail.user,
|
||||||
roles: e.detail.roles,
|
roles: e.detail.roles,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("Users updated");
|
successAlert.show("Users updated");
|
||||||
users = response.data.users;
|
users = response.users;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data?.error ?? "";
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
@@ -63,14 +60,12 @@
|
|||||||
<div class="common-wrapper">
|
<div class="common-wrapper">
|
||||||
<div class="lx-card mt-5">
|
<div class="lx-card mt-5">
|
||||||
<h3 class="header-small mb-5">Invite people</h3>
|
<h3 class="header-small mb-5">Invite people</h3>
|
||||||
<ErrorAlert message={errorMessage}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
<SuccessAlert bind:this={successAlert}/>
|
<SuccessAlert bind:this={successAlert} />
|
||||||
|
|
||||||
<form on:submit={submitInvite}>
|
<form on:submit={submitInvite}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="inviteeName" class="form-label"
|
<label for="inviteeName" class="form-label">Invitee Name</label>
|
||||||
>Invitee Name</label
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
@@ -97,16 +92,13 @@
|
|||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<select bind:value={role}>
|
<select bind:value={role}>
|
||||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||||
<option
|
<option value={arole}>{arole}</option>
|
||||||
value={arole}
|
|
||||||
|
|
||||||
>{arole}</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 d-block text-center">
|
<div class="mt-5 d-block text-center">
|
||||||
<SpinnerButton label="Invite"/>
|
<SpinnerButton label="Invite" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {afterUpdate, getContext, onMount} from "svelte";
|
import { afterUpdate, getContext, onMount } from "svelte";
|
||||||
import axios from "axios";
|
import EditHeader from "./header/EditHeader.svelte";
|
||||||
import EditHeader from "./header/EditHeader.svelte"
|
import ContentTabs from "./header/ContentTabs.svelte";
|
||||||
import FilePreview from "./FilePreview.svelte"
|
import FormField from "./FormField.svelte";
|
||||||
import ContentTabs from "./header/ContentTabs.svelte"
|
import Graph from "./Graph.svelte";
|
||||||
import FormField from "./FormField.svelte"
|
import Info from "./Info.svelte";
|
||||||
import Graph from "./Graph.svelte"
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import Info from "./Info.svelte"
|
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte"
|
|
||||||
import Title from "./header/Title.svelte";
|
import Title from "./header/Title.svelte";
|
||||||
import {hasDataChanged} from "./editor.js";
|
import { apiPost, isEqual } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
edges: []
|
edges: [],
|
||||||
};
|
};
|
||||||
// export let recordHistory;
|
// export let recordHistory;
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
@@ -33,10 +31,7 @@
|
|||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
(f) => f.name !== "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
@@ -45,10 +40,7 @@
|
|||||||
function setOriginalContent() {
|
function setOriginalContent() {
|
||||||
originalContent = {
|
originalContent = {
|
||||||
data: JSON.parse(JSON.stringify(record.data)),
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
schema: record.schema,
|
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
|
||||||
_file: JSON.parse(JSON.stringify(record._file)),
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -74,12 +66,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkUnsavedData() {
|
function checkUnsavedData() {
|
||||||
return hasDataChanged(isCreateMode,originalContent,{
|
if (isCreateMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isEqual(originalContent, {
|
||||||
data: record.data,
|
data: record.data,
|
||||||
schema: record.schema,
|
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: record._sys,
|
|
||||||
_file: record._file,
|
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,9 +94,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
graph.edges = graph.edges?.filter(
|
||||||
axios
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
.post(channel.lucentUrl + "/records", {
|
);
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
record: record,
|
record: record,
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
@@ -112,16 +106,17 @@
|
|||||||
console.log("SAVE: SAVED");
|
console.log("SAVE: SAVED");
|
||||||
|
|
||||||
if (isCreateMode) {
|
if (isCreateMode) {
|
||||||
window.location = channel.lucentUrl + "/records/" + record.id;
|
window.location =
|
||||||
|
channel.lucentUrl + "/records/" + record.id;
|
||||||
} else {
|
} else {
|
||||||
record = response.data.records[0] ?? null;
|
record = response.records[0] ?? null;
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// means trashed
|
// means trashed
|
||||||
hasUnsavedData = false;
|
hasUnsavedData = false;
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
graph = response.data;
|
graph = response;
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +129,7 @@
|
|||||||
errorMessage = error.response.data.error;
|
errorMessage = error.response.data.error;
|
||||||
} else {
|
} else {
|
||||||
validationErrors = error.response.data.error;
|
validationErrors = error.response.data.error;
|
||||||
console.log(validationErrors)
|
console.log(validationErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -146,17 +141,14 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={beforeUnload}/>
|
<svelte:window on:beforeunload={beforeUnload} />
|
||||||
|
|
||||||
<div class="record-edit">
|
<div class="record-edit">
|
||||||
<div class="tools-header">
|
<div class="tools-header">
|
||||||
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
|
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
||||||
{#if isCreateMode}
|
{#if isCreateMode}
|
||||||
<button
|
<button class="button primary btn-spinner" on:click={save}>
|
||||||
class="button primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
@@ -178,26 +170,19 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Title {schema} {record} {isCreateMode}/>
|
<Title {schema} {record} {isCreateMode} />
|
||||||
|
|
||||||
|
<ErrorAlert message={errorMessage} />
|
||||||
<ErrorAlert message={errorMessage}/>
|
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<ContentTabs
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
{schema}
|
|
||||||
{isCreateMode}
|
|
||||||
bind:active={activeContentTab}
|
|
||||||
/>
|
|
||||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||||
<FilePreview {record} {schema}/>
|
|
||||||
{#each activeFields as field (field.name)}
|
{#each activeFields as field (field.name)}
|
||||||
{#if activeContentTab === field.group}
|
{#if activeContentTab === field.group}
|
||||||
<FormField
|
<FormField
|
||||||
bind:data={record.data}
|
bind:data={record.data}
|
||||||
bind:graph={graph}
|
bind:graph
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
@@ -207,9 +192,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{:else if activeContentTab === "_graph"}
|
{:else if activeContentTab === "_graph"}
|
||||||
<Graph {graph} {record}/>
|
<Graph {graph} {record} />
|
||||||
{:else if activeContentTab === "_info"}
|
{:else if activeContentTab === "_info"}
|
||||||
<Info {record} {graph} {users} {schema}/>
|
<Info {record} {graph} {users} {schema} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../files/Preview.svelte";
|
import Preview from "../files/Preview.svelte";
|
||||||
import {fileurl} from "../files/imageserver"
|
import { fileurl } from "../files/imageserver";
|
||||||
import {getContext} from "svelte"
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
@@ -11,10 +11,9 @@
|
|||||||
{#if schema.type === "files"}
|
{#if schema.type === "files"}
|
||||||
<div class="record-edit-file-preview">
|
<div class="record-edit-file-preview">
|
||||||
<div>
|
<div>
|
||||||
<Preview {record} size="large"/>
|
<Preview {record} size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div class="file-details">
|
<div class="file-details">
|
||||||
|
|
||||||
<div class="file-details-item">
|
<div class="file-details-item">
|
||||||
<span class="text-muted">Filename</span>
|
<span class="text-muted">Filename</span>
|
||||||
<span>{record._file.path}</span>
|
<span>{record._file.path}</span>
|
||||||
@@ -42,9 +41,13 @@
|
|||||||
<span>{record._file.checksum}</span>
|
<span>{record._file.checksum}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-details-item">
|
<div class="file-details-item">
|
||||||
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(channel,record)}">Download</a>
|
<a
|
||||||
|
class="button primary"
|
||||||
|
target="_blank"
|
||||||
|
style="display: inline-flex"
|
||||||
|
href={fileurl(channel, record)}>Download</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
import Color from "./elements/Color.svelte";
|
import Color from "./elements/Color.svelte";
|
||||||
import Checkbox from "./elements/Checkbox.svelte";
|
import Checkbox from "./elements/Checkbox.svelte";
|
||||||
import Number from "./elements/Number.svelte";
|
import Number from "./elements/Number.svelte";
|
||||||
|
import Url from "./elements/Url.svelte";
|
||||||
import Date from "./elements/Date.svelte";
|
import Date from "./elements/Date.svelte";
|
||||||
|
import UUID from "./elements/UUID.svelte";
|
||||||
import File from "./elements/File.svelte";
|
import File from "./elements/File.svelte";
|
||||||
import Textarea from "./elements/Textarea.svelte";
|
import Textarea from "./elements/Textarea.svelte";
|
||||||
import Datetime from "./elements/Datetime.svelte";
|
import Datetime from "./elements/Datetime.svelte";
|
||||||
@@ -23,8 +25,10 @@
|
|||||||
color: Color,
|
color: Color,
|
||||||
checkbox: Checkbox,
|
checkbox: Checkbox,
|
||||||
number: Number,
|
number: Number,
|
||||||
|
url: Url,
|
||||||
date: Date,
|
date: Date,
|
||||||
datetime: Datetime,
|
datetime: Datetime,
|
||||||
|
uuid: UUID,
|
||||||
json: Json,
|
json: Json,
|
||||||
markdown: Markdown,
|
markdown: Markdown,
|
||||||
};
|
};
|
||||||
@@ -41,25 +45,20 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<FieldHeader {field} {id}/>
|
<FieldHeader {field} {id} />
|
||||||
{#if field.info.name === "reference" && field.layout === "tags"}
|
{#if field.info.name === "reference" && field.layout === "tags"}
|
||||||
<ReferenceTags
|
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
||||||
bind:graph
|
|
||||||
{id}
|
|
||||||
{record}
|
|
||||||
{field}
|
|
||||||
{validationErrors}
|
|
||||||
/>
|
|
||||||
{:else if field.info.name === "reference"}
|
{:else if field.info.name === "reference"}
|
||||||
<Reference
|
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
||||||
bind:graph
|
{:else if field.info.name === "file"}
|
||||||
{id}
|
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
|
||||||
|
<File
|
||||||
|
bind:value={data[field.name]}
|
||||||
{record}
|
{record}
|
||||||
|
{id}
|
||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
/>
|
/>
|
||||||
{:else if field.info.name === "file"}
|
|
||||||
<File bind:graph {record} {field} {validationErrors}/>
|
|
||||||
{:else if field.info.name === "text"}
|
{:else if field.info.name === "text"}
|
||||||
<Text
|
<Text
|
||||||
bind:value={data[field.name]}
|
bind:value={data[field.name]}
|
||||||
|
|||||||
@@ -13,16 +13,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let backlinks = graph.parentEdges.map(edge => {
|
let backlinks = graph.parentEdges.map(edge => {
|
||||||
const parentRecord = graph.records.find( record => record.id === edge.source);
|
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||||
|
|
||||||
let schema = channel.schemas.find((s) => s.name === parentRecord.schema);
|
|
||||||
let edgeField = findEdgeField(schema,edge.field);
|
let edgeField = findEdgeField(schema,edge.field);
|
||||||
if(!edgeField){
|
if(!edgeField){
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
field: edgeField.label,
|
field: edgeField.label,
|
||||||
record: parentRecord
|
record: graph.records.find( record => record.id === edge.source)
|
||||||
}
|
}
|
||||||
}).filter( edgeOrNull => !!edgeOrNull)
|
}).filter( edgeOrNull => !!edgeOrNull)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import {friendlyDate} from "../../helpers";
|
import { friendlyDate, isEqual } from "../../helpers";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {usernameById} from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
|
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import RevisionCell from "./revisions/RevisionCell.svelte";
|
import RevisionCell from "./revisions/RevisionCell.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte";
|
import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte";
|
||||||
import axios from "axios";
|
|
||||||
import {hasDataChanged} from "./editor.js";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
@@ -31,27 +30,27 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getEdgesByField(fieldsWithDiff, revision) {
|
function getEdgesByField(fieldsWithDiff, revision) {
|
||||||
|
edgeFieldsDiff = graph.edges
|
||||||
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
|
.filter((e) => e.depth === 1)
|
||||||
|
.reduce((c, e) => {
|
||||||
if (!c[e.field]) {
|
if (!c[e.field]) {
|
||||||
c[e.field] = {
|
c[e.field] = {
|
||||||
record: [],
|
record: [],
|
||||||
revision: [],
|
revision: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
c[e.field]["record"].push(e);
|
||||||
c[e.field]["record"].push(e)
|
|
||||||
return c;
|
return c;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
edgeFieldsDiff = revision._edges.reduce((c, e) => {
|
edgeFieldsDiff = revision._edges.reduce((c, e) => {
|
||||||
if (!c[e.field]) {
|
if (!c[e.field]) {
|
||||||
c[e.field] = {
|
c[e.field] = {
|
||||||
record: [],
|
record: [],
|
||||||
revision: [],
|
revision: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
c[e.field]["revision"].push(e);
|
||||||
c[e.field]["revision"].push(e)
|
|
||||||
return c;
|
return c;
|
||||||
}, edgeFieldsDiff);
|
}, edgeFieldsDiff);
|
||||||
}
|
}
|
||||||
@@ -61,9 +60,9 @@
|
|||||||
selectedRevision = revision;
|
selectedRevision = revision;
|
||||||
|
|
||||||
fieldsWithDiff = schema.fields.filter((f) => {
|
fieldsWithDiff = schema.fields.filter((f) => {
|
||||||
return hasDataChanged(false,selectedRevision.data[f.name], record.data[f.name]);
|
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
|
||||||
});
|
});
|
||||||
getEdgesByField(fieldsWithDiff, revision)
|
getEdgesByField(fieldsWithDiff, revision);
|
||||||
revisionSection.scrollIntoView();
|
revisionSection.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@
|
|||||||
rollbackError = "";
|
rollbackError = "";
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
|
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -85,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="lx-card ">
|
<div class="lx-card">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div>
|
<div>
|
||||||
@@ -94,29 +93,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">current version </span>
|
<span class="label text-end text-muted">current version </span>
|
||||||
{record._sys.version}
|
{record.version}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted"> created </span>
|
<span class="label text-end text-muted"> created </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.createdBy)}
|
name={usernameById(users, record.createdBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.createdAt)}
|
{friendlyDate(record.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">updated </span>
|
<span class="label text-end text-muted">updated </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.updatedBy)}
|
name={usernameById(users, record.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.updatedAt)}
|
{friendlyDate(record.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="label d-block text-muted "
|
<span class="label d-block text-muted">Rules for this schema </span>
|
||||||
>Rules for this schema
|
|
||||||
</span>
|
|
||||||
<small>
|
<small>
|
||||||
Each record maintains the last {schema.revisions}
|
Each record maintains the last {schema.revisions}
|
||||||
versions
|
versions
|
||||||
@@ -128,31 +125,29 @@
|
|||||||
{#if schema.revisions > 0}
|
{#if schema.revisions > 0}
|
||||||
<div class="header-small mb-3">Revisions</div>
|
<div class="header-small mb-3">Revisions</div>
|
||||||
{#each revisions as revision}
|
{#each revisions as revision}
|
||||||
{#if revision._sys.version !== record._sys.version}
|
{#if revision.version !== record.version}
|
||||||
<div
|
<div
|
||||||
class="revision"
|
class="revision"
|
||||||
class:active={revision._sys.version ===
|
class:active={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="version">
|
<div class="version">
|
||||||
<span>version {revision._sys.version}</span>
|
<span>version {revision.version}</span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, revision._sys.updatedBy)}
|
name={usernameById(users, revision.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(revision._sys.updatedAt)}
|
{friendlyDate(revision.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
<button
|
<button
|
||||||
disabled={revision._sys.version ===
|
disabled={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
class="button"
|
class="button"
|
||||||
on:click={(e) => compare(e, revision)}
|
on:click={(e) => compare(e, revision)}
|
||||||
>Compare
|
>Compare
|
||||||
</button
|
</button>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -170,15 +165,13 @@
|
|||||||
<p class="text-center fw-bold mb-3 mt-5">
|
<p class="text-center fw-bold mb-3 mt-5">
|
||||||
If you choose to rollback to this revision
|
If you choose to rollback to this revision
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button on:click={rollback} class="button">
|
||||||
on:click={rollback}
|
Rollback to version {selectedRevision.version}
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
Rollback to version {selectedRevision._sys.version}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if rollbackError}
|
{#if rollbackError}
|
||||||
<span class="d-block text-danger mt-3">{rollbackError}</span>
|
<span class="d-block text-danger mt-3">{rollbackError}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
{#each fieldsWithDiff as field}
|
{#each fieldsWithDiff as field}
|
||||||
@@ -189,10 +182,7 @@
|
|||||||
<!-- <div class="d-block" style="width:200px;">
|
<!-- <div class="d-block" style="width:200px;">
|
||||||
{field.label}
|
{field.label}
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div class="revision-field" style="overflow:hidden">
|
||||||
class="revision-field"
|
|
||||||
style="overflow:hidden"
|
|
||||||
>
|
|
||||||
<div class="compare-left">
|
<div class="compare-left">
|
||||||
<RevisionCell
|
<RevisionCell
|
||||||
{field}
|
{field}
|
||||||
@@ -227,22 +217,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<p class="text-center fw-bold mb-3 mt-5">
|
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
|
||||||
Record References
|
|
||||||
</p>
|
|
||||||
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
||||||
<div
|
<div class="revision-references" style="overflow:hidden">
|
||||||
class="revision-references"
|
|
||||||
style="overflow:hidden"
|
|
||||||
>
|
|
||||||
<div class="reference-field">
|
<div class="reference-field">
|
||||||
{field}:
|
{field}:
|
||||||
</div>
|
</div>
|
||||||
<div class="reference-compare">
|
<div class="reference-compare">
|
||||||
|
|
||||||
<p class="">Record</p>
|
<p class="">Record</p>
|
||||||
{#each edges.record as edge}
|
{#each edges.record as edge}
|
||||||
<RevisionEdgeRow {edge}/>
|
<RevisionEdgeRow {edge} />
|
||||||
{:else}
|
{:else}
|
||||||
<p>No references</p>
|
<p>No references</p>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -250,7 +234,7 @@
|
|||||||
<div class="reference-compare">
|
<div class="reference-compare">
|
||||||
<p class="text-success">Revision</p>
|
<p class="text-success">Revision</p>
|
||||||
{#each edges.revision as edge}
|
{#each edges.revision as edge}
|
||||||
<RevisionEdgeRow {edge}/>
|
<RevisionEdgeRow {edge} />
|
||||||
{:else}
|
{:else}
|
||||||
<p>No references</p>
|
<p>No references</p>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -259,7 +243,5 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import {afterUpdate, createEventDispatcher, getContext, onMount} from "svelte";
|
import {
|
||||||
import {hasDataChanged} from "./editor.js";
|
afterUpdate,
|
||||||
|
createEventDispatcher,
|
||||||
|
getContext,
|
||||||
|
onMount,
|
||||||
|
} from "svelte";
|
||||||
|
|
||||||
import FormField from "./FormField.svelte";
|
import FormField from "./FormField.svelte";
|
||||||
import FilePreview from "./FilePreview.svelte";
|
import FilePreview from "./FilePreview.svelte";
|
||||||
import ContentTabs from "./header/ContentTabs.svelte";
|
import ContentTabs from "./header/ContentTabs.svelte";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import EditHeader from "./header/EditHeader.svelte";
|
import EditHeader from "./header/EditHeader.svelte";
|
||||||
import axios from "axios";
|
|
||||||
import Title from "./header/Title.svelte";
|
import Title from "./header/Title.svelte";
|
||||||
|
import { apiPost, isEqual } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -15,7 +20,7 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
edges: []
|
edges: [],
|
||||||
};
|
};
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
let originalContent;
|
let originalContent;
|
||||||
@@ -28,9 +33,7 @@
|
|||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
(f) => f.name !== "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
let tabname = "_default";
|
let tabname = "_default";
|
||||||
let fieldToTabs = schema.fields.reduce((c, f) => {
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
@@ -52,8 +55,6 @@
|
|||||||
data: JSON.parse(JSON.stringify(record.data)),
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
schema: record.schema,
|
schema: record.schema,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
|
||||||
_file: JSON.parse(JSON.stringify(record._file)),
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -79,12 +80,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkUnsavedData() {
|
function checkUnsavedData() {
|
||||||
return hasDataChanged(isCreateMode, originalContent, {
|
if (isCreateMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isEqual(originalContent, {
|
||||||
data: record.data,
|
data: record.data,
|
||||||
schema: record.schema,
|
schema: record.schema,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: record._sys,
|
|
||||||
_file: record._file,
|
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,11 +112,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
|
graph.edges =
|
||||||
|
graph.edges?.filter(
|
||||||
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
axios
|
|
||||||
.post(channel.lucentUrl + "/records", {
|
|
||||||
record: record,
|
record: record,
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
@@ -122,8 +125,8 @@
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED INLINE");
|
console.log("SAVE: SAVED INLINE");
|
||||||
|
|
||||||
record = response.data.records[0];
|
record = response.records[0];
|
||||||
graph = response.data;
|
graph = response;
|
||||||
if (!isCreateMode) {
|
if (!isCreateMode) {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
@@ -146,16 +149,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={beforeUnload}/>
|
<svelte:window on:beforeunload={beforeUnload} />
|
||||||
|
|
||||||
<div class="inline-edit record-edit">
|
<div class="inline-edit record-edit">
|
||||||
<div class="tools-header">
|
<div class="tools-header">
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
|
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
||||||
{#if isCreateMode}
|
{#if isCreateMode}
|
||||||
<button
|
<button class="button primary btn-spinner" on:click={save}>
|
||||||
class="button primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
@@ -177,24 +177,19 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Title {schema} {record} {isCreateMode}/>
|
<Title {schema} {record} {isCreateMode} />
|
||||||
<ErrorAlert message={errorMessage}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<ContentTabs
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
{schema}
|
<FilePreview {record} {schema} />
|
||||||
{isCreateMode}
|
|
||||||
bind:active={activeContentTab}
|
|
||||||
/>
|
|
||||||
<FilePreview {record} {schema}/>
|
|
||||||
<!-- <fieldset disabled="disabled"> -->
|
<!-- <fieldset disabled="disabled"> -->
|
||||||
{#each activeFields as field (field.name)}
|
{#each activeFields as field (field.name)}
|
||||||
{#if activeContentTab === field.group}
|
{#if activeContentTab === field.group}
|
||||||
<FormField
|
<FormField
|
||||||
bind:data={record.data}
|
bind:data={record.data}
|
||||||
bind:graph={graph}
|
bind:graph
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
@@ -206,4 +201,3 @@
|
|||||||
<!-- </fieldset> -->
|
<!-- </fieldset> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import Mustache from "mustache";
|
|
||||||
import {stripHtml} from "../../helpers";
|
|
||||||
|
|
||||||
export function previewTitle(schemas, record, graph) {
|
|
||||||
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
|
|
||||||
if (!schema?.cardTitle) {
|
|
||||||
return noTemplate(schema, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
let recordData = record.data;
|
|
||||||
let render = Mustache.render(schema.cardTitle, recordData);
|
|
||||||
if (!render || render === "") {
|
|
||||||
return noTemplate(schema, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stripHtml(render.slice(0, 300));
|
|
||||||
}
|
|
||||||
|
|
||||||
function noTemplate(schema, record) {
|
|
||||||
if (schema?.type === "files") {
|
|
||||||
return record._file.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = stripHtml(
|
|
||||||
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
|
||||||
).slice(0, 300);
|
|
||||||
|
|
||||||
if(title.trim() === ""){
|
|
||||||
return "~Untitled~";
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {previewTitle} from "./Preview";
|
import { getContext } from "svelte";
|
||||||
import {getContext} from "svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
export let graph;
|
$: title = record.data.name;
|
||||||
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
|
||||||
|
|
||||||
$: title = previewTitle(channel.schemas, record, graph);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if record?.data}
|
{#if record?.data}
|
||||||
<a
|
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
{title}
|
|
||||||
class="reference"
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export function hasDataChanged(isCreateMode, originalContent, newContent){
|
|
||||||
if (isCreateMode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return JSON.stringify(originalContent) !== JSON.stringify(newContent);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import flatpickr from "flatpickr";
|
import flatpickr from "flatpickr";
|
||||||
|
import "flatpickr/dist/flatpickr.css";
|
||||||
|
import "flatpickr/dist/themes/light.css";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
import {getErrorMessage} from "./errorMessage";
|
||||||
|
|
||||||
export let field;
|
export let field;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import flatpickr from "flatpickr";
|
import flatpickr from "flatpickr";
|
||||||
|
import "flatpickr/dist/flatpickr.css";
|
||||||
|
import "flatpickr/dist/themes/light.css";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
import {getErrorMessage} from "./errorMessage";
|
||||||
|
|
||||||
export let field;
|
export let field;
|
||||||
|
|||||||
@@ -1,101 +1,69 @@
|
|||||||
<script>
|
<script>
|
||||||
import {sortByField} from "../../edges/sortEdges";
|
import { array_move } from "../../edges/sortEdges";
|
||||||
import Sortable from "../../libs/Sortable.svelte";
|
import Sortable from "../../libs/Sortable.svelte";
|
||||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import FileDialog from "../../dialog/FileDialog.svelte";
|
||||||
import Dialog from "../../dialog/Dialog.svelte";
|
import Uploader from "../../files/Uploader.svelte";
|
||||||
import {
|
|
||||||
fullDeleteRecord,
|
|
||||||
graphToReferences,
|
|
||||||
insertEdges,
|
|
||||||
removeReferenceFromGraph,
|
|
||||||
restoreReferenceToGraph
|
|
||||||
} from "./reference.js";
|
|
||||||
import {getContext} from "svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
export let field;
|
export let field;
|
||||||
export let record;
|
export let record;
|
||||||
export let graph
|
export let value = [];
|
||||||
let browseModal;
|
let browseModal;
|
||||||
$: references = graphToReferences(graph, record, field)
|
|
||||||
|
|
||||||
let collections = channel.schemas.filter((aschema) =>
|
function removeFile(e) {
|
||||||
field.collections.includes(aschema.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
function removeReference(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = removeReferenceFromGraph(graph, field, e.detail)
|
value = value.filter((f) => !(f.id === e.detail));
|
||||||
}
|
|
||||||
|
|
||||||
function restoreReference(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
graph.edges = restoreReferenceToGraph(graph, field, e.detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullDelete(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
graph.edges = fullDeleteRecord(channel,graph, field, e.detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBrowseModal(e, schema) {
|
|
||||||
e.preventDefault();
|
|
||||||
browseModal.open(schema);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reorder(e) {
|
async function reorder(e) {
|
||||||
|
value = await array_move(value, e.detail.source, e.detail.target);
|
||||||
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e) {
|
function insertFiles(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
browseModal.close();
|
browseModal.close();
|
||||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
value = [...(value ?? []), ...(e.detail ?? [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceFiles(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.close();
|
||||||
|
value = e.detail ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadComplete(e) {
|
||||||
|
value = [...value, ...e.detail];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowseModal(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.open(record.id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
{#if field.collections.length === 1}
|
<button class="button" on:click={openBrowseModal}> Browse </button>
|
||||||
<button
|
|
||||||
class="button"
|
<div>
|
||||||
on:click={(e) => openBrowseModal(e, collections[0].name)}
|
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<Dropdown>
|
|
||||||
<div slot="button">
|
|
||||||
Browse
|
|
||||||
</div>
|
</div>
|
||||||
{#each collections as collection}
|
|
||||||
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
on:click={(e) => openBrowseModal(e, collection.name)}
|
|
||||||
href="/">{collection.label}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</Dropdown>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if references.length > 0}
|
{#if value.length > 0}
|
||||||
<Sortable sortableClass="mt-3" on:update={reorder}>
|
<Sortable sortableClass="mt-3" on:update={reorder}>
|
||||||
{#each references as reference (reference.record.id)}
|
{#each value ?? [] as aFile (aFile.id)}
|
||||||
<!--This div helps the sorting thing-->
|
<!--This div helps the sorting thing-->
|
||||||
<div>
|
<div>
|
||||||
<PreviewFile
|
<PreviewFile
|
||||||
record={reference.record}
|
file={aFile}
|
||||||
edge={reference.edge}
|
|
||||||
hasDelete={true}
|
hasDelete={true}
|
||||||
on:remove={removeReference}
|
on:remove_file={removeFile}
|
||||||
on:restore={restoreReference}
|
|
||||||
on:fulldelete={fullDelete}
|
|
||||||
></PreviewFile>
|
></PreviewFile>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Sortable>
|
</Sortable>
|
||||||
{/if}
|
{/if}
|
||||||
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
|
<FileDialog
|
||||||
|
bind:this={browseModal}
|
||||||
|
on:insert_files={insertFiles}
|
||||||
|
on:replace_files={replaceFiles}
|
||||||
|
></FileDialog>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import { insertEdges } from "./reference";
|
||||||
fullDeleteRecord,
|
import { getErrorMessage } from "./errorMessage";
|
||||||
graphToReferences,
|
import { sortByField } from "../../edges/sortEdges";
|
||||||
insertEdges,
|
|
||||||
removeReferenceFromGraph,
|
|
||||||
restoreReferenceToGraph
|
|
||||||
} from "./reference";
|
|
||||||
import {getErrorMessage} from "./errorMessage";
|
|
||||||
import {sortByField} from "../../edges/sortEdges";
|
|
||||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||||
import Sortable from "../../libs/Sortable.svelte";
|
import Sortable from "../../libs/Sortable.svelte";
|
||||||
import PreviewReference from "../previews/PreviewReference.svelte";
|
import PreviewReference from "../previews/PreviewReference.svelte";
|
||||||
@@ -20,37 +14,57 @@
|
|||||||
export let validationErrors;
|
export let validationErrors;
|
||||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
|
||||||
|
$: references =
|
||||||
$: references = graphToReferences(graph,record,field)
|
graph.edges
|
||||||
|
.filter((edge) => edge.field === field.name)
|
||||||
|
.map((edge) => {
|
||||||
|
return graph.records.find(
|
||||||
|
(increc) =>
|
||||||
|
increc.id === edge.target && record.id === edge.source,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
let collections = channel.schemas.filter((aschema) =>
|
let collections = channel.schemas.filter((aschema) =>
|
||||||
field.collections.includes(aschema.name)
|
field.collections.includes(aschema.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
function removeReference(e) {
|
function removeReference(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = removeReferenceFromGraph(graph,field,e.detail)
|
graph.edges = graph.edges.filter(
|
||||||
}
|
(edge) => !(edge.target === e.detail && edge.field === field.name),
|
||||||
|
);
|
||||||
function restoreReference(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
graph.edges = restoreReferenceToGraph(graph,field,e.detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullDelete(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
graph.edges = fullDeleteRecord(channel,graph, field, e.detail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorder(e) {
|
function reorder(e) {
|
||||||
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
graph.edges = sortByField(
|
||||||
|
e.detail.source,
|
||||||
|
e.detail.target,
|
||||||
|
graph.edges,
|
||||||
|
field.name,
|
||||||
|
references,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e) {
|
function insert(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
// axios.post(channel.lucentUrl + "/edges/insert-many", {
|
||||||
|
// source: record.id,
|
||||||
|
// sourceSchema: record.schema,
|
||||||
|
// targetSchema: e.detail.schema,
|
||||||
|
// field: field.name,
|
||||||
|
// targets: e.detail.records.map(r => r.id),
|
||||||
|
// }).then(function (response) {
|
||||||
|
// graph = response.data.graph;
|
||||||
|
// })
|
||||||
|
graph = insertEdges(
|
||||||
|
graph,
|
||||||
|
record,
|
||||||
|
e.detail.records,
|
||||||
|
field.name,
|
||||||
|
e.detail.action,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
@@ -68,16 +82,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if references.length > 0}
|
{#if references.length > 0}
|
||||||
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
||||||
{#each references as reference (reference.record.id)}
|
{#each references as reference (reference.id)}
|
||||||
<div>
|
<div>
|
||||||
<PreviewReference
|
<PreviewReference
|
||||||
{graph}
|
{graph}
|
||||||
record={reference.record}
|
record={reference}
|
||||||
edge={reference.edge}
|
|
||||||
hasDelete={true}
|
hasDelete={true}
|
||||||
on:remove={removeReference}
|
on:remove={removeReference}
|
||||||
on:restore={restoreReference}
|
|
||||||
on:fulldelete={fullDelete}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import InlineEdit from "../InlineEdit.svelte";
|
import InlineEdit from "../InlineEdit.svelte";
|
||||||
import Dialog from "../../dialog/Dialog.svelte";
|
import Dialog from "../../dialog/Dialog.svelte";
|
||||||
import DialogRecord from "../../dialog/DialogRecord.svelte";
|
import DialogRecord from "../../dialog/DialogRecord.svelte";
|
||||||
import axios from "axios";
|
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
import { apiGet } from "../../../helpers";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("Save inline");
|
console.log("Save inline");
|
||||||
inLineCreateRecord = null;
|
inLineCreateRecord = null;
|
||||||
dialogRecord.close()
|
dialogRecord.close();
|
||||||
dispatch("save", {
|
dispatch("save", {
|
||||||
records: e.detail.records,
|
records: e.detail.records,
|
||||||
after: recordId,
|
after: recordId,
|
||||||
@@ -44,11 +44,10 @@
|
|||||||
function createInlineReference(e, schemaUId) {
|
function createInlineReference(e, schemaUId) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inLineCreateRecord = null;
|
inLineCreateRecord = null;
|
||||||
axios
|
apiGet(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
||||||
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
inLineCreateRecord = response.data;
|
inLineCreateRecord = response;
|
||||||
dialogRecord.open()
|
dialogRecord.open();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -57,23 +56,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schemas.length > 1}
|
{#if schemas.length > 1}
|
||||||
<div
|
<div style="display: flex;align-items: center;gap:4px">
|
||||||
style="display: flex;align-items: center;gap:4px"
|
|
||||||
>
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">New</div>
|
<div slot="button">New</div>
|
||||||
{#each schemas as schema}
|
{#each schemas as schema}
|
||||||
<button
|
<button
|
||||||
class=" button"
|
class=" button"
|
||||||
on:click={(e) =>
|
on:click={(e) => createInlineReference(e, schema.name)}
|
||||||
createInlineReference(e, schema.name)}
|
|
||||||
>{schema.label}
|
>{schema.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button"> <Icon icon="magnifying-glass"/></div>
|
<div slot="button"><Icon icon="magnifying-glass" /></div>
|
||||||
{#each schemas as schema}
|
{#each schemas as schema}
|
||||||
<button
|
<button
|
||||||
class="button"
|
class="button"
|
||||||
@@ -81,7 +76,6 @@
|
|||||||
>{schema.label}
|
>{schema.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -95,9 +89,8 @@
|
|||||||
class="button"
|
class="button"
|
||||||
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
||||||
>
|
>
|
||||||
<Icon icon="magnifying-glass"/>
|
<Icon icon="magnifying-glass" />
|
||||||
</button
|
</button>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -109,8 +102,7 @@
|
|||||||
on:cancel={(e) => (inLineCreateRecord = null)}
|
on:cancel={(e) => (inLineCreateRecord = null)}
|
||||||
on:inlinesaved={save}
|
on:inlinesaved={save}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</DialogRecord>
|
</DialogRecord>
|
||||||
|
|
||||||
<Dialog bind:this={browseModal} on:insert={insert}/>
|
<Dialog bind:this={browseModal} on:insert={insert} />
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {debounce} from "../../../debounce.js";
|
import { getErrorMessage } from "./errorMessage";
|
||||||
import {previewTitle} from "../Preview";
|
import { insertEdges } from "./reference.js";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
|
||||||
import {insertEdges} from "./reference.js";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let field;
|
export let field;
|
||||||
@@ -16,20 +13,24 @@
|
|||||||
export let validationErrors;
|
export let validationErrors;
|
||||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
|
||||||
$: references = graph.edges
|
$: references =
|
||||||
|
graph.edges
|
||||||
.filter((edge) => edge.field === field.name)
|
.filter((edge) => edge.field === field.name)
|
||||||
.map((edge) => {
|
.map((edge) => {
|
||||||
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
|
return graph.records.find(
|
||||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
(increc) =>
|
||||||
|
increc.id == edge.target && record.id == edge.source,
|
||||||
let search = ""
|
);
|
||||||
$: searchOptions = []
|
})
|
||||||
|
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
|
let search = "";
|
||||||
|
$: searchOptions = [];
|
||||||
|
|
||||||
function removeReference(e, recordId) {
|
function removeReference(e, recordId) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = graph.edges.filter(
|
graph.edges = graph.edges.filter(
|
||||||
(edge) => !(edge.target === recordId && edge.field === field.name)
|
(edge) => !(edge.target === recordId && edge.field === field.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,25 +43,33 @@
|
|||||||
schema: field.collections[0],
|
schema: field.collections[0],
|
||||||
status: "published",
|
status: "published",
|
||||||
data: {
|
data: {
|
||||||
[field.searchField]: newValue
|
[field.searchField]: newValue,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
insert(e, response.data.records[0]);
|
insert(e, response.data.records[0]);
|
||||||
|
console.log(response);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e, insertRecord) {
|
function insert(e, insertRecord) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
|
graph = insertEdges(
|
||||||
search = ""
|
graph,
|
||||||
searchEl.focus()
|
record,
|
||||||
searchEl.blur()
|
[insertRecord],
|
||||||
|
field.name,
|
||||||
|
e.detail.action,
|
||||||
|
);
|
||||||
|
search = "";
|
||||||
|
searchEl.focus();
|
||||||
|
searchEl.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
@@ -78,11 +87,11 @@
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
|
console.log(error);
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="reference-tags">
|
<div class="reference-tags">
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="invalid-feedback d-block mb-3">
|
<div class="invalid-feedback d-block mb-3">
|
||||||
@@ -97,12 +106,11 @@
|
|||||||
on:keyup={updateResults}
|
on:keyup={updateResults}
|
||||||
class:is-invalid={errorMessage}
|
class:is-invalid={errorMessage}
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
placeholder={"Search for "+field.label}
|
placeholder={"Search for " + field.label}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reference-tags-results">
|
<div class="reference-tags-results">
|
||||||
|
|
||||||
{#if searchOptions}
|
{#if searchOptions}
|
||||||
{#each searchOptions as option (option.id)}
|
{#each searchOptions as option (option.id)}
|
||||||
<div
|
<div
|
||||||
@@ -112,23 +120,19 @@
|
|||||||
on:click={(e) => insert(e, option)}
|
on:click={(e) => insert(e, option)}
|
||||||
on:keypress={(e) => insert(e, option)}
|
on:keypress={(e) => insert(e, option)}
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, option ,graph)}
|
{option.data.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="start-typing">Start typing...</div>
|
||||||
class="start-typing">
|
|
||||||
Start typing...
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{#if search }
|
{#if search}
|
||||||
<div
|
<div
|
||||||
class="reference-tags-option"
|
class="reference-tags-option"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click={(e) => saveNew(e,search)}
|
on:click={(e) => saveNew(e, search)}
|
||||||
on:keypress={(e) => saveNew(e,search)}
|
on:keypress={(e) => saveNew(e, search)}
|
||||||
>
|
>
|
||||||
Add "{search}"
|
Add "{search}"
|
||||||
</div>
|
</div>
|
||||||
@@ -144,22 +148,19 @@
|
|||||||
class="record-title"
|
class="record-title"
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, record)}
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => removeReference(e, record.id)}
|
on:click|preventDefault={(e) =>
|
||||||
|
removeReference(e, record.id)}
|
||||||
type="button"
|
type="button"
|
||||||
class="button-text"
|
class="button-text"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<Icon width={12} height={12} icon="close"></Icon>
|
<Icon width={12} height={12} icon="close"></Icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
import { getErrorMessage } from "./errorMessage";
|
||||||
import Trix from "../../libs/Trix.svelte";
|
import Trix from "../../libs/Trix.svelte";
|
||||||
|
|
||||||
export let value;
|
export let value;
|
||||||
@@ -17,14 +17,14 @@
|
|||||||
readonly: field.readonly && !isCreateMode,
|
readonly: field.readonly && !isCreateMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
function insertMedia(e){
|
function insertMedia(e) {
|
||||||
editor.insertMedia(e.detail)
|
editor.insertMedia(e.detail);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
|
|
||||||
<Trix {field} bind:this={editor} bind:value></Trix>
|
<Trix {field} bind:this={editor} bind:value></Trix>
|
||||||
|
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
|
||||||
{#if field.collections.length > 0}
|
{#if field.collections.length > 0}
|
||||||
<RichEditorFiles
|
<RichEditorFiles
|
||||||
bind:graph
|
bind:graph
|
||||||
@@ -32,13 +32,9 @@
|
|||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
on:editor-insert={insertMedia}
|
on:editor-insert={insertMedia}
|
||||||
>
|
></RichEditorFiles>
|
||||||
|
|
||||||
|
|
||||||
</RichEditorFiles>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="invalid-feedback d-block">
|
<div class="invalid-feedback d-block">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import { getErrorMessage } from "./errorMessage";
|
||||||
|
const channelurl = getContext("channelurl");
|
||||||
|
export let validationErrors;
|
||||||
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
export let field;
|
||||||
|
export let value;
|
||||||
|
export let id;
|
||||||
|
export let isCreateMode;
|
||||||
|
let readonly = field.readonly && !isCreateMode;
|
||||||
|
|
||||||
|
function generateId(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
value = self.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:value
|
||||||
|
autocomplete="off"
|
||||||
|
{readonly}
|
||||||
|
/>
|
||||||
|
{#if !readonly}
|
||||||
|
<button
|
||||||
|
class="btn btn-primary ms-2"
|
||||||
|
title="Generate a new UUIDv4"
|
||||||
|
on:click={generateId}
|
||||||
|
>
|
||||||
|
<Icon icon="dice" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
const channelurl = getContext("channelurl");
|
||||||
|
export let field;
|
||||||
|
export let value;
|
||||||
|
export let schema;
|
||||||
|
let id = self.crypto.randomUUID();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label for={id} class="form-label">{field.label}</label>
|
||||||
|
<a
|
||||||
|
class="text-decoration-none"
|
||||||
|
href="{channelurl}/schemas/{schema.name}/fields/edit/{field.name}"
|
||||||
|
><code class="text-primary opacity-50">{field.name}</code></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
bind:value
|
||||||
|
placeholder="https://www.example.com"
|
||||||
|
/>
|
||||||
|
{#if field.help}
|
||||||
|
<small class=" text-primary opacity-50">{field.help}</small>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
import {uniqueBy} from "../../../helpers.js";
|
import { arrayUniqueCb } from "../../../helpers";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") {
|
export function insertEdges(
|
||||||
|
graph,
|
||||||
|
sourceRecord,
|
||||||
|
targetRecords,
|
||||||
|
fieldName,
|
||||||
|
action = "",
|
||||||
|
) {
|
||||||
let newEdges = targetRecords.map((r) => {
|
let newEdges = targetRecords.map((r) => {
|
||||||
return {
|
return {
|
||||||
target: r.id,
|
target: r.id,
|
||||||
source: sourceRecord.id,
|
source: sourceRecord.id,
|
||||||
|
sourceSchema: sourceRecord.schema,
|
||||||
|
targetSchema: r.schema,
|
||||||
field: fieldName,
|
field: fieldName,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
rank: ""
|
rank: "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,53 +24,13 @@ export function insertEdges(graph, sourceRecord, targetRecords, fieldName, actio
|
|||||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.records = uniqueBy([...graph.records, ...targetRecords], (r) => r.id);
|
graph.records = arrayUniqueCb(
|
||||||
graph.edges = uniqueBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth);
|
[...graph.records, ...targetRecords],
|
||||||
|
(r) => r.id,
|
||||||
|
);
|
||||||
|
graph.edges = arrayUniqueCb(
|
||||||
|
[...replacedEdges, ...newEdges],
|
||||||
|
(edge) => edge.source + edge.target + edge.field + edge.depth,
|
||||||
|
);
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function graphToReferences(graph,record,field){
|
|
||||||
return graph.edges
|
|
||||||
.filter((edge) => edge.field === field.name)
|
|
||||||
.map((edge) => {
|
|
||||||
return {
|
|
||||||
record: graph.records.find((increc) => increc.id === edge.target && record.id === edge.source),
|
|
||||||
edge: edge
|
|
||||||
};
|
|
||||||
}).filter((rec) => (rec.record?.id ? true : false)) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeReferenceFromGraph(graph,field,id){
|
|
||||||
return graph.edges.map(
|
|
||||||
(edge) => {
|
|
||||||
if(edge.target === id && edge.field === field.name){
|
|
||||||
edge._isTrashed = true;
|
|
||||||
}
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function restoreReferenceToGraph(graph,field,id){
|
|
||||||
return graph.edges.map(
|
|
||||||
(edge) => {
|
|
||||||
if(edge.target === id && edge.field === field.name){
|
|
||||||
edge._isTrashed = false;
|
|
||||||
}
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function fullDeleteRecord(channel,graph,field,id){
|
|
||||||
axios
|
|
||||||
.post(channel.lucentUrl + "/records/status/trashed" , {
|
|
||||||
records: [id]
|
|
||||||
});
|
|
||||||
|
|
||||||
return graph.edges.filter(
|
|
||||||
(edge) => !(edge.target === id && edge.field === field.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
import StatusSelect from "./StatusSelect.svelte";
|
import StatusSelect from "./StatusSelect.svelte";
|
||||||
|
import { apiPost } from "../../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let schema;
|
||||||
@@ -12,21 +13,19 @@
|
|||||||
|
|
||||||
function clone(e) {
|
function clone(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
|
apiPost(channel.lucentUrl + "/records/clone/" + record.id)
|
||||||
window.location = channel.lucentUrl + "/records/" + response.data.id;
|
.then((response) => {
|
||||||
}).catch(error => {
|
window.location = channel.lucentUrl + "/records/" + response.id;
|
||||||
|
})
|
||||||
});
|
.catch((error) => {});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style="display: flex;align-items: center; gap:10px;">
|
<div style="display: flex;align-items: center; gap:10px;">
|
||||||
{#if !isCreateMode}
|
{#if !isCreateMode}
|
||||||
<Dropdown >
|
<Dropdown>
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
<Icon icon="ellipsis"/>
|
<Icon icon="ellipsis" />
|
||||||
</div>
|
</div>
|
||||||
<h6 class="dropdown-header">Record Actions</h6>
|
<h6 class="dropdown-header">Record Actions</h6>
|
||||||
<a
|
<a
|
||||||
@@ -44,13 +43,12 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
on:click|preventDefault={(e) =>
|
on:click|preventDefault={(e) => (activeContentTab = "_info")}
|
||||||
(activeContentTab = "_info")}
|
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="{channel.lucentUrl}">Revisions</a
|
href={channel.lucentUrl}>Revisions</a
|
||||||
>
|
>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<StatusSelect bind:status={record.status} {record}></StatusSelect>
|
<StatusSelect bind:status={record.status} {record}></StatusSelect>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,27 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let schema;
|
||||||
export let record;
|
export let record;
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="record-header">
|
<div class="record-header">
|
||||||
|
<a class="schema-name" href="{channel.lucentUrl}/content/{schema.name}"
|
||||||
<a
|
|
||||||
class="schema-name"
|
|
||||||
href="{channel.lucentUrl}/content/{schema.name}"
|
|
||||||
>{schema.label.toUpperCase()}</a
|
>{schema.label.toUpperCase()}</a
|
||||||
>
|
>
|
||||||
<span class="record-title">
|
<span class="record-title">
|
||||||
{#if !isCreateMode}
|
{#if !isCreateMode}
|
||||||
{previewTitle(channel.schemas, record)}
|
{record.data.name}
|
||||||
{:else}
|
{:else}
|
||||||
New Record
|
New Record
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,123 +1,77 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
import {fileurl, htmlurl} from "../../files/imageserver.js"
|
|
||||||
import Status from "./../Status.svelte";
|
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let file;
|
||||||
export let edge;
|
|
||||||
export let hasDelete = false;
|
export let hasDelete = false;
|
||||||
export let hasInsert = false;
|
export let hasInsert = false;
|
||||||
|
|
||||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
|
||||||
let cardTitle = previewTitle(channel.schemas, record);
|
|
||||||
let imagePresets = Object.keys(channel.imageFilters);
|
let imagePresets = Object.keys(channel.imageFilters);
|
||||||
|
|
||||||
function remove(e) {
|
function remove(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch("remove", record.id);
|
dispatch("remove_file", file.id);
|
||||||
}
|
|
||||||
|
|
||||||
function restore(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("restore", record.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullDelete(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("fulldelete", record.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e, preset) {
|
function insert(e, preset) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let html = htmlurl(channel, record, preset)
|
// let html = htmlurl(channel, record, preset);
|
||||||
let url = !preset ? `/${record._file.path}` : `/templates/${preset}/${record._file.path}`;
|
// let url = !preset
|
||||||
dispatch("editor-insert", {
|
// ? `/${record._file.path}`
|
||||||
html: html,
|
// : `/templates/${preset}/${record._file.path}`;
|
||||||
url: channel.filesUrl + url,
|
// dispatch("editor-insert", {
|
||||||
originalUrl: channel.filesUrl + "/" + record._file.path,
|
// html: html,
|
||||||
record: record
|
// url: channel.filesUrl + url,
|
||||||
});
|
// originalUrl: channel.filesUrl + "/" + record._file.path,
|
||||||
|
// record: record,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="preview-file" class:is-trashed={edge?._isTrashed}>
|
<div class="preview-file">
|
||||||
<div style="display: flex;align-items: center;gap: 10px;">
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<Preview {record} size="small"/>
|
<Preview {file} size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>
|
<div>
|
||||||
<a
|
{file.filename}
|
||||||
class="record-title"
|
</div>
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="display: flex;gap:4px; align-items: center; margin-right: 10px;"
|
||||||
>
|
>
|
||||||
{cardTitle}
|
|
||||||
{#if edge?._isTrashed}
|
|
||||||
<span class="trashed-text">will remove on save</span>
|
|
||||||
{/if}
|
|
||||||
</a>
|
|
||||||
<small class="d-block">
|
|
||||||
from {schema.label}
|
|
||||||
{#if record.status === "draft"}
|
|
||||||
<Status status={record.status}/>
|
|
||||||
{/if}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;gap:4px; align-items: center; margin-right: 10px;">
|
|
||||||
{#if hasInsert}
|
{#if hasInsert}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
<Icon icon="photo-film"/>
|
<Icon icon="photo-film" />
|
||||||
</div>
|
</div>
|
||||||
<button class="dropdown-item button" on:click={e => insert(e,null)}>original</button>
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => insert(e, null)}>original</button
|
||||||
|
>
|
||||||
{#each imagePresets as preset}
|
{#each imagePresets as preset}
|
||||||
<button class="dropdown-item button" on:click={e => insert(e,preset)}>{preset}</button>
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => insert(e, preset)}>{preset}</button
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasDelete}
|
{#if hasDelete}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
{#if edge?._isTrashed}
|
<button class="button" on:click={remove}>
|
||||||
<button
|
<Icon icon="trash-can" />
|
||||||
title="Restore"
|
|
||||||
class="button"
|
|
||||||
on:click={restore}
|
|
||||||
>
|
|
||||||
<Icon icon="undo"/>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
title="Delete from everywhere"
|
|
||||||
class="button"
|
|
||||||
on:click={fullDelete}
|
|
||||||
>
|
|
||||||
<Icon icon="destroy"/>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
title="Remove"
|
|
||||||
class="button"
|
|
||||||
on:click={remove}
|
|
||||||
>
|
|
||||||
<Icon icon="trash-can"/>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
import Status from "./../Status.svelte";
|
import Status from "./../Status.svelte";
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
|
|
||||||
@@ -10,37 +9,29 @@
|
|||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let graph;
|
export let graph;
|
||||||
export let record;
|
export let record;
|
||||||
export let edge;
|
|
||||||
export let hasDelete = false;
|
export let hasDelete = false;
|
||||||
|
|
||||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
let schema = channel.schemas.find(
|
||||||
let cardTitle = previewTitle(channel.schemas, record, graph);
|
(aschema) => aschema.name === record.schema,
|
||||||
const cardImageEdge = graph.edges.find(e => e.source === record.id && e.field === schema.cardImage);
|
);
|
||||||
let cardImageRecord = graph.records.find(r => r.id === cardImageEdge?.target);
|
const cardImageEdge = graph.edges.find(
|
||||||
|
(e) => e.source === record.id && e.field === schema.cardImage,
|
||||||
|
);
|
||||||
|
let cardImageRecord = graph.records.find(
|
||||||
|
(r) => r.id === cardImageEdge?.target,
|
||||||
|
);
|
||||||
|
|
||||||
function remove(e) {
|
function remove(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch("remove", record.id);
|
dispatch("remove", record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("restore", record.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullDelete(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("fulldelete", record.id);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="preview-reference">
|
||||||
<div class="preview-reference" class:is-trashed={edge?._isTrashed}>
|
|
||||||
<div style="display: flex;align-items: center;gap: 10px;">
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
|
|
||||||
{#if cardImageRecord}
|
{#if cardImageRecord}
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<Preview record={cardImageRecord} size="small"/>
|
<Preview record={cardImageRecord} size="small" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
@@ -49,51 +40,22 @@
|
|||||||
class="record-title"
|
class="record-title"
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
>
|
>
|
||||||
|
{record.data.name}
|
||||||
{cardTitle}
|
|
||||||
{#if edge?._isTrashed}
|
|
||||||
<span class="trashed-text">will remove on save</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<small class="d-block">
|
<small class="d-block">
|
||||||
from {schema.label}
|
from {schema.label}
|
||||||
{#if record.status === "draft"}
|
{#if record.status === "draft"}
|
||||||
<Status status={record.status}/>
|
<Status status={record.status} />
|
||||||
{/if}
|
{/if}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if hasDelete}
|
{#if hasDelete}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
{#if edge?._isTrashed}
|
<button class="button" on:click={remove}>
|
||||||
<button
|
<Icon icon="trash-can" />
|
||||||
title="Restore"
|
|
||||||
class="button"
|
|
||||||
on:click={restore}
|
|
||||||
>
|
|
||||||
<Icon icon="undo"/>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
title="Delete from everywhere"
|
|
||||||
class="button"
|
|
||||||
on:click={fullDelete}
|
|
||||||
>
|
|
||||||
<Icon icon="destroy"/>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
title="Remove"
|
|
||||||
class="button"
|
|
||||||
on:click={remove}
|
|
||||||
>
|
|
||||||
<Icon icon="trash-can"/>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../../files/Preview.svelte";
|
|
||||||
import PreviewCardSmall from "../PreviewCardSmall.svelte";
|
import PreviewCardSmall from "../PreviewCardSmall.svelte";
|
||||||
|
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||||
|
|
||||||
export let field;
|
export let field;
|
||||||
export let side;
|
export let side;
|
||||||
@@ -8,26 +8,23 @@
|
|||||||
export let colorClass;
|
export let colorClass;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if ["reference", "file"].includes(field.info.name)}
|
{#if ["reference"].includes(field.info.name)}
|
||||||
<div class="{colorClass} field-content">
|
<div class="{colorClass} field-content">
|
||||||
<div class="d-flex align-items-center text-center flex-wrap">
|
<div class="d-flex align-items-center text-center flex-wrap">
|
||||||
{#each edges[field.name] as edgeRecord}
|
{#each edges[field.name] as edgeRecord}
|
||||||
{#if edgeRecord._file?.path}
|
<div class="ms-2">
|
||||||
<div
|
<PreviewCardSmall record={edgeRecord} />
|
||||||
class="ms-2 "
|
|
||||||
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
|
|
||||||
>
|
|
||||||
<Preview
|
|
||||||
record={edgeRecord}
|
|
||||||
size="small"
|
|
||||||
showFilename={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{/each}
|
||||||
<div class="ms-2 ">
|
</div>
|
||||||
<PreviewCardSmall record={edgeRecord}/>
|
</div>
|
||||||
|
{:else if ["file"].includes(field.info.name)}
|
||||||
|
<div class="{colorClass} field-content">
|
||||||
|
<div class="d-flex align-items-center text-center flex-wrap">
|
||||||
|
{#each side as aFile}
|
||||||
|
<div class="ms-2">
|
||||||
|
<PreviewFile file={aFile} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +40,6 @@
|
|||||||
|
|
||||||
<!-- {/if} -->
|
<!-- {/if} -->
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.field-content {
|
.field-content {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
Generated
+584
-3530
File diff suppressed because it is too large
Load Diff
+2
-7
@@ -11,20 +11,15 @@
|
|||||||
"@codemirror/lang-markdown": "^6.2.5",
|
"@codemirror/lang-markdown": "^6.2.5",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
"axios": "^1.7.4",
|
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"htmx.org": "^2.0.1",
|
|
||||||
"install": "^0.13.0",
|
|
||||||
"laravel-vite-plugin": "^1.0.5",
|
"laravel-vite-plugin": "^1.0.5",
|
||||||
"mustache": "^4.2.0",
|
"sass": "^1.77.8",
|
||||||
"npm": "^10.8.2",
|
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
"tinymce": "^6.8.4",
|
|
||||||
"trix": "^2.1.5",
|
"trix": "^2.1.5",
|
||||||
"vite": "5.4.6"
|
"vite": "5.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
.scope-login {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
.bg-image {
|
|
||||||
width: 50%;
|
|
||||||
background: url("/vendor/lucent/public/art.jpg");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form{
|
|
||||||
width: 50%;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
.autocomplete {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1000;
|
|
||||||
overflow: visible;
|
|
||||||
.autocomplete-option {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
&:hover {
|
|
||||||
background: var(--p40);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:focus-within {
|
|
||||||
.autocomplete-results{
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
.autocomplete-selected-value {
|
|
||||||
font-size: 13px;
|
|
||||||
margin-top: 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--p30);
|
|
||||||
padding: 3px 10px;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
line-height: 22px;
|
|
||||||
&:hover {
|
|
||||||
background: var(--p40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-results {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 10px;
|
|
||||||
overflow: visible;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 12px;
|
|
||||||
z-index: 20;
|
|
||||||
background: var(--p30);
|
|
||||||
//border: 1px solid var(--p40);
|
|
||||||
transition: 600ms;
|
|
||||||
flex-grow: 1;
|
|
||||||
top: 45px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.avatar {
|
|
||||||
/* Center the content */
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
/* Used to position the content */
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
/* Colors */
|
|
||||||
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
/* Rounded border */
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar__letters {
|
|
||||||
/* Center the content */
|
|
||||||
left: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
.button {
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--p20);
|
|
||||||
padding: 3px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 0px solid var(--p30);
|
|
||||||
font-size: 14px;
|
|
||||||
min-height: 27px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--p30);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--p50) !important;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--p30);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background: var(--p30);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--p40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: var(--p70);
|
|
||||||
color: var(--p10);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--p90);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: .7;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-button {
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
background: var(--p80) !important;
|
|
||||||
color: var(--p10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.spinner-border {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid var(--p10);
|
|
||||||
border-bottom-color: var(--p30);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: rotation 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotation {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
|
|
||||||
@supports (-webkit-appearance: none) or (-moz-appearance: none) {
|
|
||||||
.checkbox-wrapper input[type=checkbox] {
|
|
||||||
--active-inner: var(--p10);
|
|
||||||
--focus: 2px var(--p30);
|
|
||||||
--border-hover: var(--p30);
|
|
||||||
--disabled: #F6F8FF;
|
|
||||||
--disabled-inner: #E1E6F9;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
height: 21px;
|
|
||||||
outline: none;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--bc, var(--p30));
|
|
||||||
background: var(--b, var(--p10));
|
|
||||||
transition: background 0.3s, border-color 0.3s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
position: absolute;
|
|
||||||
transition: transform var(--d-t, 0.3s) var(--d-t-e, ease), opacity var(--d-o, 0.2s);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:checked {
|
|
||||||
--b: var(--p40);
|
|
||||||
--bc: var(--p40);
|
|
||||||
--d-o: .3s;
|
|
||||||
--d-t: .6s;
|
|
||||||
--d-t-e: cubic-bezier(.2, .85, .32, 1.2);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:disabled {
|
|
||||||
--b: var(--disabled);
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:disabled:checked {
|
|
||||||
--b: var(--disabled-inner);
|
|
||||||
--bc: var(--p40);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:disabled + label {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:hover:not(:checked):not(:disabled) {
|
|
||||||
--bc: var(--border-hover);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:focus {
|
|
||||||
box-shadow: 0 0 0 var(--focus);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch) {
|
|
||||||
width: 21px;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch):after {
|
|
||||||
opacity: var(--o, 0);
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch):checked {
|
|
||||||
--o: 1;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox] + label {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch) {
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch):after {
|
|
||||||
width: 5px;
|
|
||||||
height: 9px;
|
|
||||||
border: 2px solid var(--active-inner);
|
|
||||||
border-top: 0;
|
|
||||||
border-left: 0;
|
|
||||||
left: 7px;
|
|
||||||
top: 4px;
|
|
||||||
transform: rotate(var(--r, 20deg));
|
|
||||||
}
|
|
||||||
.checkbox-wrapper input[type=checkbox]:not(.switch):checked {
|
|
||||||
--r: 43deg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper * {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
.checkbox-wrapper *:before,
|
|
||||||
.checkbox-wrapper *:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper input[type=checkbox]:indeterminate {
|
|
||||||
--b: var(--p40);
|
|
||||||
--bc: var(--p40);
|
|
||||||
--d-o: .3s;
|
|
||||||
--d-t: .6s;
|
|
||||||
--d-t-e: cubic-bezier(.2, .85, .32, 1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
.is-editable-false{
|
|
||||||
.cm-content{
|
|
||||||
background-color: var(--p10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.cm-focused{
|
|
||||||
|
|
||||||
.cm-content{
|
|
||||||
background-color: var(--p10);
|
|
||||||
color: var(--p100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-content{
|
|
||||||
background-color: var(--p20);
|
|
||||||
|
|
||||||
}
|
|
||||||
.ͼ4 .cm-line ::selection, .ͼ4 .cm-line::selection{
|
|
||||||
background: var(--p40) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-activeLine{
|
|
||||||
background-color: var(--p20)!important;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
.flatpickr-wrapper {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-field {
|
|
||||||
.flatpickr-calendar {
|
|
||||||
border-radius: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-months .flatpickr-month {
|
|
||||||
background: var(--p30);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
|
||||||
background: var(--p30);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-weekdays{
|
|
||||||
background: var(--p30);
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-weekdaycontainer .flatpickr-weekday{
|
|
||||||
background: var(--p30);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-days{
|
|
||||||
background: var(--p10);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatpickr-time{
|
|
||||||
background: var(--p10);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
//
|
|
||||||
//:modal {
|
|
||||||
// background-color: beige;
|
|
||||||
// border: 2px solid burlywood;
|
|
||||||
// border-radius: 5px;
|
|
||||||
//}
|
|
||||||
|
|
||||||
html {
|
|
||||||
//scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:has(dialog[open]) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
margin: 2vh auto;
|
|
||||||
background-color: var(--p10);
|
|
||||||
padding: 34px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 96vh;
|
|
||||||
box-shadow: none!important;
|
|
||||||
//position: relative;
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-body {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
dialog::backdrop {
|
|
||||||
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
position: sticky;
|
|
||||||
top: -34px;
|
|
||||||
z-index: 999;
|
|
||||||
background-color: var(--p10);
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
.dropdown {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-button > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 10px;
|
|
||||||
overflow: visible;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 12px;
|
|
||||||
z-index: 22;
|
|
||||||
background: var(--p20);
|
|
||||||
transition: 600ms;
|
|
||||||
flex-grow: 1;
|
|
||||||
top: 35px;
|
|
||||||
min-width: max-content;
|
|
||||||
border: 1px solid var(--p30);
|
|
||||||
|
|
||||||
|
|
||||||
&.orientation-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.orientation-left {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-header, .dropdown-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-header {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--p30);
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: var(--p30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-field{
|
|
||||||
.dropdown-menu {
|
|
||||||
background: var(--p30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text],input[type=number],input[type=search],input[type=email],textarea{
|
|
||||||
width: 100%;
|
|
||||||
background: var(--p20);
|
|
||||||
border: 1px solid var(--p50);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 7px;
|
|
||||||
font-size: 16px;
|
|
||||||
&:focus{
|
|
||||||
background: var(--p10);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea{
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select{
|
|
||||||
width: 100%;
|
|
||||||
background: var(--p20);
|
|
||||||
border: 1px solid var(--p50);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 7px;
|
|
||||||
font-size: 16px;
|
|
||||||
&:focus{
|
|
||||||
background: var(--p10);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.htmx-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.htmx-request .htmx-indicator {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.htmx-request.htmx-indicator {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bt {
|
|
||||||
appearance: none;
|
|
||||||
background-color: #000;
|
|
||||||
background-image: none;
|
|
||||||
border: 1px solid #000;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: #fff 4px 4px 0 0, #000 4px 4px 0 1px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-family: ITCAvantGardeStd-Bk, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
margin: 0 5px 10px 0;
|
|
||||||
overflow: visible;
|
|
||||||
padding: 8px 40px;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: none;
|
|
||||||
touch-action: manipulation;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt:focus {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt:active {
|
|
||||||
box-shadow: rgba(0, 0, 0, .125) 0 3px 5px inset;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt:not([disabled]):active {
|
|
||||||
box-shadow: #fff 2px 2px 0 0, #000 2px 2px 0 1px;
|
|
||||||
transform: translate(2px, 2px);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
.mt-1{margin-top: 4px}
|
|
||||||
.mt-2{margin-top: 8px}
|
|
||||||
.mt-3{margin-top: 12px}
|
|
||||||
.mt-4{margin-top: 16px}
|
|
||||||
.mt-5{margin-top: 20px}
|
|
||||||
|
|
||||||
.mb-1{margin-bottom: 4px}
|
|
||||||
.mb-2{margin-bottom: 8px}
|
|
||||||
.mb-3{margin-bottom: 12px}
|
|
||||||
.mb-4{margin-bottom: 16px}
|
|
||||||
.mb-5{margin-bottom: 20px}
|
|
||||||
|
|
||||||
.pt-1{padding-top: 4px}
|
|
||||||
.pt-2{padding-top: 8px}
|
|
||||||
.pt-3{padding-top: 12px}
|
|
||||||
.pt-4{padding-top: 16px}
|
|
||||||
.pt-5{padding-top: 20px}
|
|
||||||
|
|
||||||
.pb-1{padding-bottom: 4px}
|
|
||||||
.pb-2{padding-bottom: 8px}
|
|
||||||
.pb-3{padding-bottom: 12px}
|
|
||||||
.pb-4{padding-bottom: 16px}
|
|
||||||
.pb-5{padding-bottom: 20px}
|
|
||||||
|
|
||||||
.gap-1{gap: 4px}
|
|
||||||
.gap-2{gap: 8px}
|
|
||||||
.gap-3{gap: 12px}
|
|
||||||
.gap-4{gap: 16px}
|
|
||||||
.gap-5{gap: 20px}
|
|
||||||
|
|
||||||
.hide{
|
|
||||||
display: none!important;
|
|
||||||
}
|
|
||||||
.hidden{
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-block{
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-inline-block{
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-bold{
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.in-place{
|
|
||||||
padding: 36px;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.sidebar-content{
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 400px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
position: relative;
|
|
||||||
width: fit-content;
|
|
||||||
min-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 40px;
|
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user