Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d2cafdf11 | |||
| e9c2e82bc3 | |||
| 49c1d5efd0 | |||
| 0e19c38f23 | |||
| b4521e92b8 | |||
| 2e95fca8ad | |||
| 02224eb580 | |||
| e74e1e7956 | |||
| d824e52dce | |||
| 322c48b78b | |||
| c649077e37 | |||
| b8efa5f586 | |||
| 8526fd471f | |||
| bb77a37ff7 | |||
| 1f03eebd08 | |||
| 137c338719 | |||
| 842bd71a18 |
@@ -6,4 +6,3 @@ front/node_modules
|
|||||||
front/npm-debug.log
|
front/npm-debug.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
/.claude
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "/phpactor.schema.json",
|
|
||||||
"language_server_phpstan.enabled": false
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ include_toc: true
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- PHP 8.3
|
- PHP 8.2
|
||||||
- Laravel 11
|
- Laravel 10
|
||||||
- Postgres or Sqlite database
|
- Postgres or Sqlite database
|
||||||
- ImageMagick
|
- ImageMagick
|
||||||
|
|
||||||
@@ -82,9 +82,7 @@ return [
|
|||||||
### Database
|
### Database
|
||||||
|
|
||||||
The recommended database for small website is sqlite. But you can also use postresql
|
The recommended database for small website is sqlite. But you can also use postresql
|
||||||
|
Make sure to delete the existing migration scripts in your database/migrations folder.
|
||||||
> [!CAUTION]
|
|
||||||
> Make sure to delete the existing migration scripts in your database/migrations folder.
|
|
||||||
|
|
||||||
Then run:
|
Then run:
|
||||||
|
|
||||||
@@ -92,26 +90,6 @@ Then run:
|
|||||||
php artisan migrate
|
php artisan migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
### File Storage
|
|
||||||
|
|
||||||
You can use your local filesystem or s3 compatible storage. Lucent expects you to have a valid configuration inside ``config/filesystems.php``
|
|
||||||
|
|
||||||
example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
return [
|
|
||||||
'disks' => [
|
|
||||||
'lucent' => [
|
|
||||||
'driver' => 'local',
|
|
||||||
'root' => storage_path('app/public'),
|
|
||||||
'url' => env('APP_URL').'/storage',
|
|
||||||
'visibility' => 'public',
|
|
||||||
'throw' => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### First user
|
### First user
|
||||||
|
|
||||||
To create your first user, head to your localhost:8000/lucent
|
To create your first user, head to your localhost:8000/lucent
|
||||||
|
|||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
# 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,
|
|
||||||
],
|
|
||||||
```
|
|
||||||
+11
-5
@@ -6,13 +6,17 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
|
"ext-sqlite3": "*",
|
||||||
"ext-imagick": "*",
|
"ext-imagick": "*",
|
||||||
"ext-pdo": "*",
|
"php": "^8.2",
|
||||||
"php": "^8.4",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"intervention/image": "^2.7",
|
"intervention/image": "^2.7",
|
||||||
"phpoption/phpoption": "^1.9",
|
|
||||||
"spatie/image-optimizer": "^1.6",
|
"spatie/image-optimizer": "^1.6",
|
||||||
"staudenmeir/laravel-cte": "^1.0"
|
"staudenmeir/laravel-cte": "^1.0",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"opis/json-schema": "^2.3",
|
||||||
|
"symfony/yaml": "^7.0",
|
||||||
|
"spatie/laravel-data": "^4.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^1.8",
|
"phpstan/phpstan": "^1.8",
|
||||||
@@ -24,7 +28,8 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/Response.php",
|
"src/Response.php",
|
||||||
"src/macros.php"
|
"src/macros.php",
|
||||||
|
"src/File/Uploader.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
@@ -36,4 +41,5 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2905
-698
File diff suppressed because it is too large
Load Diff
+298
-145
@@ -5,221 +5,374 @@ include_toc: true
|
|||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
|
|
||||||
Fields define the columns of a schema. Each field has a `ui` type that controls both storage and the admin UI component rendered.
|
Fields are similar to a table's columns in a relational databases.
|
||||||
|
|
||||||
## Common Optional Properties
|
## Available fields for Collections and Files
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
One-line text input.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
Multi-line text input.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
Slug input. Auto-generates from a source field if left empty.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`, `source`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
- **source**: The source field from which it generates
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
WYSIWYG rich text editor.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
### markdown
|
- **min**: Minimum characters
|
||||||
|
- **max**: Maximum characters
|
||||||
Markdown editor.
|
- **help**: Help text
|
||||||
|
- **default**: Default value when creating new record
|
||||||
**Required:** `name`, `label`
|
- **readonly**: Cannot edit this value from the UI
|
||||||
|
- **group**: The group that this field belongs to
|
||||||
| Property | Description |
|
|
||||||
|---|---|
|
|
||||||
| `min` | Minimum character count |
|
|
||||||
| `max` | Maximum character count |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### number
|
### number
|
||||||
|
Any numeric value
|
||||||
|
|
||||||
Numeric input.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
Boolean true/false toggle.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **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
|
||||||
|
- **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
|
||||||
|
|
||||||
Color picker.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
Date selector. Stores as a date string.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
|
|
||||||
Date and time selector. Stores as an ISO 8601 string.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
||||||
### uuid
|
- **min**: Minimum date
|
||||||
|
- **max**: Maximum date
|
||||||
UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID.
|
- **help**: Help text
|
||||||
|
- **default**: Default value when creating new record
|
||||||
**Required:** `name`, `label`
|
- **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
|
||||||
|
|
||||||
### json
|
### json
|
||||||
|
Json data
|
||||||
|
|
||||||
Raw JSON data field. Accepts either a JSON string or a plain array — both are stored as JSON.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`
|
- **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
|
||||||
|
- **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
|
||||||
|
|
||||||
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.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`, `collections`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
- **collections**: File collections to choose from
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `collections` | Array of file schema names to choose from |
|
- **mime**: The mime types allowed to select
|
||||||
| `mime` | Allowed MIME types e.g. `["image/*"]` |
|
- **nullable**: Can the field be saved as null
|
||||||
| `min` | Minimum number of files |
|
- **min**: Minimum files
|
||||||
| `max` | Maximum number of files |
|
- **max**: Maximum files
|
||||||
|
- **help**: Help text
|
||||||
|
- **group**: The group that this field belongs to
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### reference
|
### reference
|
||||||
|
Reference other records
|
||||||
|
|
||||||
Reference records from another collection.
|
required
|
||||||
|
|
||||||
**Required:** `name`, `label`, `collections`
|
- **name**: The id of the field
|
||||||
|
- **label**: The friendly name of the field
|
||||||
|
- **collections**: Collections to choose from
|
||||||
|
|
||||||
| Property | Description |
|
optional
|
||||||
|---|---|
|
|
||||||
| `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
|
|
||||||
|
|
||||||
```json
|
### block
|
||||||
{
|
The block editor
|
||||||
"ui": "text",
|
|
||||||
"name": "title",
|
required
|
||||||
"label": "Title",
|
|
||||||
"required": true,
|
- **name**: The id of the field
|
||||||
"min": 3,
|
- **label**: The friendly name of the field
|
||||||
"max": 200,
|
- **schema**: The block schema name
|
||||||
"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
|
||||||
+52
-133
@@ -5,169 +5,88 @@ include_toc: true
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
|
|
||||||
The `Query` class is the main way to fetch records. Inject it via the Laravel container.
|
## Graph or Tree
|
||||||
|
|
||||||
|
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
|
||||||
public function __construct(private Query $query) {}
|
$query->childrenDepth(2);
|
||||||
```
|
```
|
||||||
|
Maybe you only want to get a specific type of relationship:
|
||||||
|
|
||||||
## Return Formats
|
```php
|
||||||
|
$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
|
||||||
|
|
||||||
Filter keys use the format `field_operator`. When no operator suffix is given, `eq` is assumed.
|
You can filter your query with the following format:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->filter(["field_operator" => "value"]);
|
$query->filter(["field_operator" => "value"]);
|
||||||
|
|
||||||
// No operator = eq
|
// example:
|
||||||
$query->filter(["schema" => "blogPosts"]);
|
|
||||||
|
|
||||||
// With operator
|
$query->filter(["date_lt" => "2020-09-15"]);
|
||||||
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Operator Reference
|
Or filters are also available:
|
||||||
|
|
||||||
| 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(["schema" => "blogPosts"])
|
->filter(["price_eqn" => 10])
|
||||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"]);
|
->orFilter(["title_regex" => "search", "slug_regex" => "search"])
|
||||||
|
;
|
||||||
```
|
```
|
||||||
|
## Operator List
|
||||||
|
|
||||||
Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together.
|
- regex
|
||||||
|
- eq
|
||||||
|
- ne
|
||||||
|
- eqnum
|
||||||
|
- neqnum
|
||||||
|
- filter
|
||||||
|
- eqtrue
|
||||||
|
- eqfalse
|
||||||
|
- netrue
|
||||||
|
- nefalse
|
||||||
|
- in:
|
||||||
|
- innum
|
||||||
|
- nin
|
||||||
|
- ninnum
|
||||||
|
- lt
|
||||||
|
- lte
|
||||||
|
- gt
|
||||||
|
- gte
|
||||||
|
- null
|
||||||
|
- nnull
|
||||||
|
- exists
|
||||||
|
- nexists
|
||||||
|
|
||||||
### Cross-Schema (Children) Filters
|
## Example
|
||||||
|
|
||||||
You can filter records by properties of their related records using the `children.` prefix:
|
Get 10 posts from the sports category as a tree
|
||||||
|
|
||||||
```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" => "blogPosts",
|
"schema" => "posts",
|
||||||
"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();
|
|
||||||
```
|
|
||||||
|
|||||||
+41
-74
@@ -5,106 +5,76 @@ include_toc: true
|
|||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
||||||
Schemas define both the shape of your data and how the admin UI behaves.
|
Schemas define both the shape of your data and how the UI on the admin will behave.
|
||||||
|
|
||||||
There are 2 types of schemas:
|
There are 3 types of schemas
|
||||||
|
|
||||||
- **collection** — Regular data records
|
- Collections: Normal data
|
||||||
- **files** — Images and file uploads
|
- Files: Images and files
|
||||||
|
- Block: Used in the block editor
|
||||||
|
|
||||||
|
|
||||||
## Collection Reference
|
## Collection Reference
|
||||||
|
|
||||||
| Field | Required | Description |
|
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||||
|---|---|---|
|
- **label**: The friendly name of the schema
|
||||||
| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` |
|
- **type**: The type of the collection. Should be "collection"
|
||||||
| `label` | yes | Friendly display name |
|
- **visible**: An array of field id to show on the content browser _optional_
|
||||||
| `type` | yes | Must be `"collection"` |
|
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
||||||
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
- **fields**: The list of your fields. Look the field reference for more
|
||||||
| `visible` | no | Field IDs to show in the content browser list |
|
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _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` |
|
- **titleTemplate**: Mustache code to customize the preview field _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
|
||||||
|
|
||||||
| Field | Required | Description |
|
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||||
|---|---|---|
|
- **label**: The friendly name of the schema
|
||||||
| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` |
|
- **type**: The type of the collection. Should be "files"
|
||||||
| `label` | yes | Friendly display name |
|
- **path**: The relative directory that these files will be stored.
|
||||||
| `type` | yes | Must be `"files"` |
|
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
||||||
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
- **fields**: The list of your fields. Look the field reference for more
|
||||||
| `groups` | no | Group IDs to split fields into tabs |
|
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
||||||
| `isEntry` | no | Show in sidebar. Default: `false` |
|
- **sortBy**: The default sorting in the content browser _optional_
|
||||||
| `sortBy` | no | Default sort in browser |
|
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
||||||
| `cardTitle` | no | Mustache template for the record preview title |
|
- **revisions**: How many revisions are going to be kept for each record _optional_
|
||||||
| `cardImage` | no | Field name to use as the preview image |
|
- **read**: Array of user groups that have read permissions _optional_
|
||||||
| `revisions` | no | Number of revisions to keep per record |
|
- **write**: Array of user groups that have write permissions _optional_
|
||||||
| `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`.
|
|
||||||
|
|
||||||
|
|
||||||
## System Fields
|
## Block Reference
|
||||||
|
|
||||||
Every record automatically has these read-only system fields available in queries:
|
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||||
|
- **label**: The friendly name of the schema
|
||||||
| Field | Description |
|
- **type**: The type of the collection. Should be "block"
|
||||||
|---|---|
|
- **fields**: The list of your fields. Look the field reference for more
|
||||||
| `_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
|
A full Collection example without the fields:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schemas": [
|
"label": "Departments",
|
||||||
{
|
"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": [
|
||||||
"Content",
|
"Extra Info",
|
||||||
|
"Metadata",
|
||||||
"SEO"
|
"SEO"
|
||||||
],
|
],
|
||||||
"sortBy": "-_sys.createdAt",
|
"sortBy": "-_sys.createdAt",
|
||||||
"cardTitle": "{{title}}",
|
"titleTemplate": "{{name}} {{slug}}",
|
||||||
"cardImage": "cover",
|
|
||||||
"revisions": 15,
|
"revisions": 15,
|
||||||
"read": [
|
"read": [
|
||||||
"admin",
|
"admin",
|
||||||
@@ -116,8 +86,5 @@ Every record automatically has these read-only system fields available in querie
|
|||||||
"editors"
|
"editors"
|
||||||
],
|
],
|
||||||
"fields": []
|
"fields": []
|
||||||
}
|
|
||||||
],
|
|
||||||
"roles": ["admin", "editors", "reviewers"]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
+23
-82
@@ -5,94 +5,51 @@ To generate the static content of the website, lucent provides a helper class.
|
|||||||
|
|
||||||
## Laravel Command
|
## Laravel Command
|
||||||
|
|
||||||
Create an Artisan command and inject `StaticGenerator`:
|
Just create a command and name it as you like. Make sure to inject the StaticGenerator class.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
class GenerateStatic extends Command
|
public function __construct(
|
||||||
{
|
|
||||||
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"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`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.
|
## Redirect command
|
||||||
|
|
||||||
|
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
|
||||||
$writer->save("/blog/my-post", $html);
|
$this->staticGenerator->run(function ($writer) {
|
||||||
// writes to: storage/lucent/build/blog/my-post/index.html
|
$writer->createRedirect("/", "/el");
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
An optional third argument changes the file extension (default `"html"`).
|
## The Writer save command
|
||||||
|
|
||||||
|
In order to create an html file, you have to use the writer's save command
|
||||||
|
|
||||||
## Writer: createRedirect
|
The first argument is the url and the second is the rendered HTML. That's where the page classes do come handy.
|
||||||
|
|
||||||
Generates an HTML meta-refresh redirect. Useful for root-level locale redirects:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$writer->createRedirect("/", "/el");
|
$this->staticGenerator->run(function ($writer) {
|
||||||
$writer->createRedirect("/", "/el", "Redirecting", "Please wait...");
|
$writer->save("/", $this->ctx->render("homepage"));
|
||||||
|
$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 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:
|
All the generated html is stored on `storage/lucent/live`
|
||||||
|
In order to make it accessible you have to create symlink:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan lucent:livelink
|
php artisan lucent:livelink
|
||||||
```
|
```
|
||||||
|
|
||||||
The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost:
|
Now your static website is accessible in http://localhost:8000/live
|
||||||
|
|
||||||
|
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 / {
|
||||||
@@ -100,30 +57,14 @@ 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
|
||||||
|
|
||||||
Register your generate command in `config/lucent.php` so admin users can trigger it from the UI:
|
In your lucent.php config file you can define your command that is used to generate the static files:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
"commands" => [
|
"generateCommand" => "generate:static"
|
||||||
"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,113 +99,3 @@ 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
-212
File diff suppressed because one or more lines are too long
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+188
File diff suppressed because one or more lines are too long
Vendored
+6
-3
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"main.js": {
|
"main.js": {
|
||||||
"file": "assets/main-DH0OAeUr.js",
|
"file": "assets/main.7c3e8b7b.js",
|
||||||
"name": "main",
|
|
||||||
"src": "main.js",
|
"src": "main.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main-BVNnoznq.css"
|
"assets/main.587d6006.css"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"main.css": {
|
||||||
|
"file": "assets/main.587d6006.css",
|
||||||
|
"src": "main.css"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
+7
-131
@@ -1,156 +1,32 @@
|
|||||||
import { formatDistanceToNow, parseJSON, format, parse } 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) {
|
||||||
if (!date) {
|
if(!date){
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return format(parseJSON(date), "dd MMM yyyy");
|
return format(parseJSON(date), "dd MMM yyyy");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readableDatetime(date) {
|
export function readableDatetime(date) {
|
||||||
if (!date) {
|
if(!date){
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
return Math.random().toString(36).substring(2, length + 2);
|
||||||
.toString(36)
|
|
||||||
.substring(2, length + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clickOutside(node) {
|
|
||||||
const handleClick = (event) => {
|
|
||||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
|
||||||
node.dispatchEvent(new CustomEvent("click_outside", node));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", handleClick, true);
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
document.removeEventListener("click", handleClick, true);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apiFetch(url, options = {}) {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apiPost(url, body, options = {}) {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
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 || x == [];
|
|
||||||
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 isEqual(db_value?.[k], ed_value?.[k]);
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
if (isArray(db_value)) {
|
|
||||||
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;
|
|
||||||
|
|
||||||
// const ok = Object.keys,
|
|
||||||
// tx = typeof x,
|
|
||||||
// ty = typeof y;
|
|
||||||
// return x && y && tx === "object" && tx === ty
|
|
||||||
// ? ok(x).length === ok(y).length &&
|
|
||||||
// ok(x).every((key) => isEqual(x[key], y[key]))
|
|
||||||
// : x === y;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-6
@@ -1,11 +1,23 @@
|
|||||||
|
import {axiosInstance} from "./bootstrap";
|
||||||
import "../sass/app.scss";
|
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 * as bootstrap from "bootstrap";
|
||||||
|
import Mustache from "mustache";
|
||||||
|
|
||||||
// Mustache.escape = function (value) {
|
Mustache.escape = function (value) {
|
||||||
// return value;
|
return value;
|
||||||
// };
|
};
|
||||||
|
|
||||||
|
function enableTooltipsAnywhere() {
|
||||||
|
// Enable tooltips everywhere
|
||||||
|
let tooltipTriggerList = [].slice.call(
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
);
|
||||||
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Define all components
|
// Define all components
|
||||||
const entryComponents = {
|
const entryComponents = {
|
||||||
@@ -26,14 +38,17 @@ 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("json-" + componentId).innerHTML;
|
const jsonData = document.getElementById(
|
||||||
|
"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,
|
||||||
@@ -46,3 +61,4 @@ let loadSvelte = function () {
|
|||||||
|
|
||||||
// document.addEventListener("turbo:load", loadSvelte);
|
// document.addEventListener("turbo:load", loadSvelte);
|
||||||
document.addEventListener("DOMContentLoaded", loadSvelte);
|
document.addEventListener("DOMContentLoaded", loadSvelte);
|
||||||
|
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
|
||||||
|
|||||||
@@ -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,7 +21,10 @@
|
|||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
</script>
|
</script>
|
||||||
|
<div class="text-center">
|
||||||
<div>
|
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name}</a></h1>
|
||||||
<svelte:component this={components[view]} {channel} {title} {...data} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<svelte:component this={components[view]} {title} {...data}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
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 "./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";
|
||||||
import Header from "./layout/Header.svelte";
|
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
members: Members,
|
members: Members,
|
||||||
@@ -24,23 +23,19 @@
|
|||||||
export let data;
|
export let data;
|
||||||
// export let layout;
|
// export let layout;
|
||||||
export let channel;
|
export let channel;
|
||||||
|
export let sidebar;
|
||||||
|
export let axios;
|
||||||
export let readableSchemas;
|
export let readableSchemas;
|
||||||
|
|
||||||
|
|
||||||
|
setContext("axios", axios);
|
||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext(
|
setContext("readableSchemas", channel.schemas.filter((s) => readableSchemas.includes(s.name)));
|
||||||
"readableSchemas",
|
|
||||||
channel.schemas.filter((s) => readableSchemas.includes(s.name)),
|
|
||||||
);
|
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<Navbar {sidebar}/>
|
||||||
<div class="sidebar-content">
|
|
||||||
<Navbar schema={data.schema} />
|
<svelte:component this={components[view]} {title} {...data}/>
|
||||||
</div>
|
|
||||||
<div class="main-content">
|
|
||||||
<Header />
|
|
||||||
<svelte:component this={components[view]} {title} {...data} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import Avatar from "./account/Avatar.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
export let sidebar;
|
||||||
|
const channel = getContext("channel");
|
||||||
|
const user = getContext("user");
|
||||||
|
let contentIsOpen = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="lx-nav">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button on:click={(e) => contentIsOpen = true} class="btn btn-primary btn-sm d-xxl-none">« Content</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center ">
|
||||||
|
|
||||||
|
<!-- <div>-->
|
||||||
|
<!-- <form method="GET">-->
|
||||||
|
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
|
||||||
|
<!-- class="form-control" required/>-->
|
||||||
|
<!-- </form>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center ">
|
||||||
|
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||||
|
|
||||||
|
{#if channel.generateCommand}
|
||||||
|
<a href="{channel.lucentUrl}/build-report" class="btn btn-outline-primary btn-sm d-">Build website</a>
|
||||||
|
{/if}
|
||||||
|
<a class="nav-item" href="{channel.lucentUrl}/profile">
|
||||||
|
<Avatar side="28" name={user.name}/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="offcanvas offcanvas-start d-xxl-block show border-0 bg-primary-subtle " class:d-none={!contentIsOpen}
|
||||||
|
data-bs-scroll="true"
|
||||||
|
data-bs-backdrop="false"
|
||||||
|
tabindex="-1" aria-labelledby="offcanvasScrollingLabel">
|
||||||
|
<div class="offcanvas-body">
|
||||||
|
<button on:click={(e) => contentIsOpen = false} class="btn btn-primary btn-sm d-xxl-none mb-4">« close</button>
|
||||||
|
{@html sidebar}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,48 +1,39 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import {getContext} from "svelte";
|
||||||
import { apiPost } from "../../helpers";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let email = "";
|
let email = "";
|
||||||
let submitted = false;
|
let message = "";
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/login", { email: email })
|
axios
|
||||||
.then(() => {
|
.post(channel.lucentUrl + "/login", {
|
||||||
submitted = true;
|
email: email,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.then((response) => {
|
||||||
|
console.log(response)
|
||||||
|
message = "You will receive an email with a login link"
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="scope-login">
|
<div class="wrapper-tiny">
|
||||||
<div class="bg-image"></div>
|
{#if message}
|
||||||
<div class="login-form">
|
|
||||||
{#if submitted}
|
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<p>
|
{message}
|
||||||
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}>
|
||||||
<p>
|
<div class="mb-3">
|
||||||
Submit your email address and you will receive a <b
|
<label for="emailaddress" class="form-label">Email address</label>
|
||||||
>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}
|
||||||
@@ -52,17 +43,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="bt bt-primary">
|
|
||||||
Send email
|
<div class="text-center mt-5 d-block">
|
||||||
<img
|
<SpinnerButton label="Login"/>
|
||||||
alt="indicator"
|
</div>
|
||||||
id="indicator"
|
|
||||||
class="htmx-indicator"
|
|
||||||
src="/img/spinner.svg"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
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 { apiPost } from "../../helpers";
|
|
||||||
|
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -17,7 +16,8 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/account/update-name", {
|
axios
|
||||||
|
.post(channel.lucentUrl + "/account/update-name", {
|
||||||
name: name,
|
name: name,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data.error;
|
errorMessage = error.response?.data.error;
|
||||||
console.log({ errorMessage });
|
console.log({errorMessage});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/account/update-email", {
|
axios
|
||||||
|
.post(channel.lucentUrl + "/account/update-email", {
|
||||||
email: email,
|
email: email,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -41,47 +42,46 @@
|
|||||||
})
|
})
|
||||||
.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}/>
|
||||||
<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-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
class="form-control mb-3"
|
class="form-control"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update Name" />
|
<SpinnerButton label="Update"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form on:submit={saveEmail}>
|
<form on:submit={saveEmail}>
|
||||||
<div class="input-group mb-5">
|
<div class="input-group mb-3">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
class="form-control mb-3"
|
class="form-control"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update Email" />
|
<SpinnerButton label="Update"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a
|
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this
|
||||||
class="list-group-item list-group-item-action"
|
device</a>
|
||||||
href="{channel.lucentUrl}/logout">Logout from this device</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<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 = "";
|
||||||
@@ -13,7 +12,8 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/register", {
|
axios
|
||||||
|
.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,8 +50,10 @@
|
|||||||
/>
|
/>
|
||||||
</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,42 +1,43 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import {getContext} from "svelte";
|
||||||
import { apiPost } from "../../helpers";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
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();
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/verify", {
|
axios
|
||||||
|
.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>
|
||||||
|
|
||||||
<div class="scope-login">
|
<SuccessAlert bind:this={successAlert}/>
|
||||||
<div class="bg-image"></div>
|
<div class="wrapper-tiny">
|
||||||
<div class="login-form">
|
|
||||||
<div class="form">
|
|
||||||
<h2 class="mb-5">Welcome to Lucent</h2>
|
|
||||||
<form on:submit={login}>
|
<form on:submit={login}>
|
||||||
<button class="bt bt-primary">
|
<div class="mb-3 text-center">
|
||||||
Enter as {email}
|
<h3>Login as {email}</h3>
|
||||||
<img
|
|
||||||
alt="indicator"
|
</div>
|
||||||
id="indicator"
|
|
||||||
class="htmx-indicator"
|
|
||||||
src="/img/spinner.svg"
|
<div class="text-center mt-5 d-block">
|
||||||
/>
|
<SpinnerButton label="Enter"/>
|
||||||
</button>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="form-errors"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
/* 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,52 +0,0 @@
|
|||||||
<script>
|
|
||||||
|
|
||||||
import Selectlist from "./Selectlist.svelte";
|
|
||||||
import Icon from "../common/Icon.svelte";
|
|
||||||
|
|
||||||
let searchEl;
|
|
||||||
let search;
|
|
||||||
export let value;
|
|
||||||
export let field;
|
|
||||||
|
|
||||||
function handleSelect(){
|
|
||||||
searchEl.focus();
|
|
||||||
searchEl.blur()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="autocomplete">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
bind:value={search}
|
|
||||||
bind:this={searchEl}
|
|
||||||
placeholder="Search for options"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<div class="autocomplete-results">
|
|
||||||
<Selectlist
|
|
||||||
{field}
|
|
||||||
bind:value
|
|
||||||
bind:search
|
|
||||||
on:selected={handleSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if value}
|
|
||||||
<div class="autocomplete-selected-value">
|
|
||||||
{#if Array.isArray(field.selectOptions)}
|
|
||||||
{value}
|
|
||||||
{:else}
|
|
||||||
{field.selectOptions[value]}
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={(e) => (value = "")}
|
|
||||||
type="button"
|
|
||||||
class="button-text"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<Icon width={12} height={12} icon="close"></Icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import {createEventDispatcher} from "svelte";
|
|
||||||
|
|
||||||
export let field;
|
|
||||||
export let value;
|
|
||||||
export let search = "";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
|
|
||||||
function select(e, option) {
|
|
||||||
e.preventDefault();
|
|
||||||
value = option.value;
|
|
||||||
search = "";
|
|
||||||
dispatch("selected", {option: option})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function formatOptionsForSearch(listOptions) {
|
|
||||||
if (Array.isArray(listOptions)) {
|
|
||||||
return listOptions.map(value => {
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
label: value,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(listOptions).map(([k, v]) => {
|
|
||||||
return {
|
|
||||||
value: k,
|
|
||||||
label: v,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let formattedOptions = formatOptionsForSearch(field.selectOptions);
|
|
||||||
const fuse = new Fuse(formattedOptions, {
|
|
||||||
includeScore: false,
|
|
||||||
keys: ['value', 'label']
|
|
||||||
})
|
|
||||||
$: result = search === "" ? formattedOptions : fuse.search(search).map(resItem => resItem.item)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if result}
|
|
||||||
{#each result as suggestion (suggestion.value)}
|
|
||||||
<div
|
|
||||||
class="autocomplete-option"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
on:click={(e) => select(e, suggestion)}
|
|
||||||
on:keypress={(e) => select(e, suggestion)}
|
|
||||||
>
|
|
||||||
{suggestion.label}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
@@ -1,81 +1,71 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
import {getContext, onMount} from "svelte";
|
||||||
import { apiPost } from "../../helpers";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let title;
|
export let title;
|
||||||
export let command;
|
|
||||||
$: date = "";
|
$: date = "";
|
||||||
$: logs = "";
|
$: logs = "";
|
||||||
|
|
||||||
let anchorEl;
|
|
||||||
let inProgress = false;
|
let inProgress = false;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const eventSource = new EventSource(
|
const eventSource = new EventSource(channel.lucentUrl + "/build-report-source");
|
||||||
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();
|
|
||||||
};
|
}
|
||||||
eventSource.onerror = (e) => {
|
eventSource.onerror = (e) => {
|
||||||
console.log(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;
|
||||||
apiPost(channel.lucentUrl + "/command/" + command.signature).then(
|
|
||||||
(response) => {
|
axios.post(channel.lucentUrl + "/build").then(response => {
|
||||||
connect();
|
connect()
|
||||||
},
|
})
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
connect();
|
connect()
|
||||||
});
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="common-wrapper">
|
<div class="wrapper-tiny transparent mb-5">
|
||||||
<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
|
<button on:click={buildWebsite} class="btn btn-outline-primary btn-sm mb-3" disabled={inProgress}>Start Build
|
||||||
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"> Action in progress </span>
|
<span class="badge text-bg-warning">
|
||||||
|
Build in progress
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !inProgress && logs}
|
{#if !inProgress && logs}
|
||||||
<span class="badge text-bg-info"> Action completed </span>
|
<span class="badge text-bg-info">
|
||||||
|
Build completed
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre class="logs">{logs}
|
<pre>{logs}</pre>
|
||||||
<div bind:this={anchorEl}> </div>
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.logs {
|
|
||||||
max-height: 70vh;
|
|
||||||
overflow: scroll;
|
|
||||||
background: var(--p90);
|
|
||||||
color: var(--p10);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<script>
|
|
||||||
|
|
||||||
let checkboxEl = null;
|
|
||||||
|
|
||||||
export let indeterminate = false;
|
|
||||||
|
|
||||||
export let value;
|
|
||||||
export let checked = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="checkbox-wrapper">
|
|
||||||
<input bind:this={checkboxEl} on:change id="c1-13" type="checkbox" {value} {indeterminate} checked={checked}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,33 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import {clickOutside} from "../../helpers.js";
|
|
||||||
|
|
||||||
let dropdownMenu;
|
|
||||||
export let orientation = "left";
|
|
||||||
|
|
||||||
export function open() {
|
|
||||||
dropdownMenu.classList.remove("hide")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close() {
|
|
||||||
dropdownMenu.classList.add("hide")
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClickOutside() {
|
|
||||||
dropdownMenu.classList.add("hide")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export let width = "300";
|
||||||
|
let dropdownMenu;
|
||||||
|
export function hide(){
|
||||||
|
dropdownMenu.classList.remove("show")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<div class="dropdown">
|
|
||||||
<button
|
|
||||||
class="button dropdown-button"
|
|
||||||
type="button"
|
|
||||||
on:click={open}
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<slot name="button">Dropdown</slot>
|
|
||||||
</button>
|
|
||||||
<div bind:this={dropdownMenu} class="dropdown-menu hide orientation-{orientation}" use:clickOutside on:click_outside={handleClickOutside}>
|
|
||||||
<slot/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
|
||||||
|
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-auto-close="outside"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<slot name="button">Dropdown</slot>
|
||||||
|
</button>
|
||||||
|
<div bind:this={dropdownMenu} class="dropdown-menu" style="width:{width}px;">
|
||||||
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
<div class="notice notice-error" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<div class="title">Submission Errors</div>
|
{message}
|
||||||
<div class="content"> {message}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -105,29 +105,7 @@
|
|||||||
path: '<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>',
|
path: '<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>',
|
||||||
viewBox: "0 0 448 512",
|
viewBox: "0 0 448 512",
|
||||||
},
|
},
|
||||||
"close": {
|
|
||||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
},
|
|
||||||
"arrow-left": {
|
|
||||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
},
|
|
||||||
"list": {
|
|
||||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 8h10M9 12h10M9 16h10M4.99 8H5m-.02 4h.01m0 4H5"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
},
|
|
||||||
"ordered-list": {
|
|
||||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6h8m-8 6h8m-8 6h8M4 16a2 2 0 1 1 3.321 1.5L4 20h5M4 5l2-1v6m-2 0h4"/>',
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
},
|
|
||||||
"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"/>',
|
|
||||||
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 = "";
|
||||||
@@ -147,7 +125,6 @@
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
{stroke}
|
{stroke}
|
||||||
{fill}
|
{fill}
|
||||||
|
|
||||||
>
|
>
|
||||||
{@html selectedIcon.path}
|
{@html selectedIcon.path}
|
||||||
</svg>
|
</svg>
|
||||||
@@ -155,6 +132,5 @@
|
|||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import {onDestroy, onMount} from "svelte";
|
||||||
|
import offcanvas from "bootstrap/js/src/offcanvas.js";
|
||||||
|
export let title = "";
|
||||||
|
let offCanvasEl;
|
||||||
|
let offCanvasInstance;
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
if(!offCanvasInstance){
|
||||||
|
offCanvasInstance = new offcanvas(offCanvasEl);
|
||||||
|
}
|
||||||
|
offCanvasInstance.show();
|
||||||
|
}
|
||||||
|
onMount(()=>{
|
||||||
|
offCanvasInstance = new offcanvas(offCanvasEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
offCanvasInstance.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={offCanvasEl} class="offcanvas offcanvas-end" tabindex="-1"
|
||||||
|
aria-labelledby="offcanvasEditContent">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title">{title}</h5>
|
||||||
|
<button type="button" on:click={hide} class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body" style="overflow: auto">
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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="btn btn-primary 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>
|
||||||
|
|||||||
@@ -12,10 +12,21 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isVisible}
|
{#if isVisible}
|
||||||
<div class="notice notice-success" transition:fly={{ duration: 500 }} role="alert">
|
<div
|
||||||
<div class="title">Success</div>
|
transition:fly={{ duration: 500 }}
|
||||||
<div class="content"> {message}</div>
|
class="lx-alert text-white bg-success border-1 border rounded px-3 py-0 text-center"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lx-alert {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 45px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<script>
|
|
||||||
|
|
||||||
|
|
||||||
export let value;
|
|
||||||
export let checked = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input type="checkbox" {value} on:change
|
|
||||||
class="switch" {checked}/>
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import {uniqueId} from "lodash";
|
||||||
|
|
||||||
|
export let label = "";
|
||||||
|
let id = uniqueId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id={id} checked>
|
||||||
|
<label class="form-check-label" for={id}>{label}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import {getContext} from "svelte";
|
||||||
import { apiPost } from "../../helpers";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let selected;
|
export let selected;
|
||||||
@@ -9,7 +8,8 @@
|
|||||||
|
|
||||||
function deleteRecords(e) {
|
function deleteRecords(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
apiPost(channel.lucentUrl + "/records/delete", {
|
axios
|
||||||
|
.post(channel.lucentUrl + "/records/delete", {
|
||||||
ids: selected.map((s) => s.id),
|
ids: selected.map((s) => s.id),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -21,9 +21,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeStatus(e, status) {
|
function changeStatus(e, status) {
|
||||||
apiPost(channel.lucentUrl + "/records/status/" + status, {
|
axios
|
||||||
|
.post(channel.lucentUrl + "/records/status/" + status, {
|
||||||
schemaName: schema.name,
|
schemaName: schema.name,
|
||||||
records: selected,
|
records: selected
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -34,47 +35,49 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="display: flex;align-items: center; gap: 8px">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<span class="me-2">{selected.length} records selected</span>
|
<span class="me-2">{selected.length} records selected</span>
|
||||||
|
<div class="btn-group " role="group" aria-label="Basic example">
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary">Publish
|
||||||
>Publish
|
</button
|
||||||
</button>
|
>
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary">Make Draft
|
||||||
>Make Draft
|
</button
|
||||||
</button>
|
>
|
||||||
{#if filter["status_in"] === "trashed"}
|
{#if filter["status_in"] === "trashed"}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary">Publish
|
||||||
>Publish
|
</button
|
||||||
</button>
|
>
|
||||||
{#if schema.hasDrafts}
|
{#if schema.hasDrafts}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary">Make Draft
|
||||||
>Make Draft
|
</button
|
||||||
</button>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={deleteRecords}
|
on:click|preventDefault={deleteRecords}
|
||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary">Delete forever
|
||||||
>Delete forever
|
</button
|
||||||
</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"
|
class="btn btn-sm btn-outline-primary">Move to trash
|
||||||
>Move to trash
|
</button
|
||||||
</button>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import {selectRecord} from "./functions/recordSelect.js";
|
||||||
|
import Preview from "../newPreview/Preview.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
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 queryRecord (queryRecord.record.id)}
|
||||||
|
<div class="col-6 col-md-4">
|
||||||
|
<div
|
||||||
|
class="file-wrapper rounded p-2 mb-4 bg-light"
|
||||||
|
class:selected={selected.includes(queryRecord)}
|
||||||
|
>
|
||||||
|
{#if isWritable}
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
on:change={() => select(queryRecord.record)}
|
||||||
|
class="form-check-input "
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.find(
|
||||||
|
(r) => r.id === queryRecord.record.id
|
||||||
|
)}
|
||||||
|
value={queryRecord}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<Preview record={queryRecord.record} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
|
||||||
|
title={queryRecord.record._file.path}
|
||||||
|
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
||||||
|
style="
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
">{queryRecord.record._file.path}</a
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="lx-small-text text-muted d-block text-center"
|
||||||
|
>{queryRecord.record._file.mime}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-check {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import Tools from "./tools/Tools.svelte";
|
// import Tools from "./tools/Tools.svelte";
|
||||||
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";
|
import Grid from "./Grid.svelte";
|
||||||
|
import Tools from "./tools/Tools.svelte";
|
||||||
|
|
||||||
|
const axios = getContext("axios");
|
||||||
export let schema;
|
export let schema;
|
||||||
export let users;
|
export let users;
|
||||||
export let records;
|
export let records;
|
||||||
export let graph;
|
|
||||||
// export let visibleFields;
|
// export let visibleFields;
|
||||||
export let systemFields;
|
export let systemFields;
|
||||||
export let sortParam;
|
export let sortParam;
|
||||||
@@ -26,18 +27,18 @@
|
|||||||
|
|
||||||
function refresh(e) {
|
function refresh(e) {
|
||||||
const newUrl = e.detail;
|
const newUrl = e.detail;
|
||||||
apiGet(newUrl)
|
axios
|
||||||
|
.get(newUrl)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
records = response.records;
|
records = response.data.records;
|
||||||
sortParam = response.sortParam;
|
sortParam = response.data.sortParam;
|
||||||
sortField = response.sortField;
|
sortField = response.data.sortField;
|
||||||
operators = response.operators;
|
operators = response.data.operators;
|
||||||
filter = response.filter;
|
filter = response.data.filter;
|
||||||
skip = response.skip;
|
skip = response.data.skip;
|
||||||
limit = response.limit;
|
limit = response.data.limit;
|
||||||
total = response.total;
|
total = response.data.total;
|
||||||
modalUrl = response.modalUrl;
|
modalUrl = response.data.modalUrl;
|
||||||
document.querySelector("dialog h3").scrollIntoView();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -45,13 +46,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="">
|
<div class="wrapper-large transparent ">
|
||||||
<div class={inModal ? "mt-0" : "mt-5"}>
|
<div class="lx-card mb-4 mt-0">
|
||||||
<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
|
||||||
@@ -61,7 +62,6 @@
|
|||||||
{sortField}
|
{sortField}
|
||||||
{operators}
|
{operators}
|
||||||
{filter}
|
{filter}
|
||||||
{graph}
|
|
||||||
{inModal}
|
{inModal}
|
||||||
{modalUrl}
|
{modalUrl}
|
||||||
{isWritable}
|
{isWritable}
|
||||||
@@ -69,9 +69,9 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if schema.type === "collection"}
|
||||||
<Table
|
<Table
|
||||||
{records}
|
{records}
|
||||||
{graph}
|
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
@@ -81,6 +81,15 @@
|
|||||||
{isWritable}
|
{isWritable}
|
||||||
bind:selected
|
bind:selected
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<Grid
|
||||||
|
{records}
|
||||||
|
{schema}
|
||||||
|
{isWritable}
|
||||||
|
bind:selected
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -92,3 +101,4 @@
|
|||||||
{modalUrl}
|
{modalUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
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;
|
||||||
export let graph;
|
export let queryRecord;
|
||||||
export let record;
|
|
||||||
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}
|
||||||
@@ -19,48 +19,40 @@
|
|||||||
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 {queryRecord} {field}/>
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
{#if schema.visible?.includes("_sys.status")}
|
{#if schema.visible.includes("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={queryRecord.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 ||
|
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
|
||||||
"_sys.createdBy" == sortParam}
|
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record.createdBy)} side={24} />
|
<Avatar name={usernameById(users, queryRecord.record._sys.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 ||
|
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
|
||||||
"_sys.updatedBy" == sortParam}
|
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
<Avatar name={usernameById(users, queryRecord.record._sys.updatedBy)} side={24}/>
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.createdAt")}
|
{#if schema.visible.includes("_sys.createdAt")}
|
||||||
<td
|
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
|
||||||
class:is-sort={"-_sys.createdAt" == sortParam ||
|
{friendlyDate(queryRecord.record._sys.createdAt)}
|
||||||
"_sys.createdAt" == sortParam}
|
|
||||||
>
|
|
||||||
{friendlyDate(record.createdAt)}
|
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.updatedAt")}
|
{#if schema.visible.includes("_sys.updatedAt")}
|
||||||
<td
|
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
|
||||||
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
{friendlyDate(queryRecord.record._sys.updatedAt)}
|
||||||
"_sys.updatedAt" == sortParam}
|
|
||||||
>
|
|
||||||
{friendlyDate(record.updatedAt)}
|
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -29,17 +29,13 @@
|
|||||||
file: File,
|
file: File,
|
||||||
};
|
};
|
||||||
export let field;
|
export let field;
|
||||||
export let schema;
|
export let queryRecord;
|
||||||
export let record;
|
|
||||||
export let graph;
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={renderElements[field.info.name]}
|
this={renderElements[field.info.name]}
|
||||||
value={record.data[field.name]}
|
value={queryRecord.record.data[field.name]}
|
||||||
{record}
|
{queryRecord}
|
||||||
{graph}
|
|
||||||
{schema}
|
|
||||||
{field}
|
{field}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import RecordRow from "./RecordRow.svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import { usernameById } from "../account/users";
|
import {previewTitle} from "../records/Preview";
|
||||||
import { getContext } from "svelte";
|
import {usernameById} from "../account/users";
|
||||||
|
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";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let users;
|
export let users;
|
||||||
export let records;
|
export let records;
|
||||||
export let graph;
|
|
||||||
export let systemFields;
|
export let systemFields;
|
||||||
export let sortParam;
|
export let sortParam;
|
||||||
export let sortField;
|
export let sortField;
|
||||||
@@ -20,31 +19,31 @@
|
|||||||
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(
|
$: visibleColumns = schema.fields.filter(c => schema.visible.includes(c.name))
|
||||||
(c) => schema.visible?.includes(c.name) ?? [],
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table mt-5">
|
<div class="lx-table rounded">
|
||||||
<table>
|
<table class="">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<th>
|
<th>
|
||||||
<Checkbox
|
<input
|
||||||
value=""
|
on:change|preventDefault={eventToggleAll}
|
||||||
on:change={eventToggleAll}
|
|
||||||
indeterminate={selected.length > 0 &&
|
indeterminate={selected.length > 0 &&
|
||||||
selected.length < records.length}
|
selected.length < records.length}
|
||||||
checked={selected.length === records.length}
|
checked={selected.length == records.length}
|
||||||
></Checkbox>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -53,63 +52,72 @@
|
|||||||
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}>{field.label}</th
|
title={field.help}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top">{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}
|
<th>{sysField.label}</th>
|
||||||
>{sysField.label}</th
|
|
||||||
>
|
|
||||||
{/each}
|
{/each}
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each records as record (record.id)}
|
{#each records as queryRecord (queryRecord.record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title-td">
|
<td class="title-td">
|
||||||
<div class="title-td-contents">
|
<div
|
||||||
|
class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center ">
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<Checkbox
|
<div class="form-check">
|
||||||
on:change={() => select(record)}
|
<input
|
||||||
|
on:change={() => select(queryRecord.record)}
|
||||||
|
class="form-check-input "
|
||||||
|
type="checkbox"
|
||||||
checked={selected.find(
|
checked={selected.find(
|
||||||
(r) => r.id === record.id,
|
(r) => r.id === queryRecord.record.id
|
||||||
)}
|
)}
|
||||||
value={record}
|
value={queryRecord}
|
||||||
></Checkbox>
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
|
class="me-2 text-decoration-none text-dark fs-6"
|
||||||
|
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
|
||||||
target={inModal ? "_blank" : "_self"}
|
target={inModal ? "_blank" : "_self"}
|
||||||
|
title={previewTitle(queryRecord.record)}
|
||||||
|
data-bs-toggle="tooltip" data-bs-placement="left"
|
||||||
|
|
||||||
>
|
>
|
||||||
{#if record.status === "draft"}
|
{previewTitle(queryRecord.record)}
|
||||||
<span
|
|
||||||
style="text-transform: uppercase;font-size:10px"
|
|
||||||
>{record.status}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{record.data.name}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Avatar
|
||||||
|
name={usernameById(
|
||||||
|
users,
|
||||||
|
queryRecord.record._sys.updatedBy
|
||||||
|
)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<RecordRow
|
<RecordRow
|
||||||
{record}
|
{queryRecord}
|
||||||
{graph}
|
|
||||||
{schema}
|
{schema}
|
||||||
{visibleColumns}
|
{visibleColumns}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
{users}
|
{users}
|
||||||
/>
|
/>
|
||||||
<td>
|
|
||||||
<Avatar
|
|
||||||
name={usernameById(users, record.updatedBy)}
|
|
||||||
side={24}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
|
|
||||||
export let record;
|
export let queryRecord;
|
||||||
export let field;
|
export let field;
|
||||||
export let graph;
|
|
||||||
|
|
||||||
|
let filePreviews = queryRecord?._children[field.name];
|
||||||
|
|
||||||
let filePreviews = graph.edges?.filter((ed) => ed.field === field.name && ed.source === record.id)
|
|
||||||
.map((ed) => graph.records.find((r) => r.id === ed.target));
|
|
||||||
// if (edges[0]) {
|
|
||||||
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
|
|
||||||
// }
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- {#if firstRecord}
|
||||||
|
<Preview record={firstRecord} size="tiny" />
|
||||||
|
{/if} -->
|
||||||
<div class="d-flex me-1">
|
<div class="d-flex me-1">
|
||||||
{#each filePreviews as file}
|
{#each filePreviews as file}
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
|
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
|
||||||
|
|
||||||
export let record;
|
export let queryRecord;
|
||||||
export let field;
|
export let field;
|
||||||
export let schemas;
|
$: recordEdges = queryRecord?._children[field.name];
|
||||||
export let graph;
|
|
||||||
|
|
||||||
$: recordEdges =
|
|
||||||
graph.edges
|
|
||||||
?.filter((ed) => ed.field === field.name && ed.source === record.id)
|
|
||||||
.map((edge) => {
|
|
||||||
return graph.records.find((r) => r.id === edge.target);
|
|
||||||
})
|
|
||||||
.filter((record) => (!record ? false : true)) ?? [];
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="references">
|
<div class="references">
|
||||||
{#each recordEdges as recordEdge}
|
{#each recordEdges as recordEdge}
|
||||||
<span class="reference">
|
<span class="mr-3">
|
||||||
<PreviewCardSmall {schemas} {graph} record={recordEdge}/>
|
<PreviewCardSmall record={recordEdge}/>
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each pages as i}
|
{#each pages as i}
|
||||||
<li class="page-item" class:active={currentPage === i}>
|
<li class="page-item">
|
||||||
{#if currentPage === i}
|
{#if currentPage == i}
|
||||||
<span class="page-link active">{i}</span>
|
<span class="page-link active">{i}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
|
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
import { range } from "lodash";
|
||||||
import NavItem from "./NavItem.svelte";
|
import NavItem from "./NavItem.svelte";
|
||||||
export let inModal;
|
export let inModal;
|
||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
@@ -11,11 +11,7 @@
|
|||||||
|
|
||||||
$: 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;
|
||||||
});
|
});
|
||||||
@@ -47,7 +43,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination">
|
<ul class="pagination justify-content-center">
|
||||||
{#if totalPages > 1}
|
{#if totalPages > 1}
|
||||||
<li class="page-item disabled" class:disabled={currentPage === 1}>
|
<li class="page-item disabled" class:disabled={currentPage === 1}>
|
||||||
<a on:click={first} href="/" class="page-link"> First </a>
|
<a on:click={first} href="/" class="page-link"> First </a>
|
||||||
@@ -73,7 +69,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<p style="display: flex;justify-content: center; gap: 4px">
|
<p class="text-muted text-center">
|
||||||
Showing
|
Showing
|
||||||
<span class="font-medium">{+skip + 1}</span>
|
<span class="font-medium">{+skip + 1}</span>
|
||||||
to
|
to
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import {createEventDispatcher, getContext} from "svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import {previewTitle} from "../../records/Preview";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
export let operators;
|
export let operators;
|
||||||
@@ -9,7 +10,8 @@
|
|||||||
export let value;
|
export let value;
|
||||||
export let inModal;
|
export let inModal;
|
||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
export let graph;
|
export let records
|
||||||
|
|
||||||
let filter = {
|
let filter = {
|
||||||
label: "",
|
label: "",
|
||||||
operator: "",
|
operator: "",
|
||||||
@@ -17,10 +19,10 @@
|
|||||||
isReference: key.startsWith("children"),
|
isReference: key.startsWith("children"),
|
||||||
};
|
};
|
||||||
|
|
||||||
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
|
filter = [
|
||||||
(mem, fn) => fn(mem),
|
extractOperator(key),
|
||||||
filter,
|
extractLabel(schema, key),
|
||||||
);
|
].reduce((mem, fn) => fn(mem), filter);
|
||||||
|
|
||||||
function extractOperator(key) {
|
function extractOperator(key) {
|
||||||
return (filter) => {
|
return (filter) => {
|
||||||
@@ -45,16 +47,17 @@
|
|||||||
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);
|
}
|
||||||
|
|
||||||
function extractFilterRecord(graph, value) {
|
const filterRecord = extractFilterRecord(records, value);
|
||||||
|
|
||||||
|
function extractFilterRecord(records, value) {
|
||||||
if (!filter.isReference) {
|
if (!filter.isReference) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return graph.records.find((r) => r.id === value);
|
return records.find(r => r.id === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFilter(k) {
|
function removeFilter(k) {
|
||||||
@@ -70,22 +73,30 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="applied-filter">
|
<span class="applied-filter d-inline-block border border-primary rounded lx-small-text me-1 px-2 py-1">
|
||||||
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
{#if filter.isReference && filterRecord}
|
{#if filter.isReference && filterRecord}
|
||||||
{filter.label} is {filterRecord.data.name}
|
{filter.label} is {previewTitle(filterRecord)}
|
||||||
{:else}
|
{:else}
|
||||||
{filter.label}
|
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {value}
|
||||||
{operators.find((o) => o.name === filter.operator)?.symbol ?? ""}
|
|
||||||
{operators.find((o) => o.name === filter.operator)?.hasValue
|
|
||||||
? value
|
|
||||||
: ""}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={() => removeFilter(key)}
|
on:click|preventDefault={() => removeFilter(key)}
|
||||||
type="button"
|
type="button"
|
||||||
class="button-text"
|
class="btn-close btn-close ms-1"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
><Icon width={12} height={12} icon="close"></Icon></button
|
/>
|
||||||
>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.applied-filter {
|
||||||
|
background-color: #fff;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.applied-filter:hover {
|
||||||
|
opacity: .8;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
export let inModal;
|
|
||||||
export let modalUrl;
|
|
||||||
const url = new URL(modalUrl ?? window.location.href);
|
|
||||||
|
|
||||||
function removeFilter(k) {
|
|
||||||
|
|
||||||
const url = new URL(modalUrl ?? window.location.href);
|
|
||||||
url.searchParams.set("skip", "0");
|
|
||||||
url.searchParams.delete("notlinked");
|
|
||||||
if (inModal) {
|
|
||||||
dispatch("refresh", url);
|
|
||||||
} else {
|
|
||||||
window.location.replace(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{#if url.searchParams.get("notlinked")}
|
|
||||||
<span class="applied-filter">
|
|
||||||
|
|
||||||
Not linked
|
|
||||||
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={() => removeFilter()}
|
|
||||||
type="button"
|
|
||||||
class="button-text"
|
|
||||||
aria-label="Close"
|
|
||||||
><Icon width={12} height={12} icon="close"></Icon></button>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import {createEventDispatcher} from "svelte";
|
import {createEventDispatcher} from "svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
|
||||||
import FilterReferenceInput from "./FilterReferenceInput.svelte";
|
import FilterReferenceInput from "./FilterReferenceInput.svelte";
|
||||||
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
@@ -14,10 +14,50 @@
|
|||||||
let dropdown;
|
let dropdown;
|
||||||
let search = "";
|
let search = "";
|
||||||
let systemFieldsFiltered = systemFields;
|
let systemFieldsFiltered = systemFields;
|
||||||
if (schema.type === "collection") {
|
if (schema.type == "collection") {
|
||||||
systemFieldsFiltered = systemFields.filter((f) => f.files === false);
|
systemFieldsFiltered = systemFields.filter((f) => f.files === false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filterableFields = [...schema.fields, ...systemFieldsFiltered].filter(
|
||||||
|
(f) => !["file", "json"].includes(f.info?.name ?? f.ui)
|
||||||
|
);
|
||||||
|
let selectedField;
|
||||||
|
|
||||||
|
let selectedInput = "";
|
||||||
|
$: operatorsFiltered = operators.filter(
|
||||||
|
(o) => o.uis.includes(selectedField?.info?.name) || o.uis[0] == "*"
|
||||||
|
);
|
||||||
|
|
||||||
|
$: selectedOperator = operatorsFiltered[0];
|
||||||
|
|
||||||
|
function addFilter(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let filterPrefix = "";
|
||||||
|
let filterKey;
|
||||||
|
if (schema.fields.find(f => f.name === selectedField.name)) {
|
||||||
|
|
||||||
|
if (selectedField.info.name == "reference" && selectedOperator.name == "eq") {
|
||||||
|
filterPrefix = "children." + selectedField.name + ".id";
|
||||||
|
filterKey = `filter[${filterPrefix}]`;
|
||||||
|
} else {
|
||||||
|
filterPrefix = "data.";
|
||||||
|
filterKey = `filter[${filterPrefix + selectedField.name}_${selectedOperator.name}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
url.searchParams.set("skip", "0");
|
||||||
|
url.searchParams.set(filterKey, selectedInput);
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
dropdown.hide()
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function submitSearch(e) {
|
function submitSearch(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let filterKeyValue = search.split("=")[0] ?? "";
|
let filterKeyValue = search.split("=")[0] ?? "";
|
||||||
@@ -39,195 +79,63 @@
|
|||||||
} else {
|
} else {
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
resetFilters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// New Start
|
|
||||||
let selectedInput = null;
|
|
||||||
let activeField = null;
|
|
||||||
let activeReference = null;
|
|
||||||
let activeOperator = null;
|
|
||||||
let activeMenu = "main";
|
|
||||||
let activeOperators = null;
|
|
||||||
let dataFields = [...schema.fields, ...systemFieldsFiltered].filter(
|
|
||||||
(f) => !["file", "json", "reference"].includes(f.info?.name ?? f.ui)
|
|
||||||
);
|
|
||||||
let referenceFields = [...schema.fields].filter(
|
|
||||||
(f) => ["reference"].includes(f.info?.name ?? f.ui)
|
|
||||||
);
|
|
||||||
|
|
||||||
function selectField(e, field) {
|
|
||||||
activeField = field;
|
|
||||||
activeOperators = operators.filter(
|
|
||||||
(o) => o.uis.includes(activeField?.info?.name) || o.uis[0] === "*"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectReference(e, field) {
|
|
||||||
activeReference = field;
|
|
||||||
activeOperator = operators.find(o => o.name === "eq")
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOperator(e, operator) {
|
|
||||||
activeOperator = operator;
|
|
||||||
if (!operator.hasValue) {
|
|
||||||
applyFilter(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFilter(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
let filterPrefix = "";
|
|
||||||
let filterKey;
|
|
||||||
let selectedField = activeField ?? activeReference;
|
|
||||||
if (schema.fields.find(f => f.name === selectedField.name)) {
|
|
||||||
if (selectedField.info.name === "reference" && activeOperator.name === "eq") {
|
|
||||||
filterPrefix = "children." + selectedField.name + ".id";
|
|
||||||
filterKey = `filter[${filterPrefix}]`;
|
|
||||||
} else {
|
|
||||||
filterPrefix = "data.";
|
|
||||||
filterKey = `filter[${filterPrefix + selectedField.name}_${activeOperator.name}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(modalUrl ?? window.location.href);
|
|
||||||
url.searchParams.set("skip", "0");
|
|
||||||
url.searchParams.set(filterKey, selectedInput);
|
|
||||||
if (inModal) {
|
|
||||||
dispatch("refresh", url);
|
|
||||||
dropdown.close()
|
|
||||||
} else {
|
|
||||||
window.location.href = url.toString();
|
|
||||||
}
|
|
||||||
resetFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters() {
|
|
||||||
activeField = null;
|
|
||||||
activeOperator = null;
|
|
||||||
activeMenu = "main";
|
|
||||||
activeReference = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-2 d-flex align-items-center">
|
||||||
<div>
|
<Dropdown bind:this={dropdown} width="300">
|
||||||
<Dropdown bind:this={dropdown}>
|
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
<Icon icon="filter"/>
|
<Icon icon="filter"/>
|
||||||
<span class="ms-1">Filter</span>
|
<span class="ms-1">Filter</span>
|
||||||
</div>
|
</div>
|
||||||
<div class:hide={activeMenu !== "main"}>
|
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "byField" }>
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
Filter by field
|
<select bind:value={selectedField} class="form-select">
|
||||||
</button>
|
{#each filterableFields as field}
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "byReference" }>
|
<option value={field}>{field.label}</option>
|
||||||
Filter by Reference
|
{/each}
|
||||||
</button>
|
</select>
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "advanced" }>
|
|
||||||
Advanced filter
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class:hide={activeMenu !== "byField"}>
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
{#if !activeField}
|
<select class="form-select" bind:value={selectedOperator}>
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
{#each operatorsFiltered as operator}
|
||||||
<Icon icon="arrow-left"></Icon>
|
<option value={operator}>{operator.label}</option>
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
{#each dataFields as field}
|
|
||||||
<button class="dropdown-item button" on:click={e => selectField(e,field)}>
|
|
||||||
{field.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
</select>
|
||||||
|
</div>
|
||||||
{#if activeField && !activeOperator}
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
<button class="dropdown-item button" on:click={e => activeField = null }>
|
{#if selectedField?.info?.name === "reference" && selectedOperator.name === "eq"}
|
||||||
<Icon icon="arrow-left"></Icon>
|
<FilterReferenceInput field={selectedField} bind:value={selectedInput} on:addFilter={addFilter}/>
|
||||||
Back
|
{:else}
|
||||||
</button>
|
|
||||||
<div class="selected-filter">field: {activeField.label}</div>
|
|
||||||
|
|
||||||
{#each activeOperators as operator}
|
|
||||||
<button class="dropdown-item button" on:click={e => selectOperator(e,operator)}>
|
|
||||||
{operator.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#if activeField && activeOperator}
|
|
||||||
<button class="dropdown-item button" on:click={e => activeOperator = null }>
|
|
||||||
<Icon icon="arrow-left"></Icon>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<div class="selected-filter">field: {activeField.label} operator: {activeOperator.label}</div>
|
|
||||||
<div class="filter-input">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={selectedInput}
|
bind:value={selectedInput}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
<button
|
<button
|
||||||
on:click={applyFilter}
|
on:click={addFilter}
|
||||||
class="button applied-filter"
|
class="btn btn-outline-primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Add filter
|
Add filter
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class:hide={activeMenu !== "byReference"}>
|
|
||||||
{#if !activeReference}
|
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
|
||||||
<Icon icon="arrow-left"></Icon>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
{#each referenceFields as field}
|
|
||||||
<button class="dropdown-item button" on:click={e => selectReference(e,field)}>
|
|
||||||
{field.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#if activeReference}
|
|
||||||
<button class="dropdown-item button" on:click={e => activeReference = null }>
|
|
||||||
<Icon icon="arrow-left"></Icon>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<div class="selected-filter">field: {activeReference.label}</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<FilterReferenceInput field={activeReference} bind:value={selectedInput}
|
|
||||||
on:addFilter={applyFilter}/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
<hr/>
|
||||||
</div>
|
<div><h6 class="dropdown-header">Advanced filters</h6></div>
|
||||||
<div class:hide={activeMenu !== "advanced"}>
|
|
||||||
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
|
||||||
<Icon icon="arrow-left"></Icon>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<form on:submit={submitSearch}>
|
<form on:submit={submitSearch}>
|
||||||
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
<input
|
<input
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
type="search"
|
type="search"
|
||||||
class="mb-2 mt-2"
|
class="form-control"
|
||||||
placeholder="Advanced filters"
|
placeholder="Advanced filters"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="button applied-filter">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import {createEventDispatcher, getContext} from "svelte";
|
||||||
import { apiGet, debounce } from "../../../helpers";
|
import {debounce} from "lodash";
|
||||||
|
import {previewTitle} from "../../records/Preview";
|
||||||
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -8,11 +10,13 @@
|
|||||||
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) => {
|
||||||
apiGet(channel.lucentUrl + "/records/suggestions", {
|
axios
|
||||||
|
.get(channel.lucentUrl + "/records/suggestions", {
|
||||||
params: {
|
params: {
|
||||||
schema: field.collections[0],
|
schema: field.collections[0],
|
||||||
field: "search",
|
field: "search",
|
||||||
@@ -21,7 +25,7 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = response;
|
searchOptions = response.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
@@ -31,36 +35,44 @@
|
|||||||
|
|
||||||
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">
|
<input
|
||||||
<input
|
|
||||||
type="search"
|
type="search"
|
||||||
on:keyup={updateResults}
|
on:keyup={updateResults}
|
||||||
|
class="form-control dropdown-toggle"
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
placeholder={"Search for " + field.label}
|
placeholder={"Search for "+field.label}
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="dropdown-menu w-100">
|
||||||
|
|
||||||
<div class="reference-tags-results">
|
|
||||||
{#if searchOptions}
|
{#if searchOptions}
|
||||||
{#each searchOptions as option (option.id)}
|
{#each searchOptions as option (option.id)}
|
||||||
<div
|
<div
|
||||||
class="reference-tags-option"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
on:click={(e) => apply(e, option)}
|
on:click={(e) => apply(e, option)}
|
||||||
on:keypress={(e) => apply(e, option)}
|
on:keypress={(e) => apply(e, option)}
|
||||||
>
|
>
|
||||||
{option.data.name}
|
<span class="dropdown-item">
|
||||||
|
{previewTitle( option)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="start-typing">Start typing...</div>
|
|
||||||
|
Start typing...
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import {createEventDispatcher} from "svelte";
|
import {createEventDispatcher} from "svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
@@ -43,80 +42,90 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
<Dropdown>
|
<button
|
||||||
<div slot="button">
|
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-auto-close="outside"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
{#if sortParam.startsWith("-")}
|
{#if sortParam.startsWith("-")}
|
||||||
<Icon icon="arrow-down-wide-short"/>
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="arrow-up-short-wide"/>
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="ms-1">{sortField.label}</span>
|
<span class="ms-1">{sortField.label}</span>
|
||||||
</div>
|
</button>
|
||||||
<div>
|
<div class="dropdown-menu" style="width:auto;max-width:800px;">
|
||||||
|
<div class="row">
|
||||||
{#each sortableFields as field}
|
{#each sortableFields as field}
|
||||||
<div class="dropdown-item">
|
<div class="col-4 px-3 py-1 d-flex align-items-center">
|
||||||
|
<div class="btn-group w-100">
|
||||||
<button
|
<button
|
||||||
on:click={(e) => sortAsc(e, field)}
|
on:click={(e) => sortAsc(e, field)}
|
||||||
title="Sort Ascending"
|
title="Sort Ascending"
|
||||||
class="button button-icon {field.name == sortField.name && !sortParam.startsWith("-")
|
class="btn btn-sm {field.name == sortField.name && !sortParam.startsWith("-")
|
||||||
? 'active'
|
? 'btn-primary'
|
||||||
: ''} "
|
: 'btn-outline-primary'} "
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
<Icon icon="arrow-up-short-wide"/>
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={(e) => sortDesc(e, field)}
|
on:click={(e) => sortDesc(e, field)}
|
||||||
title="Sort Descending"
|
title="Sort Descending"
|
||||||
class="button button-icon {field.name == sortField.name && sortParam.startsWith("-")
|
class="btn btn-sm {field.name == sortField.name && sortParam.startsWith("-")
|
||||||
? 'active'
|
? 'btn-primary'
|
||||||
: ''} "
|
: 'btn-outline-primary'} "
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
<Icon icon="arrow-down-wide-short"/>
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Sort Ascending"
|
title="Sort Ascending"
|
||||||
on:click={(e) => sortAsc(e, field)}
|
on:click={(e) => sortAsc(e, field)}
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
|
||||||
|
style="overflow: hidden;"
|
||||||
>
|
>
|
||||||
{field.label}
|
{field.label}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<h6 class="dropdown-header">System</h6>
|
</div>
|
||||||
|
<h6 class="dropdown-header px-0">System</h6>
|
||||||
|
<div class="row">
|
||||||
{#each systemFieldsFiltered as field}
|
{#each systemFieldsFiltered as field}
|
||||||
<div class="dropdown-item">
|
<div class="col-4 px-3 py-1 d-flex align-items-center">
|
||||||
|
<div class="btn-group w-100">
|
||||||
<button
|
<button
|
||||||
on:click={(e) => sortAsc(e, field)}
|
on:click={(e) => sortAsc(e, field)}
|
||||||
title="Sort Ascending"
|
title="Sort Ascending"
|
||||||
class="button button-icon {field.name == sortParam
|
class="btn btn-sm {field.name == sortParam
|
||||||
? 'active'
|
? 'btn-primary'
|
||||||
: ''} "
|
: 'btn-outline-primary'} "
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up-short-wide"/>
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={(e) => sortDesc(e, field)}
|
on:click={(e) => sortDesc(e, field)}
|
||||||
title="Sort Descending"
|
title="Sort Descending"
|
||||||
class="button button-icon {'-' + field.name == sortParam
|
class="btn btn-sm {'-' + field.name == sortParam
|
||||||
? 'active'
|
? 'btn-primary'
|
||||||
: ''} "
|
: 'btn-outline-primary'} "
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-down-wide-short"/>
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Sort Ascending"
|
title="Sort Ascending"
|
||||||
on:click={(e) => sortAsc(e, field)}
|
on:click={(e) => sortAsc(e, field)}
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
|
||||||
|
style="overflow: hidden;"
|
||||||
>
|
>
|
||||||
{field.label}
|
{field.label}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<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 {getContext, createEventDispatcher} from "svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
|
||||||
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
export let isWritable;
|
export let isWritable;
|
||||||
export let records;
|
export let records;
|
||||||
export let graph;
|
|
||||||
export let systemFields = [];
|
export let systemFields = [];
|
||||||
// export let visibleFields = [];
|
// export let visibleFields = [];
|
||||||
|
|
||||||
@@ -38,13 +36,19 @@
|
|||||||
if (inModal) {
|
if (inModal) {
|
||||||
dispatch("refresh", url);
|
dispatch("refresh", url);
|
||||||
} else {
|
} else {
|
||||||
window.location = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadComplete(e) {
|
||||||
|
records = e.detail;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="mb-3 d-flex align-items-center justify-content-between">
|
||||||
<div class="toolbar-filters">
|
<div class=" d-flex align-items-center">
|
||||||
|
|
||||||
<SortFields
|
<SortFields
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
@@ -55,6 +59,7 @@
|
|||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FilterFields
|
<FilterFields
|
||||||
bind:schema
|
bind:schema
|
||||||
{systemFields}
|
{systemFields}
|
||||||
@@ -66,62 +71,78 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<form method="GET" on:submit={search}>
|
<form method="GET" on:submit={search}>
|
||||||
<input
|
<input type="search" name="filter[search_regex]" placeholder="Search"
|
||||||
type="search"
|
class="form-control" required>
|
||||||
name="filter[search_regex]"
|
|
||||||
placeholder="Search"
|
|
||||||
class="search"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items: center;gap:4px">
|
<div class="d-flex align-items-center ">
|
||||||
|
{#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}"
|
||||||
class="button"
|
class="btn btn-sm btn-primary"
|
||||||
>
|
>
|
||||||
New Record
|
New Record
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else }
|
||||||
{#if !inModal}
|
<div class="d-inline-block ms-1">
|
||||||
<Dropdown orientation="right">
|
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
||||||
<div slot="button">
|
|
||||||
<Icon icon="ellipsis-vertical" />
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !inModal}
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<Icon icon="ellipsis-vertical"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu">
|
||||||
{#if filter["status_in"] === "trashed"}
|
{#if filter["status_in"] === "trashed"}
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
||||||
>
|
>
|
||||||
Empty trash
|
Empty trash
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href={csvUrl}
|
||||||
|
>Export to CSV</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<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"
|
||||||
>View trashed records</a
|
>View trashed records</a
|
||||||
>
|
>
|
||||||
<a
|
</li>
|
||||||
class="dropdown-item"
|
|
||||||
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
|
|
||||||
>View unlinked records</a
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Dropdown>
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="applied-filters">
|
|
||||||
<AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
|
{#if Object.entries(filter).length > 0}
|
||||||
></AppliedFilterNotLinked>
|
<div class=" d-flex mb-3">
|
||||||
{#if Object.entries(filter).length > 0}
|
|
||||||
{#each Object.entries(filter) as [k, v]}
|
{#each Object.entries(filter) as [k, v]}
|
||||||
<AppliedFilter
|
<AppliedFilter
|
||||||
{schema}
|
{schema}
|
||||||
@@ -130,9 +151,10 @@
|
|||||||
value={v}
|
value={v}
|
||||||
{inModal}
|
{inModal}
|
||||||
{modalUrl}
|
{modalUrl}
|
||||||
{graph}
|
{records}
|
||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
|
||||||
import Icon from "../common/Icon.svelte";
|
|
||||||
import Index from "../content/Index.svelte";
|
|
||||||
import { apiGet } from "../../helpers";
|
|
||||||
|
|
||||||
let dialogEl;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
const channel = getContext("channel");
|
|
||||||
$: data = {};
|
|
||||||
let selectedRecords = [];
|
|
||||||
// onMount(() => {
|
|
||||||
// load();
|
|
||||||
// });
|
|
||||||
|
|
||||||
export function close(e) {
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogEl.close();
|
|
||||||
selectedRecords = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function load(schema) {
|
|
||||||
apiGet(channel.lucentUrl + "/content/" + schema)
|
|
||||||
.then((response) => {
|
|
||||||
data = response;
|
|
||||||
})
|
|
||||||
.catch((error) => console.log(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("insert", {
|
|
||||||
records: selectedRecords,
|
|
||||||
action: "insert",
|
|
||||||
schema: data.schema.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function replace(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch("insert", {
|
|
||||||
records: selectedRecords,
|
|
||||||
action: "replace",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function open(schema) {
|
|
||||||
dialogEl.showModal();
|
|
||||||
load(schema);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<dialog bind:this={dialogEl}>
|
|
||||||
{#if data.schema}
|
|
||||||
<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">
|
|
||||||
<Index {...data} bind:selected={selectedRecords}></Index>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</dialog>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script>
|
|
||||||
|
|
||||||
import Icon from "../common/Icon.svelte";
|
|
||||||
|
|
||||||
let dialogEl;
|
|
||||||
|
|
||||||
$: data = {};
|
|
||||||
|
|
||||||
export function close(e) {
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
dialogEl.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function open() {
|
|
||||||
dialogEl.showModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<dialog bind:this={dialogEl}>
|
|
||||||
<div class="dialog-header">
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={close}
|
|
||||||
type="button"
|
|
||||||
class="button close"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<Icon icon="close"></Icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog-body" style="min-width: 900px">
|
|
||||||
<slot/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</dialog>
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import {createEventDispatcher, getContext} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const channel = getContext("channel");
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
export function open() {
|
||||||
|
isOpen = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal fade show"
|
||||||
|
tabindex="-1"
|
||||||
|
class:d-block={isOpen}
|
||||||
|
aria-modal="true"
|
||||||
|
role="dialog"
|
||||||
|
style="background: rgba(100,100,100,.6);"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => (isOpen = false)}
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-dialog {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
margin: 40px auto;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
export function sortByField(from, to, edges, fieldName, references) {
|
export function sortByField(from, to, queryRecords, fieldName) {
|
||||||
if (from === to) {
|
if (from === to) {
|
||||||
return edges;
|
return queryRecords;
|
||||||
}
|
}
|
||||||
let referenceIds = references.map((r) => r.id);
|
let edgesTosort = queryRecords?.filter((qr) => qr.edge.field === fieldName && qr.edge.depth === 1 ) ?? [];
|
||||||
let edgesTosort =
|
let remainingEdge = queryRecords?.filter((qr) => !(qr.edge.field === fieldName && qr.edge.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];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function array_move(arr, old_index, new_index) {
|
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--) {
|
||||||
@@ -26,4 +18,4 @@ export 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,17 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import { imgurl } from "./imageserver.js";
|
import {imgurl} from "../files/imageserver";
|
||||||
import { getContext } from "svelte";
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
export let file;
|
export let record;
|
||||||
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;
|
||||||
@@ -31,48 +29,40 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="display: flex;align-items: center;gap: 5px;">
|
{#if record}
|
||||||
{#if file}
|
{#if record._file.mime.startsWith("image")}
|
||||||
{#if file.mime.startsWith("image")}
|
|
||||||
<!-- href={imgurl(record)} -->
|
<!-- href={imgurl(record)} -->
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/files/{file.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
title={file.filename}
|
title={record._file.path}
|
||||||
|
class="d-flex align-items-center justify-content-center "
|
||||||
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, file)}
|
src={imgurl(record)}
|
||||||
alt={file.path}
|
alt={record._file.path}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/files/{file.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
title={file.path}
|
title={record._file.path}
|
||||||
class="file-preview-small"
|
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
|
||||||
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" style="font-size:{fontSize}px"
|
||||||
>.{file.path.split(".").pop().toLowerCase()}</span
|
>.{record._file.path.split(".").pop()}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if showFilename}
|
{#if showFilename}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/files/{file.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
title={file.path}
|
title={record._file.path}
|
||||||
class="preview-file-filename lx-small-text text-decoration-none"
|
class="preview-file-filename lx-small-text text-decoration-none"
|
||||||
>{file.path}
|
>{record._file.path}</a
|
||||||
</a>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
img {
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
</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 recordId;
|
export let schema;
|
||||||
let mimeTypes = "";
|
let mimeTypes = "";
|
||||||
let files = [];
|
let files = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
@@ -17,41 +17,42 @@
|
|||||||
files = e.target.files ? [...e.target.files] : [];
|
files = e.target.files ? [...e.target.files] : [];
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
formData.append("recordId", recordId);
|
formData.append("schema", schema.name);
|
||||||
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);
|
||||||
fetch(channel.lucentUrl + "/files/upload", {
|
axios
|
||||||
method: "POST",
|
.post(channel.lucentUrl + "/files/upload", formData, {
|
||||||
body: formData,
|
|
||||||
headers: {
|
headers: {
|
||||||
"X-CSRF-TOKEN": document.querySelector(
|
"Content-Type": "multipart/form-data",
|
||||||
'meta[name="csrf-token"]',
|
|
||||||
).content,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => {
|
||||||
.then((data) => {
|
if (response.data.error) {
|
||||||
if (data.error) {
|
dispatch("uploadError", response.data.error);
|
||||||
dispatch("uploadError", data.error);
|
|
||||||
} else {
|
} else {
|
||||||
dispatch("uploadComplete", data);
|
dispatch("uploadComplete", response.data);
|
||||||
}
|
}
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
isLoading = false;
|
||||||
|
console.log(error.response.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset class="upload-button" disabled={isLoading}>
|
<fieldset disabled={isLoading}>
|
||||||
<label class="button primary btn-spinner">
|
<label class="btn btn-primary btn-sm btn-spinner ">
|
||||||
|
Upload file
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
>
|
||||||
Upload file
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
on:input={upload}
|
on:input={upload}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
|||||||
@@ -1,29 +1,15 @@
|
|||||||
export function imgurl(channel, file) {
|
import {getContext} from "svelte";
|
||||||
if (file.mime === "image/svg+xml") {
|
|
||||||
return fileurl(channel, file);
|
export function imgurl(record) {
|
||||||
|
|
||||||
|
if(record._file.mime === "image/svg+xml"){
|
||||||
|
return fileurl(record);
|
||||||
}
|
}
|
||||||
return channel.filesUrl + `/thumbs/${file.path}`;
|
const channel = getContext("channel")
|
||||||
|
return channel.filesUrl + `/thumbs/${record._file.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileurl(channel, file) {
|
export function fileurl(record) {
|
||||||
return channel.filesUrl + `/${file.path}`;
|
const channel = getContext("channel")
|
||||||
}
|
return channel.filesUrl + `/${record._file.path}`;
|
||||||
|
|
||||||
export function htmlurl(channel, file, preset) {
|
|
||||||
let html = "";
|
|
||||||
let url = fileurl(channel, file);
|
|
||||||
|
|
||||||
if (file.width > 0) {
|
|
||||||
let presetUrl = url;
|
|
||||||
if (preset) {
|
|
||||||
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
|
|
||||||
}
|
|
||||||
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
|
||||||
} else if (file.mime === "image/svg+xml") {
|
|
||||||
html = `<img src="${url}" alt="${file.path}"/>`;
|
|
||||||
} else {
|
|
||||||
html = `<a href="${url}">${file.originalName}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
export let label;
|
||||||
|
export let name;
|
||||||
|
export let group;
|
||||||
|
export let value;
|
||||||
|
export let help;
|
||||||
|
let id = uniqueId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
{value}
|
||||||
|
{name}
|
||||||
|
bind:group
|
||||||
|
{id}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{#if help}
|
||||||
|
<span class="text-muted">{help}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
|
||||||
import RecordRow from "./RecordRow.svelte";
|
import {getContext, onMount} from "svelte";
|
||||||
import { apiGet } from "../../helpers";
|
import RecordRow from "./RecordRow.svelte"
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let records = [];
|
let records = [];
|
||||||
let graph = null;
|
let graph = null;
|
||||||
let users = [];
|
let users = [];
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
apiGet(channel.lucentUrl + "/home/records")
|
axios
|
||||||
.then((data) => {
|
.get(channel.lucentUrl + "/home/records")
|
||||||
records = data.records;
|
.then((response) => {
|
||||||
users = data.users;
|
records = response.data.records;
|
||||||
|
graph = response.data.graph;
|
||||||
|
users = response.data.users;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -19,17 +21,24 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
<div class="wrapper-normal transparent">
|
||||||
{#if records.length > 0}
|
|
||||||
<div class="table">
|
<h3 class="header-small mb-4 ">Latest Content changes</h3>
|
||||||
|
{#if records.length > 0}
|
||||||
|
<div class="lx-card mb-4">
|
||||||
|
<div class="lx-table p-0">
|
||||||
<table class="">
|
<table class="">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<RecordRow {record} {users} />
|
<RecordRow {graph} {record} {users}/>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,48 @@
|
|||||||
<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 Status from "../records/Status.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(parseJSON(record.updatedAt), {
|
let frieldlyUpdatedAt = formatDistanceToNow(
|
||||||
addSuffix: true,
|
parseJSON(record._sys.updatedAt),
|
||||||
});
|
{addSuffix: true}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div class="row-name">
|
|
||||||
{#if record.status === "draft"}
|
|
||||||
<span class="status">DRAFT</span>
|
|
||||||
{/if}
|
|
||||||
{#if schema.type === "files"}
|
{#if schema.type === "files"}
|
||||||
<Preview {record} size="tiny" showFilename={true} />
|
<Preview {record} size="tiny"/>
|
||||||
{:else}
|
{:else}
|
||||||
<a href="{channel.lucentUrl}/records/{record.id}">
|
<a
|
||||||
{record.data.name}
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
class="text-decoration-none text-dark d-block"
|
||||||
|
>
|
||||||
|
{previewTitle(channel.schemas, record, graph)}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td><a
|
||||||
|
class="text-decoration-none lx-small-text"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
<Status status={record.status}/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex;gap: 14px">
|
<div class="d-flex">
|
||||||
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
{frieldlyUpdatedAt}
|
{frieldlyUpdatedAt}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Avatar from "../account/Avatar.svelte";
|
|
||||||
import { getContext } from "svelte";
|
|
||||||
import Dropdown from "../common/Dropdown.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const user = getContext("user");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="top-nav">
|
|
||||||
{#if channel.auth == "lucent"}
|
|
||||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
|
||||||
{/if}
|
|
||||||
{#if channel.commands.length > 0}
|
|
||||||
<Dropdown>
|
|
||||||
<div slot="button">Actions</div>
|
|
||||||
{#each channel.commands as command}
|
|
||||||
<a
|
|
||||||
href="{channel.lucentUrl}/command-report/{command.signature}"
|
|
||||||
class="top-nav-item">{command.name}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</Dropdown>
|
|
||||||
{/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">
|
|
||||||
<Avatar side="28" name={user.name} />
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<Avatar side="28" name={user.name} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { getContext } from "svelte";
|
|
||||||
|
|
||||||
export let schema;
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const readableSchemas = getContext("readableSchemas");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="sidebar-top">
|
|
||||||
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar">
|
|
||||||
{#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>
|
|
||||||
@@ -1,42 +1,20 @@
|
|||||||
<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 {onMount, onDestroy} 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 {EditorState, Compartment} 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;
|
||||||
export let value;
|
export let value;
|
||||||
export let editable = true;
|
export let editable = true;
|
||||||
|
|
||||||
export function insertMedia(info) {
|
|
||||||
let insertText = "";
|
|
||||||
if (info.file.width > 0) {
|
|
||||||
insertText = ``;
|
|
||||||
} else {
|
|
||||||
insertText = `[${info.file.filename}](${info.originalUrl})`;
|
|
||||||
}
|
|
||||||
const cursor = codeMirrorView.state.selection.main.head;
|
|
||||||
const transaction = codeMirrorView.state.update({
|
|
||||||
changes: {
|
|
||||||
from: cursor,
|
|
||||||
insert: insertText,
|
|
||||||
},
|
|
||||||
// the next 2 lines will set the appropriate cursor position after inserting the new text.
|
|
||||||
selection: { anchor: cursor + 1 },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (transaction) {
|
|
||||||
codeMirrorView.dispatch(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let language = new Compartment();
|
let language = new Compartment();
|
||||||
let tabSize = new Compartment();
|
let tabSize = new Compartment();
|
||||||
@@ -45,7 +23,11 @@
|
|||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
keymap.of([
|
||||||
|
indentWithTab,
|
||||||
|
...lintKeymap,
|
||||||
|
...completionKeymap
|
||||||
|
]),
|
||||||
language.of(markdown()),
|
language.of(markdown()),
|
||||||
markdown(),
|
markdown(),
|
||||||
autocompletion(),
|
autocompletion(),
|
||||||
@@ -58,14 +40,18 @@
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
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(() => {
|
||||||
@@ -75,4 +61,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="is-editable-{editable}" bind:this={parentElement} />
|
<div class="is-editable-{editable}" bind:this={parentElement}/>
|
||||||
|
|||||||
@@ -1,39 +1,59 @@
|
|||||||
<script>
|
<script>
|
||||||
import Sortable from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
import {createEventDispatcher, onMount} from "svelte";
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let sortableClass = "";
|
export let sortableClass = "";
|
||||||
// export let handle;
|
// export let handle;
|
||||||
export let isTable = false;
|
export let isTable = false;
|
||||||
export let sortableInstance;
|
export let sortableInstance = null;
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let sortableContainer;
|
let sortableContainer;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let options = {
|
let options = {
|
||||||
|
// handle: ".sortable-handle",
|
||||||
|
// draggable: ".quote-line-wrapper",
|
||||||
|
// filter: ".not-draggable", // Selectors that do not lead to dragging (String or Function)
|
||||||
|
// preventOnFilter: true,
|
||||||
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
|
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
|
||||||
easing: "cubic-bezier(1, 0, 0, 1)",
|
easing: "cubic-bezier(1, 0, 0, 1)",
|
||||||
direction: 'vertical',
|
|
||||||
onUpdate: function (/**Event*/ evt) {
|
onUpdate: function (/**Event*/ evt) {
|
||||||
|
// reorder(evt.oldIndex,evt.newIndex);
|
||||||
|
// console.log(evt)
|
||||||
dispatch("update", {
|
dispatch("update", {
|
||||||
source: evt.oldIndex,
|
source: evt.oldIndex,
|
||||||
target: evt.newIndex,
|
target: evt.newIndex,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
onMove(event) {
|
||||||
|
// if (event.related.className.indexOf("not-draggable") > -1) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if (handle) {
|
||||||
|
// options.handle = handle;
|
||||||
|
// }
|
||||||
sortableInstance = Sortable.create(sortableContainer, options);
|
sortableInstance = Sortable.create(sortableContainer, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// function reorder(from, to) {
|
||||||
|
// let newList = JSON.parse(JSON.stringify(value));
|
||||||
|
// let fromElem = newList[from];
|
||||||
|
// newList.splice(from, 1);
|
||||||
|
// newList.splice(to, 0, fromElem);
|
||||||
|
// value = newList;
|
||||||
|
// dispatch("reordered", value);
|
||||||
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isTable}
|
{#if isTable}
|
||||||
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||||
<slot/>
|
<slot />
|
||||||
</tbody>
|
</tbody>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||||
<slot/>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<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";
|
||||||
|
|
||||||
|
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(),
|
||||||
|
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,
|
||||||
|
content_style:
|
||||||
|
"img {max-width: 100%;height: auto;",
|
||||||
|
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});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
<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,66 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
|
||||||
import Trix from "trix";
|
|
||||||
import "trix/dist/trix.css";
|
|
||||||
|
|
||||||
export let value = "";
|
|
||||||
export let field;
|
|
||||||
let editor;
|
|
||||||
|
|
||||||
function updateValue(e) {
|
|
||||||
value = e.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertMedia(info) {
|
|
||||||
if (info.file.width > 0) {
|
|
||||||
var attachment = new Trix.Attachment({ content: info.html });
|
|
||||||
editor.editor.insertAttachment(attachment);
|
|
||||||
} else {
|
|
||||||
editor.editor.insertHTML(
|
|
||||||
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
editor.addEventListener("trix-file-accept", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.addEventListener("trix-before-initialize", (e) => {
|
|
||||||
Trix.config.blockAttributes.heading1.tagName = "h2";
|
|
||||||
const { toolbarElement } = e.target;
|
|
||||||
const h1Button = toolbarElement.querySelector(
|
|
||||||
"[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(() => {
|
|
||||||
// editor.removeEventListener("trix-before-initialize")
|
|
||||||
// })
|
|
||||||
|
|
||||||
Trix.config.blockAttributes.default.breakOnReturn = false;
|
|
||||||
Trix.config.blockAttributes.heading3 = {
|
|
||||||
tagName: "h3",
|
|
||||||
terminal: true,
|
|
||||||
breakOnReturn: true,
|
|
||||||
group: false,
|
|
||||||
};
|
|
||||||
// console.log(Trix.config)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="tox-wrapper">
|
|
||||||
<input id="x-{field.name}" {value} type="hidden" />
|
|
||||||
<trix-editor
|
|
||||||
bind:this={editor}
|
|
||||||
class=" content"
|
|
||||||
input="x-{field.name}"
|
|
||||||
role="textbox"
|
|
||||||
tabindex="0"
|
|
||||||
on:trix-change={updateValue}
|
|
||||||
></trix-editor>
|
|
||||||
</div>
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {fly} from "svelte/transition";
|
import {fly} from "svelte/transition";
|
||||||
import {createEventDispatcher} from "svelte";
|
import {createEventDispatcher} from "svelte";
|
||||||
import Dropdown from "../common/Dropdown.svelte";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let member;
|
export let member;
|
||||||
@@ -36,41 +35,50 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 200 }}
|
transition:fly={{ duration: 200 }}
|
||||||
class="member-item"
|
class="d-flex justify-content-between align-items-center mb-3 "
|
||||||
>
|
>
|
||||||
<div class="member-name status-{member.roles.includes('removed') ? 'removed' : 'active'}">
|
<div class="d-flex align-items-center status-{member.roles.includes('removed') ? 'removed' : 'active'}">
|
||||||
<Avatar name={member.name ?? "" } side={32}/>
|
<Avatar name={member.name ?? "" } side={32}/>
|
||||||
|
<div class="ms-3 ">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<span class="fs-5">
|
||||||
{member.name}
|
{member.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{member.email}
|
{member.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<Dropdown orientation="right">
|
<div class="dropdown dropdown-center">
|
||||||
<div slot="button">
|
<button
|
||||||
|
class=" dropdown-toggle btn btn-light"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
Roles
|
Roles
|
||||||
</div>
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
<h6 class="dropdown-header">Remove role</h6>
|
<h6 class="dropdown-header">Remove role</h6>
|
||||||
{#each roles as role}
|
{#each roles as role}
|
||||||
{#if member.roles.includes(role)}
|
{#if member.roles.includes(role)}
|
||||||
<button
|
<button
|
||||||
class="dropdown-item button"
|
class="dropdown-item text-capitalize"
|
||||||
on:click={(e) => removeFrom(e,role)}
|
on:click={(e) => removeFrom(e,role)}
|
||||||
>
|
>
|
||||||
{role}
|
{role}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
<div>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</div>
|
||||||
|
|
||||||
<h6 class="dropdown-header">Add role</h6>
|
<h6 class="dropdown-header">Add role</h6>
|
||||||
{#each roles as role}
|
{#each roles as role}
|
||||||
{#if !member.roles.includes(role)}
|
{#if !member.roles.includes(role)}
|
||||||
<button
|
<button
|
||||||
class="dropdown-item button"
|
class="dropdown-item text-capitalize"
|
||||||
on:click={(e) => addTo(e,role)}
|
on:click={(e) => addTo(e,role)}
|
||||||
>
|
>
|
||||||
{role}
|
{role}
|
||||||
@@ -78,8 +86,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</Dropdown>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.status-removed {
|
.status-removed {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
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 Radio from "../forms/Radio.svelte";
|
||||||
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
||||||
import { getContext } from "svelte";
|
import {getContext} from "svelte";
|
||||||
import { apiPost } from "../../helpers";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let users;
|
export let users;
|
||||||
@@ -22,14 +23,15 @@
|
|||||||
function invite(newName, newEmail, newRole) {
|
function invite(newName, newEmail, newRole) {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/members/invite", {
|
axios
|
||||||
|
.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.user];
|
users = [...users, response.data.user];
|
||||||
name = null;
|
name = null;
|
||||||
email = null;
|
email = null;
|
||||||
role = null;
|
role = null;
|
||||||
@@ -43,13 +45,14 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
apiPost(channel.lucentUrl + "/members/update", {
|
axios
|
||||||
|
.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.users;
|
users = response.data.users;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data?.error ?? "";
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
@@ -57,15 +60,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="common-wrapper">
|
<div class="wrapper-tiny transparent mb-5">
|
||||||
<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">Invitee Name</label>
|
<label for="inviteeName" class="form-label"
|
||||||
|
>Invitee Name</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
@@ -90,21 +95,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<select bind:value={role}>
|
|
||||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||||
<option value={arole}>{arole}</option>
|
<Radio
|
||||||
|
bind:group={role}
|
||||||
|
value={arole}
|
||||||
|
name="role"
|
||||||
|
label={arole}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</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>
|
||||||
|
|
||||||
<div class="member-list">
|
<div class="lx-card mt-3">
|
||||||
<h3 class="header-small mb-5 mt-5">Members</h3>
|
<h3 class="header-small mb-5">Members</h3>
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
<MemberSettingsCard
|
<MemberSettingsCard
|
||||||
member={user}
|
member={user}
|
||||||
@@ -113,5 +121,7 @@
|
|||||||
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script>
|
||||||
|
import File from "./includes/File.svelte";
|
||||||
|
import {previewTitle} from "../records/Preview.js";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import Status from "../records/Status.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
|
||||||
|
export let edge = null;
|
||||||
|
let schema = channel.schemas.find(s => s.name === record.schema);
|
||||||
|
let types = ["inline", "card"];
|
||||||
|
export let type = "inline";
|
||||||
|
if (!types.includes(type)) {
|
||||||
|
console.error("unknown preview type")
|
||||||
|
}
|
||||||
|
export let editable = false;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div class="preview-card">
|
||||||
|
{#if edge?.data}
|
||||||
|
<div class="preview-card-edge">Edge Data</div>
|
||||||
|
{/if}
|
||||||
|
<div class="d-flex column-gap-3">
|
||||||
|
{#if record._file}
|
||||||
|
<div>
|
||||||
|
<File {record}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="d-flex flex-md-column " style="line-height: 22px">
|
||||||
|
<a class="text-decoration-none" target="_blank" href="{channel.lucentUrl}/records/{record.id}">{previewTitle(record)}</a>
|
||||||
|
<span class="d-flex gap-1 text-muted">
|
||||||
|
{#if record.status === "draft"}
|
||||||
|
<Status status={record.status}/>
|
||||||
|
{/if}
|
||||||
|
{schema.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.preview-card {
|
||||||
|
position: relative
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card-edge {
|
||||||
|
position: absolute;
|
||||||
|
top: -28px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0px 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--{#if record._file && type === "inline"}-->
|
||||||
|
<!--<!– <FilePreviewInline {record} {edge} {editable}/>–>-->
|
||||||
|
<!--{:else if record._file && type === "card"}-->
|
||||||
|
<!-- <FilePreviewCard {record} {edge} {editable}/>-->
|
||||||
|
<!--{:else if type === "inline"}-->
|
||||||
|
<!--<!– <DocPreviewCard {record} {edge} {editable}/>–>-->
|
||||||
|
<!--{:else if type === "card"}-->
|
||||||
|
<!--<!– <DocPreviewCard {record} {edge} {editable}/>–>-->
|
||||||
|
<!--{/if}-->
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script>
|
||||||
|
import {imgurl} from "../../files/imageserver.js";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
|
||||||
|
|
||||||
|
export let size = "tiny";
|
||||||
|
let imageSide;
|
||||||
|
let fileSide;
|
||||||
|
let fontSize;
|
||||||
|
if (size === "large") {
|
||||||
|
imageSide = 256;
|
||||||
|
fileSide = 32;
|
||||||
|
fontSize = "20";
|
||||||
|
} else if (size === "medium") {
|
||||||
|
imageSide = 128;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "17";
|
||||||
|
} else if (size === "small") {
|
||||||
|
imageSide = 64;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "15";
|
||||||
|
} else if (size === "tiny") {
|
||||||
|
imageSide = 42;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "13";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if record}
|
||||||
|
{#if record._file.mime.startsWith("image")}
|
||||||
|
<!-- href={imgurl(record)} -->
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
title={record._file.path}
|
||||||
|
class="d-flex align-items-center justify-content-center "
|
||||||
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="rounded w-100"
|
||||||
|
src={imgurl(record)}
|
||||||
|
alt={record._file.path}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
title={record._file.path}
|
||||||
|
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
|
>
|
||||||
|
<Icon icon="file" width={fileSide} height={fileSide}/>
|
||||||
|
<span class="ms-2" style="font-size:{fontSize}px"
|
||||||
|
>.{record._file.path.split(".").pop()}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
+4
-22
@@ -1,7 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let isCreateMode;
|
|
||||||
export let active = "";
|
export let active = "";
|
||||||
|
|
||||||
let tabs = schema.groups?.map((group) => {
|
let tabs = schema.groups?.map((group) => {
|
||||||
@@ -11,38 +9,22 @@
|
|||||||
label: "Main",
|
label: "Main",
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
let graphTab = {
|
|
||||||
label: "Backlinks",
|
|
||||||
name: "_graph",
|
|
||||||
};
|
|
||||||
if (isCreateMode) {
|
|
||||||
tabs = [mainTab, ...tabs];
|
|
||||||
} else {
|
|
||||||
tabs = [mainTab, ...tabs, graphTab];
|
|
||||||
}
|
|
||||||
|
|
||||||
function showGraph(e) {
|
tabs = [mainTab, ...tabs];
|
||||||
e.preventDefault();
|
|
||||||
active = "_graph";
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeTab(e, tabName) {
|
function changeTab(e, tabName) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (tabName == "_graph") {
|
|
||||||
showGraph(e);
|
|
||||||
} else {
|
|
||||||
active = tabName;
|
active = tabName;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if tabs.length > 1}
|
{#if tabs.length > 1}
|
||||||
<ul class="tabs">
|
<ul class="nav nav-pills mb-4 justify-content-center">
|
||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
<li class="tab">
|
<li class="nav-item">
|
||||||
<button
|
<button
|
||||||
on:click={(e) => changeTab(e, tab.name)}
|
on:click={(e) => changeTab(e, tab.name)}
|
||||||
class="button"
|
class="nav-link"
|
||||||
class:active={active === tab.name}
|
class:active={active === tab.name}
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
>
|
>
|
||||||
@@ -1,122 +1,60 @@
|
|||||||
<script>
|
<script>
|
||||||
import { afterUpdate, getContext, onMount } from "svelte";
|
import {getContext} from "svelte";
|
||||||
import EditHeader from "./header/EditHeader.svelte";
|
import Manager from "./Manager.svelte";
|
||||||
import ContentTabs from "./header/ContentTabs.svelte";
|
import FilePreview from "./FilePreview.svelte"
|
||||||
import FormField from "./FormField.svelte";
|
import Form from "./form/Form.svelte";
|
||||||
import Graph from "./Graph.svelte";
|
import axios from "axios";
|
||||||
import Info from "./Info.svelte";
|
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
|
||||||
import Title from "./header/Title.svelte";
|
|
||||||
import { apiPost, isEqual } from "../../helpers";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = [];
|
||||||
records: [],
|
export let recordHistory;
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
// export let recordHistory;
|
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
// export let isWritable = false;
|
// export let isWritable = false;
|
||||||
export let users;
|
// export let users;
|
||||||
let originalContent;
|
|
||||||
let activeContentTab = "";
|
|
||||||
$: hasUnsavedData = false;
|
|
||||||
$: validationErrors = null;
|
$: validationErrors = null;
|
||||||
$: errorMessage = validationErrors
|
$: errorMessage = null;
|
||||||
? `Record submission failed. ${
|
|
||||||
Object.entries(validationErrors).length
|
|
||||||
} error(s)`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
let form;
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setOriginalContent();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setOriginalContent() {
|
|
||||||
originalContent = {
|
|
||||||
data: JSON.parse(JSON.stringify(record.data)),
|
|
||||||
status: record.status,
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterUpdate(() => {
|
|
||||||
hasUnsavedData = checkUnsavedData();
|
|
||||||
});
|
|
||||||
|
|
||||||
function beforeUnload(e) {
|
|
||||||
// Cancel the event as stated by the standard.
|
|
||||||
// e.preventDefault();
|
|
||||||
// console.log(hasUnsavedData);
|
|
||||||
if (hasUnsavedData) {
|
|
||||||
return (e.returnValue =
|
|
||||||
"You have unsaved changes. Are you sure you want to exit?");
|
|
||||||
}
|
|
||||||
// Chrome requires returnValue to be set.
|
|
||||||
// e.returnValue = "";
|
|
||||||
delete e["returnValue"];
|
|
||||||
// more compatibility
|
|
||||||
// return true;
|
|
||||||
return "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUnsavedData() {
|
|
||||||
if (isCreateMode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !isEqual(originalContent, {
|
|
||||||
data: record.data,
|
|
||||||
status: record.status,
|
|
||||||
edges: graph.edges,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(e) {
|
function save(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
let status = e.detail.status
|
||||||
console.log("SAVE: Attempt");
|
console.log("SAVE: Attempt");
|
||||||
validationErrors = null;
|
validationErrors = null;
|
||||||
errorMessage = "";
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
if (!hasUnsavedData && !isCreateMode) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!record) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter(
|
let replaceEdges = graph
|
||||||
(edge) => !edge._isTrashed && edge.source === record.id,
|
.map((queryRecord) => queryRecord.edge)
|
||||||
);
|
.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
||||||
apiPost(channel.lucentUrl + "/records", {
|
|
||||||
record: record,
|
axios
|
||||||
edges: graph.edges,
|
.post(channel.lucentUrl + "/records", {
|
||||||
|
schemaName: record.schema,
|
||||||
|
updateEdges: true,
|
||||||
|
id: record.id,
|
||||||
|
data: record.data,
|
||||||
|
edges: replaceEdges,
|
||||||
|
status: status,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
})
|
})
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED");
|
console.log("SAVE: SAVED");
|
||||||
|
|
||||||
if (isCreateMode) {
|
if (isCreateMode) {
|
||||||
window.location =
|
window.location.href = channel.lucentUrl + "/records/" + record.id;
|
||||||
channel.lucentUrl + "/records/" + record.id;
|
|
||||||
} else {
|
} else {
|
||||||
record = response.records[0] ?? null;
|
record = response.data.record ?? null;
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// means trashed
|
// means trashed
|
||||||
hasUnsavedData = false;
|
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
graph = response;
|
graph = [...response.data.graph];
|
||||||
setOriginalContent();
|
form.setOriginalData();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -128,72 +66,36 @@
|
|||||||
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);
|
||||||
// msgSuccess = null;
|
|
||||||
// msgError = error.response.data.error;
|
|
||||||
// submitted = false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={beforeUnload} />
|
|
||||||
|
|
||||||
<div class="record-edit">
|
<div class="wrapper-normal transparent">
|
||||||
<div class="tools-header">
|
<Manager managerRecords={recordHistory} {graph}/>
|
||||||
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
|
||||||
{#if isCreateMode}
|
|
||||||
<button class="button primary btn-spinner" on:click={save}>
|
|
||||||
<span
|
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
{:else if hasUnsavedData}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button primary ms-2 btn btn-primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Title {schema} {record} {isCreateMode} />
|
|
||||||
|
|
||||||
<ErrorAlert message={errorMessage} />
|
<FilePreview {record} {schema}/>
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-4" style="margin-bottom:150px">
|
||||||
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
<Form
|
||||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
bind:this={form}
|
||||||
{#each activeFields as field (field.name)}
|
data={record.data}
|
||||||
{#if activeContentTab === field.group}
|
status={record.status}
|
||||||
<FormField
|
|
||||||
bind:data={record.data}
|
|
||||||
bind:graph
|
bind:graph
|
||||||
{field}
|
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
{validationErrors}
|
|
||||||
{isCreateMode}
|
{isCreateMode}
|
||||||
|
{errorMessage}
|
||||||
|
{validationErrors}
|
||||||
|
on:save={save}
|
||||||
/>
|
/>
|
||||||
{/if}
|
<!-- <Graph {graph} {record}/>-->
|
||||||
{/each}
|
<!-- <Info {record} {graph} {users} {schema}/>-->
|
||||||
{:else if activeContentTab === "_graph"}
|
|
||||||
<Graph {graph} {record} />
|
|
||||||
{:else if activeContentTab === "_info"}
|
|
||||||
<Info {record} {graph} {users} {schema} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import {previewTitle} from "./Preview";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let schema;
|
||||||
|
export let record;
|
||||||
|
export let title;
|
||||||
|
export let isCreateMode;
|
||||||
|
|
||||||
|
function clone(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
|
||||||
|
window.location.href = channel.lucentUrl + "/records/" + response.data.id;
|
||||||
|
}).catch(error => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3 class="header-normal mb-0">
|
||||||
|
<a
|
||||||
|
class="text-muted d-block text-decoration-none fs-6 mb-1"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}"
|
||||||
|
>{schema.label.toUpperCase()}</a
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="text-dark d-block">
|
||||||
|
{#if !isCreateMode}
|
||||||
|
{#if record}
|
||||||
|
{previewTitle(record)}
|
||||||
|
{:else}
|
||||||
|
{ title}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
New Record
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if !isCreateMode && !!record}
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<Icon icon="ellipsis"/>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
|
||||||
|
<h6 class="dropdown-header">Record Actions</h6>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||||
|
>Create new</a
|
||||||
|
>
|
||||||
|
{#if !isCreateMode}
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
on:click={clone}
|
||||||
|
href={channel.lucentUrl}
|
||||||
|
>
|
||||||
|
Clone
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<!-- <a-->
|
||||||
|
<!-- on:click|preventDefault={(e) =>-->
|
||||||
|
<!-- (activeContentTab = "_info")}-->
|
||||||
|
<!-- class="dropdown-item"-->
|
||||||
|
<!-- href="{channel.lucentUrl}">Revisions</a-->
|
||||||
|
<!-- >-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
@@ -1,53 +1,55 @@
|
|||||||
<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";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
export let record;
|
export let record;
|
||||||
export let schema;
|
export let schema;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schema.type === "files"}
|
{#if schema.type === "files"}
|
||||||
<div class="record-edit-file-preview">
|
<div class="row mb-4">
|
||||||
<div>
|
<div class="col" style="max-width:276px">
|
||||||
<Preview {record} size="large" />
|
<Preview {record} size="large"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-details">
|
<div class="col">
|
||||||
<div class="file-details-item">
|
<ul class="list-group ">
|
||||||
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">Filename</span>
|
<span class="text-muted">Filename</span>
|
||||||
<span>{record._file.path}</span>
|
<span>{record._file.path}</span>
|
||||||
</div>
|
</li>
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">Original name</span>
|
<span class="text-muted">Original name</span>
|
||||||
<span>{record._file.originalName}</span>
|
<span>{record._file.originalName}</span>
|
||||||
</div>
|
</li>
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">Mime type</span>
|
<span class="text-muted">Mime type</span>
|
||||||
<span>{record._file.mime}</span>
|
<span>{record._file.mime}</span>
|
||||||
</div>
|
</li>
|
||||||
{#if record._file.width}
|
{#if record._file.width}
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">Dimensions</span>
|
<span class="text-muted">Dimensions</span>
|
||||||
<span>{record._file.width}x{record._file.height}</span>
|
<span>{record._file.width}x{record._file.height}</span>
|
||||||
</div>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">File size</span>
|
<span class="text-muted">File size</span>
|
||||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
||||||
</div>
|
</li>
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<span class="text-muted">Checksum</span>
|
<span class="text-muted">Checksum</span>
|
||||||
<span>{record._file.checksum}</span>
|
<span>{record._file.checksum}</span>
|
||||||
</div>
|
</li>
|
||||||
<div class="file-details-item">
|
<li class="list-group-item border-primary">
|
||||||
<a
|
<span class="text-muted">Download</span>
|
||||||
class="button primary"
|
<a href="{fileurl(record)}">{record._file.path}</a>
|
||||||
target="_blank"
|
</li>
|
||||||
style="display: inline-flex"
|
</ul>
|
||||||
href={fileurl(channel, record)}>Download</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-group {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import Text from "./elements/Text.svelte";
|
import Text from "./form/fields/Text.svelte";
|
||||||
import Slug from "./elements/Slug.svelte";
|
import Slug from "./form/fields/Slug.svelte";
|
||||||
import Reference from "./elements/Reference.svelte";
|
import Reference from "./elements/Reference.svelte";
|
||||||
import Color from "./elements/Color.svelte";
|
import ReferenceInline from "./elements/ReferenceInline.svelte";
|
||||||
import Checkbox from "./elements/Checkbox.svelte";
|
import Block from "./block/Block.svelte";
|
||||||
import Number from "./elements/Number.svelte";
|
import Color from "./form/fields/Color.svelte";
|
||||||
import Url from "./elements/Url.svelte";
|
import Checkbox from "./form/fields/Checkbox.svelte";
|
||||||
import Date from "./elements/Date.svelte";
|
import Number from "./form/fields/Number.svelte";
|
||||||
import UUID from "./elements/UUID.svelte";
|
import Date from "./form/fields/Date.svelte";
|
||||||
import File from "./elements/File.svelte";
|
import UUID from "./form/fields/UUID.svelte";
|
||||||
import Textarea from "./elements/Textarea.svelte";
|
import File from "./form/references/File.svelte";
|
||||||
import Datetime from "./elements/Datetime.svelte";
|
import Textarea from "./form/fields/Textarea.svelte";
|
||||||
import RichEditor from "./elements/RichEditor.svelte";
|
import Datetime from "./form/fields/Datetime.svelte";
|
||||||
import Json from "./elements/JSON.svelte";
|
import RichEditor from "./form/fields/RichEditor.svelte";
|
||||||
import Markdown from "./elements/Markdown.svelte";
|
import Json from "./form/fields/JSON.svelte";
|
||||||
import FieldHeader from "./elements/FieldHeader.svelte";
|
import Markdown from "./form/fields/Markdown.svelte";
|
||||||
import ReferenceTags from "./elements/ReferenceTags.svelte";
|
import FieldHeader from "./form/FieldHeader.svelte";
|
||||||
|
import ReferenceTable from "./elements/ReferenceTable.svelte";
|
||||||
|
import ReferenceTags from "./form/references/ReferenceTags.svelte";
|
||||||
|
|
||||||
const formElements = {
|
const formElements = {
|
||||||
text: Text,
|
text: Text,
|
||||||
@@ -25,7 +27,6 @@
|
|||||||
color: Color,
|
color: Color,
|
||||||
checkbox: Checkbox,
|
checkbox: Checkbox,
|
||||||
number: Number,
|
number: Number,
|
||||||
url: Url,
|
|
||||||
date: Date,
|
date: Date,
|
||||||
datetime: Datetime,
|
datetime: Datetime,
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
@@ -44,15 +45,36 @@
|
|||||||
const id = `field-${field.name}-${record.id}`;
|
const id = `field-${field.name}-${record.id}`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-field">
|
<div class="card editor-field">
|
||||||
<FieldHeader {field} {id} />
|
<FieldHeader {field} {id}/>
|
||||||
{#if field.info.name === "reference" && field.layout === "tags"}
|
{#if field.info.name === "reference" && field.layout === "inline"}
|
||||||
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
<ReferenceInline
|
||||||
{:else if field.info.name === "reference"}
|
bind:graph
|
||||||
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
{record}
|
||||||
{:else if field.info.name === "file"}
|
{field}
|
||||||
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
|
{validationErrors}
|
||||||
<File
|
/>
|
||||||
|
{:else if field.info.name === "reference" && field.layout === "table"}
|
||||||
|
<ReferenceTable
|
||||||
|
bind:graph
|
||||||
|
{id}
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
/>
|
||||||
|
{:else if field.info.name === "reference" && field.layout === "tags"}
|
||||||
|
<ReferenceTags
|
||||||
|
bind:graph
|
||||||
|
{id}
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
/>
|
||||||
|
{:else if ["reference","file"].includes(field.info.name)}
|
||||||
|
<File bind:graph {record} {field} />
|
||||||
|
{:else if field.info.name === "block"}
|
||||||
|
<Block
|
||||||
|
bind:graph
|
||||||
bind:value={data[field.name]}
|
bind:value={data[field.name]}
|
||||||
{record}
|
{record}
|
||||||
{id}
|
{id}
|
||||||
@@ -83,26 +105,6 @@
|
|||||||
{isCreateMode}
|
{isCreateMode}
|
||||||
{id}
|
{id}
|
||||||
/>
|
/>
|
||||||
{:else if field.info.name === "rich"}
|
|
||||||
<RichEditor
|
|
||||||
bind:value={data[field.name]}
|
|
||||||
{schema}
|
|
||||||
{field}
|
|
||||||
{validationErrors}
|
|
||||||
{isCreateMode}
|
|
||||||
bind:graph
|
|
||||||
{record}
|
|
||||||
/>
|
|
||||||
{:else if field.info.name === "markdown"}
|
|
||||||
<Markdown
|
|
||||||
bind:value={data[field.name]}
|
|
||||||
{schema}
|
|
||||||
{field}
|
|
||||||
{validationErrors}
|
|
||||||
{isCreateMode}
|
|
||||||
bind:graph
|
|
||||||
{record}
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={formElement}
|
this={formElement}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import PreviewCardSmall from "./PreviewCardSmall.svelte";
|
||||||
|
import PreviewCard from "./PreviewCard.svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
import {getContext} from "svelte";
|
import {getContext} from "svelte";
|
||||||
import PreviewReference from "./previews/PreviewReference.svelte";
|
import {uniqBy} from "lodash";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let graph;
|
export let graph;
|
||||||
|
export let record;
|
||||||
|
|
||||||
function findEdgeField(schema, edgeField){
|
function findEdgeField(schema, edgeField){
|
||||||
if(edgeField.includes(":")){
|
if(edgeField.includes(":")){
|
||||||
let edgeFieldAr = edgeField.split(":");
|
let edgeFieldAr = edgeField.split(":");
|
||||||
@@ -12,35 +18,121 @@
|
|||||||
return schema.fields.find((f) => f.name === edgeField);
|
return schema.fields.find((f) => f.name === edgeField);
|
||||||
}
|
}
|
||||||
|
|
||||||
let backlinks = graph.parentEdges.map(edge => {
|
let parentEdgesByField = graph.parentEdges
|
||||||
|
.filter((edge) => edge.source !== record.id && edge.depth === 1)
|
||||||
|
.reduce((carry, edge) => {
|
||||||
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||||
let edgeField = findEdgeField(schema,edge.field);
|
let edgeField = findEdgeField(schema,edge.field);
|
||||||
if(!edgeField){
|
let schemaField = edge.sourceSchema + edgeField;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
field: edgeField.label,
|
|
||||||
record: graph.records.find( record => record.id === edge.source)
|
|
||||||
}
|
|
||||||
}).filter( edgeOrNull => !!edgeOrNull)
|
|
||||||
</script>
|
|
||||||
<div class="editor-field">
|
|
||||||
{#each backlinks as backlink}
|
|
||||||
<div style="margin: 0 0 15px;position: relative;">
|
|
||||||
<span style="
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
"
|
let arecord = graph.records.find((n) => {
|
||||||
>In <i>{backlink.field}</i> of</span>
|
return n.id === edge.source;
|
||||||
<PreviewReference
|
});
|
||||||
record={backlink.record}
|
if (!carry[schemaField]) {
|
||||||
hasDelete={false}
|
carry[schemaField] = {
|
||||||
{graph}
|
field: edgeField,
|
||||||
|
schema: schema,
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (arecord) {
|
||||||
|
carry[schemaField].nodes.push(arecord);
|
||||||
|
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
|
||||||
|
}
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
|
let childrenEdgesByField = graph.edges
|
||||||
|
.filter((edge) => edge.source === record.id && edge.depth === 1)
|
||||||
|
.reduce((carry, edge) => {
|
||||||
|
|
||||||
|
let schema = channel.schemas.find((s) => s.name === record.schema);
|
||||||
|
let edgeField = findEdgeField(schema,edge.field);
|
||||||
|
|
||||||
|
// let schemaField = edge.targetSchema + edgeField;
|
||||||
|
let schemaField = edgeField.name + edge.targetSchema;
|
||||||
|
|
||||||
|
if (!carry[schemaField]) {
|
||||||
|
carry[schemaField] = {
|
||||||
|
field: edgeField,
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let arecord = graph.records.find((n) => {
|
||||||
|
return n.id === edge.target;
|
||||||
|
});
|
||||||
|
if (arecord) {
|
||||||
|
carry[schemaField].nodes.push(arecord);
|
||||||
|
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
|
||||||
|
}
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
|
||||||
|
<div class="lx-card mt-3">
|
||||||
|
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
|
||||||
|
<span>{fieldData.schema.label}</span>
|
||||||
|
<Icon icon="angle-right" width="12" height="12"/>
|
||||||
|
<span>{fieldData.field.label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||||
|
{#each fieldData.nodes as node}
|
||||||
|
{#if node._file?.path}
|
||||||
|
<div class="ms-2 mb-2" style="max-height:64px;">
|
||||||
|
<Preview record={node} size="small"/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="ms-2 mb-2">
|
||||||
|
<PreviewCardSmall {graph} record={node}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if Object.entries(parentEdgesByField).length > 0}
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<Icon icon="angles-down" width="32" height="32"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div style="max-width:400px;margin:0 auto;">
|
||||||
|
<PreviewCard {graph} record={record}/>
|
||||||
|
</div>
|
||||||
|
{#if Object.entries(childrenEdgesByField).length > 0}
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<Icon icon="angles-down" width="32" height="32"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
|
||||||
|
<div class="lx-card mt-3">
|
||||||
|
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
|
||||||
|
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||||
|
{#each fieldData.nodes as node}
|
||||||
|
{#if fieldData.field.info.ui === "file"}
|
||||||
|
<div
|
||||||
|
class="ms-2 mb-2"
|
||||||
|
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
|
||||||
|
>
|
||||||
|
<Preview
|
||||||
|
record={node}
|
||||||
|
size="small"
|
||||||
|
showFilename={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
Nothing links to this record
|
<div class="ms-2 mb-2">
|
||||||
|
<PreviewCardSmall {graph} record={node}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { friendlyDate, isEqual } from "../../helpers";
|
import {friendlyDate} 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 {isEqual, sortBy} from "lodash";
|
||||||
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";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -30,27 +30,27 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getEdgesByField(fieldsWithDiff, revision) {
|
function getEdgesByField(fieldsWithDiff, revision) {
|
||||||
edgeFieldsDiff = graph.edges
|
|
||||||
.filter((e) => e.depth === 1)
|
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
|
||||||
.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);
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
fieldsWithDiff = schema.fields.filter((f) => {
|
fieldsWithDiff = schema.fields.filter((f) => {
|
||||||
return !isEqual(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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
rollbackError = "";
|
rollbackError = "";
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
|
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -84,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>
|
||||||
@@ -93,27 +93,29 @@
|
|||||||
</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.version}
|
{record._sys.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.createdBy)}
|
name={usernameById(users, record._sys.createdBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record.createdAt)}
|
{friendlyDate(record._sys.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.updatedBy)}
|
name={usernameById(users, record._sys.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record.updatedAt)}
|
{friendlyDate(record._sys.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="label d-block text-muted">Rules for this schema </span>
|
<span class="label d-block text-muted "
|
||||||
|
>Rules for this schema
|
||||||
|
</span>
|
||||||
<small>
|
<small>
|
||||||
Each record maintains the last {schema.revisions}
|
Each record maintains the last {schema.revisions}
|
||||||
versions
|
versions
|
||||||
@@ -121,33 +123,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="revisions">
|
<div class="lx-card mt-4">
|
||||||
{#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.version !== record.version}
|
{#if revision._sys.version != record._sys.version}
|
||||||
<div
|
<div
|
||||||
class="revision"
|
class="row p-2 rounded"
|
||||||
class:active={revision.version ===
|
class:active={revision._sys.version ===
|
||||||
selectedRevision?.version}
|
selectedRevision?._sys.version}
|
||||||
>
|
>
|
||||||
<div class="version">
|
|
||||||
<span>version {revision.version}</span>
|
<div class="col-2">version {revision._sys.version}</div>
|
||||||
|
<div class="col-5">
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, revision.updatedBy)}
|
name={usernameById(users, revision._sys.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(revision.updatedAt)}
|
{friendlyDate(revision._sys.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
<button
|
<button
|
||||||
disabled={revision.version ===
|
disabled={revision._sys.version ===
|
||||||
selectedRevision?.version}
|
selectedRevision?._sys.version}
|
||||||
class="button"
|
class="btn btn-sm btn-outline-primary"
|
||||||
on:click={(e) => compare(e, revision)}
|
on:click={(e) => compare(e, revision)}
|
||||||
>Compare
|
>Compare
|
||||||
</button>
|
</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -160,18 +164,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div bind:this={revisionSection}>
|
<div bind:this={revisionSection}>
|
||||||
{#if selectedRevision}
|
{#if selectedRevision}
|
||||||
<div class="selected-revision">
|
<div class="mt-4">
|
||||||
{#if fieldsWithDiff.length > 0}
|
{#if fieldsWithDiff.length > 0}
|
||||||
<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 on:click={rollback} class="button">
|
<button
|
||||||
Rollback to version {selectedRevision.version}
|
on:click={rollback}
|
||||||
|
class="btn btn-primary mb-5 d-block mx-auto"
|
||||||
|
>
|
||||||
|
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}
|
||||||
@@ -182,15 +188,21 @@
|
|||||||
<!-- <div class="d-block" style="width:200px;">
|
<!-- <div class="d-block" style="width:200px;">
|
||||||
{field.label}
|
{field.label}
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="revision-field" style="overflow:hidden">
|
<div
|
||||||
<div class="compare-left">
|
class="lx-card row p-4 mb-4 w-100"
|
||||||
|
style="overflow:hidden"
|
||||||
|
>
|
||||||
|
<div class="col-5">
|
||||||
<RevisionCell
|
<RevisionCell
|
||||||
{field}
|
{field}
|
||||||
side={record.data[field.name]}
|
side={record.data[field.name]}
|
||||||
colorClass="text-danger"
|
colorClass="text-danger"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="compare-center">
|
<div class="col-2">
|
||||||
|
<div
|
||||||
|
class="h-100 d-flex align-items-center justify-content-center text-secondary"
|
||||||
|
>
|
||||||
<span class="me-1">{field.label}</span>
|
<span class="me-1">{field.label}</span>
|
||||||
<Icon
|
<Icon
|
||||||
icon="angle-right"
|
icon="angle-right"
|
||||||
@@ -198,7 +210,8 @@
|
|||||||
height="12"
|
height="12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="compare-right">
|
</div>
|
||||||
|
<div class="col-5">
|
||||||
<RevisionCell
|
<RevisionCell
|
||||||
edges={selectedRevision._edges}
|
edges={selectedRevision._edges}
|
||||||
{field}
|
{field}
|
||||||
@@ -217,22 +230,25 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
|
<p class="text-center fw-bold mb-3 mt-5">
|
||||||
|
Record References
|
||||||
|
</p>
|
||||||
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
||||||
<div class="revision-references" style="overflow:hidden">
|
<div
|
||||||
<div class="reference-field">
|
class="lx-card row p-4 mb-4 w-100"
|
||||||
|
style="overflow:hidden"
|
||||||
|
>
|
||||||
|
<div class="col-4">
|
||||||
{field}:
|
{field}:
|
||||||
</div>
|
</div>
|
||||||
<div class="reference-compare">
|
<div class="col-8">
|
||||||
<p class="">Record</p>
|
<p class="mb-2 text-danger">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}
|
||||||
</div>
|
<p class="mt-4 mb-2 text-success">Revision</p>
|
||||||
<div class="reference-compare">
|
|
||||||
<p class="text-success">Revision</p>
|
|
||||||
{#each edges.revision as edge}
|
{#each edges.revision as edge}
|
||||||
<RevisionEdgeRow {edge} />
|
<RevisionEdgeRow {edge} />
|
||||||
{:else}
|
{:else}
|
||||||
@@ -243,5 +259,21 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.label {
|
||||||
|
width: 180px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
|
||||||
afterUpdate,
|
|
||||||
createEventDispatcher,
|
|
||||||
getContext,
|
|
||||||
onMount,
|
|
||||||
} from "svelte";
|
|
||||||
|
|
||||||
|
import {isEqual} from "lodash";
|
||||||
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 "./ContentTabs.svelte";
|
||||||
|
import StatusSelect from "./form/StatusSelect.svelte";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import EditHeader from "./header/EditHeader.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();
|
||||||
@@ -20,7 +14,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;
|
||||||
@@ -33,7 +27,9 @@
|
|||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
let activeFields = schema.fields.filter(
|
||||||
|
(f) => f.name !== "id"
|
||||||
|
);
|
||||||
|
|
||||||
let tabname = "_default";
|
let tabname = "_default";
|
||||||
let fieldToTabs = schema.fields.reduce((c, f) => {
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
@@ -55,6 +51,8 @@
|
|||||||
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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,6 +85,8 @@
|
|||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,12 +112,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges =
|
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
|
||||||
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,
|
||||||
@@ -125,8 +124,8 @@
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED INLINE");
|
console.log("SAVE: SAVED INLINE");
|
||||||
|
|
||||||
record = response.records[0];
|
record = response.data.records[0];
|
||||||
graph = response;
|
graph = response.data;
|
||||||
if (!isCreateMode) {
|
if (!isCreateMode) {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
@@ -136,6 +135,7 @@
|
|||||||
resolve(null);
|
resolve(null);
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
// setOriginalContent();
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (typeof error.response.data.error === "string") {
|
if (typeof error.response.data.error === "string") {
|
||||||
errorMessage = error.response.data.error;
|
errorMessage = error.response.data.error;
|
||||||
@@ -144,52 +144,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
// msgSuccess = null;
|
||||||
|
// msgError = error.response.data.error;
|
||||||
|
// submitted = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={beforeUnload} />
|
<svelte:window on:beforeunload={beforeUnload}/>
|
||||||
|
|
||||||
<div class="inline-edit record-edit">
|
<div class="inline-edit my-4">
|
||||||
<div class="tools-header">
|
<ErrorAlert message={errorMessage}/>
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
|
||||||
{#if isCreateMode}
|
|
||||||
<button class="button primary btn-spinner" on:click={save}>
|
|
||||||
<span
|
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
{:else if hasUnsavedData}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button primary ms-2 btn btn-primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Title {schema} {record} {isCreateMode} />
|
|
||||||
<ErrorAlert message={errorMessage} />
|
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-1">
|
||||||
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
<ContentTabs
|
||||||
<FilePreview {record} {schema} />
|
{schema}
|
||||||
|
{isCreateMode}
|
||||||
|
bind:active={activeContentTab}
|
||||||
|
{record}
|
||||||
|
/>
|
||||||
|
<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
|
bind:graph={graph}
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
@@ -200,4 +181,48 @@
|
|||||||
{/each}
|
{/each}
|
||||||
<!-- </fieldset> -->
|
<!-- </fieldset> -->
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex mt-3 align-items-center justify-content-center">
|
||||||
|
{#if schema.hasDrafts}
|
||||||
|
<StatusSelect bind:status={record.status} {schema}/>
|
||||||
|
{/if}
|
||||||
|
{#if isCreateMode}
|
||||||
|
<button
|
||||||
|
class="ms-2 btn btn-primary btn-spinner"
|
||||||
|
on:click={save}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
disabled={!hasUnsavedData}
|
||||||
|
class="ms-2 btn btn-primary btn-spinner"
|
||||||
|
on:click={save}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button class="ms-2 btn btn-link" on:click={cancel}>
|
||||||
|
cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inline-edit {
|
||||||
|
padding: 44px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import PreviewCardSmall from "./PreviewCardSmall.svelte";
|
||||||
|
|
||||||
|
export let managerRecords;
|
||||||
|
|
||||||
|
export let graph;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if managerRecords.length > 0}
|
||||||
|
<div
|
||||||
|
class="record-history d-flex justify-content-center align-items-center w-100 mb-4 mt-4"
|
||||||
|
>
|
||||||
|
{#each managerRecords.reverse() as arecord, i}
|
||||||
|
{#if i !== 0}
|
||||||
|
<Icon icon="angle-right"/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mx-3 p-0 my-0">
|
||||||
|
<PreviewCardSmall record={arecord} {graph}/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.record-history {
|
||||||
|
/* background-color: #fff; */
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 32px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Mustache from "mustache";
|
||||||
|
import {stripHtml} from "../../helpers";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
export function previewTitle(record) {
|
||||||
|
const channel = getContext("channel");
|
||||||
|
let schema = channel.schemas.find((aSchema) => aSchema.name === record?.schema);
|
||||||
|
|
||||||
|
if (!schema?.titleTemplate) {
|
||||||
|
return noTemplate(schema, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = Mustache.parse(schema.titleTemplate);
|
||||||
|
let render = Mustache.render(schema.titleTemplate, record.data);
|
||||||
|
|
||||||
|
if (!render || render === "") {
|
||||||
|
return noTemplate(schema, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripHtml(render.slice(0, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewEdgeTitle(edge) {
|
||||||
|
const channel = getContext("channel");
|
||||||
|
let edgeSchemaName = channel.schemas
|
||||||
|
.find((aSchema) => aSchema.name === edge?.sourceSchema)
|
||||||
|
.fields.find(f => f.name === edge.field).data;
|
||||||
|
let schema = channel.schemas.find((aSchema) => aSchema.name === edgeSchemaName);
|
||||||
|
if (!schema?.titleTemplate) {
|
||||||
|
return noTemplate(schema, edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = Mustache.parse(schema.titleTemplate);
|
||||||
|
let render = Mustache.render(schema.titleTemplate, edge.data);
|
||||||
|
|
||||||
|
if (!render || render === "") {
|
||||||
|
return noTemplate(schema, edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === "") {
|
||||||
|
return "Untitled";
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
import {getContext, createEventDispatcher} from "svelte";
|
||||||
|
import {previewEdgeTitle, previewTitle} from "./Preview";
|
||||||
|
import Status from "./Status.svelte";
|
||||||
|
import Preview from "../newPreview/Preview.svelte";
|
||||||
|
import EdgeData from "./form/references/EdgeData.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let edge = null;
|
||||||
|
export let editable = false;
|
||||||
|
export let classes = "";
|
||||||
|
export let hasDelete = false;
|
||||||
|
|
||||||
|
let edgeData;
|
||||||
|
|
||||||
|
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||||
|
let cardTitle = previewTitle(record);
|
||||||
|
|
||||||
|
function remove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("remove", record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
edgeData.openEdit();
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{#if editable}
|
||||||
|
<div
|
||||||
|
class="card mb-2 bg-light w-50 "
|
||||||
|
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="text-muted d-block">Relation Data</span>
|
||||||
|
{previewEdgeTitle(edge)}
|
||||||
|
<div class="position-absolute d-flex end-0" style="top:5px">
|
||||||
|
<button
|
||||||
|
class="trash-button text-dark btn btn-sm btn-link"
|
||||||
|
on:click={edit}
|
||||||
|
>
|
||||||
|
<Icon icon="pencil"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EdgeData bind:this={edgeData} {record} {field} bind:edge/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card mb-2 bg-light w-100 {classes}"
|
||||||
|
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<Preview {record} type="card"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if hasDelete}
|
||||||
|
<div class="position-absolute d-flex end-0" style="top:5px">
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="trash-button text-dark btn btn-sm btn-link"
|
||||||
|
on:click={remove}
|
||||||
|
>
|
||||||
|
<Icon icon="trash-can"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.card .trash-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .trash-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
import {createEventDispatcher, onMount, getContext} from "svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import InlineEdit from "./InlineEdit.svelte";
|
||||||
|
import Reference from "../content/elements/Reference.svelte";
|
||||||
|
import File from "../content/elements/File.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let isFirst;
|
||||||
|
export let isLast;
|
||||||
|
export let toDelete = false;
|
||||||
|
export let record;
|
||||||
|
let editRecord;
|
||||||
|
let editGraph;
|
||||||
|
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||||
|
$: editMode = false;
|
||||||
|
$: expanded = false;
|
||||||
|
|
||||||
|
function editInline(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
axios
|
||||||
|
.get(channel.lucentUrl + "/records/editInline/" + record.id)
|
||||||
|
.then((response) => {
|
||||||
|
record = response.data;
|
||||||
|
editRecord = response.data.record;
|
||||||
|
editGraph = response.data.graph;
|
||||||
|
editMode = true;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveup(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("moveup");
|
||||||
|
}
|
||||||
|
|
||||||
|
function movedn(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("movedn");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInlinesaved(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("inlinesaved", e.detail);
|
||||||
|
editMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("remove", record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trash(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("trash", record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("undoremove", record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
editMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editMode = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteFromChannel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl +"/records/status/trashed", [record])
|
||||||
|
.then((response) => {
|
||||||
|
dispatch("remove", record.id);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if toDelete}
|
||||||
|
<div class="lx-card bg-danger bg-opacity-10 text-center">
|
||||||
|
<p>Item was removed from the current record.</p>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline border border-1 border-dark"
|
||||||
|
on:click={undo}>Undo
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-danger "
|
||||||
|
on:click={deleteFromChannel}
|
||||||
|
>Delete completely from channel
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-sm btn-link" on:click={remove}
|
||||||
|
>Dismiss Message
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else if editMode === true}
|
||||||
|
<InlineEdit
|
||||||
|
{schema}
|
||||||
|
record={editRecord}
|
||||||
|
graph={editGraph}
|
||||||
|
isCreateMode={false}
|
||||||
|
on:cancel={cancel}
|
||||||
|
on:inlinesaved={handleInlinesaved}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="lx-card mt-4 bg-primary bg-opacity-10">
|
||||||
|
<div class="actions">
|
||||||
|
<small class="text-muted">{schema.label}</small>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-link"
|
||||||
|
on:click|preventDefault={editInline}
|
||||||
|
>
|
||||||
|
<Icon icon="pencil" width={12} height={12}/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-link"
|
||||||
|
on:click={(e) => (expanded = !expanded)}
|
||||||
|
>
|
||||||
|
{#if expanded}
|
||||||
|
<Icon icon="compress" width={12} height={12}/>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="expand" width={12} height={12}/>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<Icon icon="ellipsis"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="/records/{record.id}"
|
||||||
|
target="_blank"
|
||||||
|
>Edit in new tab
|
||||||
|
</a>
|
||||||
|
<button class="dropdown-item" on:click={trash}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<!-- <a class="dropdown-item" href="#">Clone</a> -->
|
||||||
|
|
||||||
|
{#if !isFirst}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary border-0"
|
||||||
|
on:click|preventDefault={moveup}
|
||||||
|
>
|
||||||
|
<Icon icon="circle-chevron-up"/>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if !isLast}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary border-0"
|
||||||
|
on:click|preventDefault={movedn}
|
||||||
|
>
|
||||||
|
<Icon icon="circle-chevron-down"/>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-preview" class:expanded>
|
||||||
|
{#if schema.type === "files"}
|
||||||
|
<Preview {record} size="small"/>
|
||||||
|
{/if}
|
||||||
|
{#each schema.fields.filter((f) => !(f.trashed || ["tab"].includes(f.ui) || ["id"].includes(f.name))) as field}
|
||||||
|
<span class="text-muted d-block mt-2" style="font-size:13px"
|
||||||
|
>{field.label}</span
|
||||||
|
>
|
||||||
|
{#if field.ui === "reference"}
|
||||||
|
<Reference {record} {field}/>
|
||||||
|
{:else if field.ui === "file"}
|
||||||
|
<File {record} {field}/>
|
||||||
|
{:else}
|
||||||
|
{@html record.data[field.name]}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lx-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lx-card .inline-preview {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lx-card .inline-preview.expanded {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lx-card .actions {
|
||||||
|
top: 10px;
|
||||||
|
right: 44px;
|
||||||
|
position: absolute;
|
||||||
|
/* visibility: hidden; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .lx-card:hover .actions {
|
||||||
|
visibility: visible;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import {previewTitle} from "./Preview";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
$: title = record.data.name;
|
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||||
|
|
||||||
|
$: title = previewTitle( record);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if record?.data}
|
{#if record?.data}
|
||||||
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
class="text-decoration-none rounded py-1 px-2 d-inline-block"
|
||||||
|
{title}
|
||||||
|
style="border:2px solid {!schema.color
|
||||||
|
? '#999'
|
||||||
|
: schema.color}!important;white-space: nowrap;"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import BlockButtons from "./BlockButtons.svelte";
|
||||||
|
import BlockElements from "./BlockElements.svelte";
|
||||||
|
import {flip} from "svelte/animate";
|
||||||
|
import {quintOut} from 'svelte/easing';
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let value = [];
|
||||||
|
export let graph;
|
||||||
|
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
|
<div class="inline-card-wrapper">
|
||||||
|
<BlockButtons
|
||||||
|
bind:blockData={value}
|
||||||
|
{blockSchema}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#each value as blockItemData (blockItemData.id)}
|
||||||
|
<div class="block-field-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
|
||||||
|
<BlockElements
|
||||||
|
bind:block={blockItemData}
|
||||||
|
bind:blockData={value}
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
bind:graph
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {insertBlock} from "./block";
|
||||||
|
|
||||||
|
export let blockId = "";
|
||||||
|
export let blockData;
|
||||||
|
export let blockSchema;
|
||||||
|
$: showOptions = false;
|
||||||
|
|
||||||
|
function createBlock(e, ui) {
|
||||||
|
e.preventDefault();
|
||||||
|
blockData = insertBlock(blockData,ui);
|
||||||
|
showOptions = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div class="d-flex justify-content-left mb-2 ">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class:is-first={!blockId}
|
||||||
|
class=" btn btn-lg btn-link text-decoration-none block-buttons"
|
||||||
|
on:click|preventDefault={(e) => (showOptions = !showOptions)}
|
||||||
|
>
|
||||||
|
<Icon width={24} height={24} icon="circle-plus"/>
|
||||||
|
</button>
|
||||||
|
{#if showOptions}
|
||||||
|
<div class="d-flex ">
|
||||||
|
{#each blockSchema.fields as validUi}
|
||||||
|
<div class="ms-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={(e) => createBlock(e, validUi)}
|
||||||
|
>{validUi.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
:global(.block-field-wrapper) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.block-field-wrapper .block-buttons) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:global(.block-field-wrapper:hover .block-buttons) {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-buttons {
|
||||||
|
padding: 0px;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0px ;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<script>
|
||||||
|
import Heading from "./elements/Heading.svelte";
|
||||||
|
import Textarea from "./elements/Textarea.svelte";
|
||||||
|
import Rich from "./elements/Rich.svelte";
|
||||||
|
import Markdown from "./elements/Markdown.svelte";
|
||||||
|
import Reference from "./elements/Reference.svelte";
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {insertBlock} from "./block";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import {findIndex} from "lodash";
|
||||||
|
import File from "./elements/File.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
export let blockData;
|
||||||
|
export let field;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
|
||||||
|
|
||||||
|
function createBlock(e, ui, blockId) {
|
||||||
|
e.preventDefault();
|
||||||
|
blockData = insertBlock(blockData, ui, blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteBlock(e, blockId) {
|
||||||
|
e.preventDefault();
|
||||||
|
blockData = blockData.filter(b => b.id !== blockId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function upBlock(e, blockId) {
|
||||||
|
e.preventDefault();
|
||||||
|
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
|
||||||
|
let tempBlock = blockData[blockIndex];
|
||||||
|
blockData[blockIndex] = blockData[blockIndex - 1];
|
||||||
|
blockData[blockIndex - 1] = tempBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downBlock(e, blockId) {
|
||||||
|
e.preventDefault();
|
||||||
|
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
|
||||||
|
let tempBlock = blockData[blockIndex];
|
||||||
|
blockData[blockIndex] = blockData[blockIndex + 1];
|
||||||
|
blockData[blockIndex + 1] = tempBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockIsFirst(blockId) {
|
||||||
|
return findIndex(blockData, (b) => b.id === blockId) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockIsLast(blockId) {
|
||||||
|
return findIndex(blockData, (b) => b.id === blockId) === blockData.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card block-editor-field d-flex">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="text-muted d-block fs-6 mb-1">{block.meta.label}</span>
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<Icon icon="ellipsis"/>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
|
||||||
|
<h6 class="dropdown-header">
|
||||||
|
Block id: <input class="form-control-plaintext" readonly value={block.id}/>
|
||||||
|
Block name: <input class="form-control-plaintext" readonly value={block.meta.name}/>
|
||||||
|
</h6>
|
||||||
|
<div>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</div>
|
||||||
|
<h6 class="dropdown-header">Actions</h6>
|
||||||
|
<button
|
||||||
|
|
||||||
|
class="dropdown-item"
|
||||||
|
class:d-none={blockIsFirst(block.id)}
|
||||||
|
on:click={(e) => upBlock(e, block.id)}
|
||||||
|
>Move up
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="dropdown-item"
|
||||||
|
class:d-none={blockIsLast(block.id)}
|
||||||
|
on:click={(e) => downBlock(e, block.id)}
|
||||||
|
>Move down
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="dropdown-item text-danger"
|
||||||
|
on:click={(e) => deleteBlock(e, block.id)}
|
||||||
|
>Delete
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
<h6 class="dropdown-header">Insert after</h6>
|
||||||
|
|
||||||
|
{#each blockSchema.fields as blockField}
|
||||||
|
<button
|
||||||
|
class="dropdown-item"
|
||||||
|
on:click={(e) => createBlock(e, blockField, block.id)}
|
||||||
|
>{blockField.label}
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if block.meta.info.name === "heading"}
|
||||||
|
|
||||||
|
<Heading
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{:else if block.meta.info.name === "textarea"}
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{:else if block.meta.info.name === "rich"}
|
||||||
|
<Rich
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{:else if block.meta.info.name === "markdown"}
|
||||||
|
<Markdown
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{:else if block.meta.info.name === "file"}
|
||||||
|
<File
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
bind:graph
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{:else if block.meta.info.name === "reference"}
|
||||||
|
<Reference
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
bind:graph
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.block-editor-field{
|
||||||
|
|
||||||
|
margin: 10px 0;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {randomId} from "../../../helpers.js";
|
||||||
|
|
||||||
|
export function insertBlock(blockData, blockField, afterBlockId = null) {
|
||||||
|
|
||||||
|
if (!afterBlockId) {
|
||||||
|
return [{
|
||||||
|
meta: blockField,
|
||||||
|
id: randomId(),
|
||||||
|
value: null
|
||||||
|
}, ...blockData];
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockData.reduce((carry, block) => {
|
||||||
|
carry.push(block)
|
||||||
|
if (block.id === afterBlockId) {
|
||||||
|
carry.push({
|
||||||
|
meta: blockField,
|
||||||
|
id: randomId(),
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return carry;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import PreviewCard from "../../PreviewCard.svelte";
|
||||||
|
import {sortByField} from "../../../edges/sortEdges";
|
||||||
|
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
|
||||||
|
import Sortable from "../../../libs/Sortable.svelte";
|
||||||
|
import {insertEdges} from "../../form/references/reference.js";
|
||||||
|
import BrowseModal from "../../form/references/BrowseModal.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let block;
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let graph;
|
||||||
|
let browseModal;
|
||||||
|
let blockFieldName = field.name + ":" + block.id;
|
||||||
|
|
||||||
|
$: references = graph.edges
|
||||||
|
.filter((edge) => edge.field === blockFieldName)
|
||||||
|
.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) =>
|
||||||
|
block.meta.collections.includes(aschema.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeReference(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
graph.edges = graph.edges.filter(
|
||||||
|
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
|
||||||
|
);
|
||||||
|
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowseModal(e, schema) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.open(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorder(e) {
|
||||||
|
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.close();
|
||||||
|
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
{#if block.meta.collections.length === 1}
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
on:click={(e) => openBrowseModal(e, collections[0].name)}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{#each collections as collection}
|
||||||
|
<li>
|
||||||
|
<!-- {`${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
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if references.length > 0}
|
||||||
|
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
||||||
|
{#each references as reference (reference.id)}
|
||||||
|
<div class="col mb-3">
|
||||||
|
<PreviewCard
|
||||||
|
classes="h-100"
|
||||||
|
record={reference}
|
||||||
|
hasDelete={true}
|
||||||
|
on:remove={removeReference}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Sortable>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<BrowseModal bind:this={browseModal} on:insert={insert}/>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let block;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={block.id}
|
||||||
|
class="form-control"
|
||||||
|
bind:value={block.value}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
// export let id;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
|
||||||
|
<Codemirror bind:value={block.value} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import PreviewCard from "../../PreviewCard.svelte";
|
||||||
|
import {sortByField} from "../../../edges/sortEdges";
|
||||||
|
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
|
||||||
|
import Sortable from "../../../libs/Sortable.svelte";
|
||||||
|
import {insertEdges} from "../../form/references/reference.js";
|
||||||
|
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let block;
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
let blockFieldName = field.name + ":" + block.id;
|
||||||
|
|
||||||
|
$: references = graph.edges
|
||||||
|
.filter((edge) => edge.field === blockFieldName)
|
||||||
|
.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) =>
|
||||||
|
block.meta.collections.includes(aschema.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeReference(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
graph.edges = graph.edges.filter(
|
||||||
|
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
|
||||||
|
);
|
||||||
|
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorder(e) {
|
||||||
|
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inline-card-wrapper">
|
||||||
|
<ReferenceInlineButtons
|
||||||
|
buttonClass="mt-2"
|
||||||
|
recordId={null}
|
||||||
|
schemas={collections}
|
||||||
|
on:insert={insert}
|
||||||
|
on:save={insert}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if references.length > 0}
|
||||||
|
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
||||||
|
{#each references as reference (reference.id)}
|
||||||
|
<div class="col mb-3">
|
||||||
|
<PreviewCard
|
||||||
|
classes="h-100"
|
||||||
|
record={reference}
|
||||||
|
hasDelete={true}
|
||||||
|
on:remove={removeReference}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Sortable>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
import Tinymce from "../../../libs/Tinymce.svelte";
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
let additionalConfig = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<Tinymce bind:value={block.value} {additionalConfig}/>
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user