Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0725366dd5 | |||
| a2bcd10607 | |||
| 37ed966ac3 | |||
| 085c307137 | |||
| d961d910d8 | |||
| 1dc6d541cc | |||
| 7fef89b778 | |||
| 6b713e4ffb | |||
| b52a91bf52 | |||
| e8d8340448 | |||
| 81371c41a7 | |||
| 3cf5f0173b | |||
| 65844e030d | |||
| ba7e4ab151 | |||
| ec4e578aee | |||
| 1a5f300a78 | |||
| a04cdd753d | |||
| daa4b268a6 | |||
| a5161cb6b4 | |||
| 48e32bfdcb | |||
| 639ee895cd | |||
| 8cf1dd9bfd | |||
| fcadc8d7a1 | |||
| 1e2f941f47 | |||
| eeee2afc05 | |||
| 98efb76f7b | |||
| dff3748623 | |||
| 93a16ee916 | |||
| 8b3a3964a5 | |||
| 43dd36e20e | |||
| 5587e8b4b6 | |||
| 16e50e2d49 | |||
| bd01e5c32c | |||
| e058ceadee | |||
| 4a7eb217a1 | |||
| 57b0727788 | |||
| 58b047edd2 | |||
| a78b699a5e | |||
| e910ae9878 | |||
| 362c649d36 | |||
| 852c4d608d | |||
| aa59e55a41 | |||
| 348bad80e0 | |||
| f0d4686141 | |||
| a482ab3c7e | |||
| c580882ec0 | |||
| 2cf8379cbe | |||
| c39ec469df | |||
| 232fcc8845 | |||
| 9d5d4dd930 | |||
| c507dc6031 | |||
| 843f560710 | |||
| 7574d67d80 | |||
| 19931cb4d1 | |||
| 6458c1e71d | |||
| 63232585ab | |||
| 6d15591601 | |||
| 32c8378020 | |||
| d0cd8228cc | |||
| c45a3847f8 | |||
| c0b3878674 | |||
| f868219981 | |||
| 8ac0567e66 | |||
| 02f8f5970a | |||
| 0cd4e08716 | |||
| cf3d621587 | |||
| 6fc0a65b6f | |||
| a73ee21568 | |||
| ff54bcc2ef | |||
| ab1517cc8f | |||
| 9f724a3243 | |||
| ae65ca47f6 | |||
| 74d2fcc4fa | |||
| 82174afdea | |||
| ffc39f078d | |||
| 7c4e19afbc | |||
| 7b10bfca1d | |||
| 0e5ac08641 | |||
| 1505aaa909 | |||
| d9e2c4954a | |||
| 97ad9de3d2 | |||
| 9e140be0ec | |||
| a737c2d571 | |||
| c43c29eb14 | |||
| 0c00f76657 | |||
| 4165bfb95d | |||
| 570dbf747e | |||
| 14cbd0a845 | |||
| c99634bb46 | |||
| 246696f331 | |||
| 0643578d15 | |||
| 3aa9191cba | |||
| c97be8666e | |||
| 509d7c13f2 | |||
| 50c8af7bda | |||
| 5d6869c118 | |||
| ec15f21e67 | |||
| 36165444cf | |||
| 322962403d | |||
| db37653748 | |||
| 5a13ddb2ec | |||
| 9bbd53b586 | |||
| a04e338ce2 | |||
| 2429d4acb5 | |||
| 113533408d | |||
| f9806f60c9 | |||
| 1f3ebafe69 | |||
| 1ab3f678b7 | |||
| a54200c5e5 | |||
| 069ae72705 |
@@ -6,3 +6,4 @@ front/node_modules
|
|||||||
front/npm-debug.log
|
front/npm-debug.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/.claude
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "/phpactor.schema.json",
|
||||||
|
"language_server_phpstan.enabled": false
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ include_toc: true
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- PHP 8.2
|
- PHP 8.3
|
||||||
- Laravel 10
|
- Laravel 11
|
||||||
- Postgres or Sqlite database
|
- Postgres or Sqlite database
|
||||||
- ImageMagick
|
- ImageMagick
|
||||||
|
|
||||||
@@ -82,7 +82,9 @@ 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:
|
||||||
|
|
||||||
@@ -90,6 +92,26 @@ 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
@@ -0,0 +1,32 @@
|
|||||||
|
# Upgrade from 1.1.* to 1.2.0
|
||||||
|
|
||||||
|
## lucent.php config file
|
||||||
|
|
||||||
|
There is now an array of commands, accepting more than one.
|
||||||
|
|
||||||
|
from
|
||||||
|
```php
|
||||||
|
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
|
||||||
|
```
|
||||||
|
|
||||||
|
to
|
||||||
|
```php
|
||||||
|
"commands" => [
|
||||||
|
"generate:static" => "Build Website",
|
||||||
|
],
|
||||||
|
```
|
||||||
|
## config/filesystems.php
|
||||||
|
|
||||||
|
Lucent has its own filesystem.
|
||||||
|
|
||||||
|
You should now add:
|
||||||
|
|
||||||
|
```
|
||||||
|
'lucent' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
```
|
||||||
+36
-42
@@ -1,45 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "lexx27/lucent",
|
"name": "lexx27/lucent",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"description": "Lucent cms",
|
"description": "Lucent cms",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"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",
|
"require-dev": {
|
||||||
"symfony/yaml": "^7.0",
|
"phpstan/phpstan": "^1.8",
|
||||||
"spatie/laravel-data": "^4.4"
|
"laravel/framework": "^10.10"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Lucent\\": "src/"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"files": [
|
||||||
"phpstan/phpstan": "^1.8",
|
"src/Response.php",
|
||||||
"laravel/framework": "^10.10"
|
"src/macros.php"
|
||||||
},
|
]
|
||||||
"autoload": {
|
},
|
||||||
"psr-4": {
|
"extra": {
|
||||||
"Lucent\\": "src/"
|
"laravel": {
|
||||||
},
|
"providers": [
|
||||||
"files": [
|
"Lucent\\LucentServiceProvider"
|
||||||
"src/Response.php",
|
]
|
||||||
"src/macros.php",
|
}
|
||||||
"src/File/Uploader.php"
|
},
|
||||||
]
|
"minimum-stability": "stable",
|
||||||
},
|
"prefer-stable": true
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"providers": [
|
|
||||||
"Lucent\\LucentServiceProvider"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimum-stability": "stable",
|
|
||||||
"prefer-stable": true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+677
-2884
File diff suppressed because it is too large
Load Diff
+145
-298
@@ -5,374 +5,221 @@ include_toc: true
|
|||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
|
|
||||||
Fields are similar to a table's columns in a relational databases.
|
Fields define the columns of a schema. Each field has a `ui` type that controls both storage and the admin UI component rendered.
|
||||||
|
|
||||||
## Available fields for Collections and Files
|
## Common Optional Properties
|
||||||
|
|
||||||
|
Most fields share these optional properties:
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `required` | Whether the field must have a value to save as `published` |
|
||||||
|
| `nullable` | Allow saving as `null` |
|
||||||
|
| `help` | Help text shown below the input |
|
||||||
|
| `default` | Default value when creating a new record |
|
||||||
|
| `readonly` | Prevent editing from the UI |
|
||||||
|
| `group` | Tab group this field belongs to |
|
||||||
|
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
### text
|
### text
|
||||||
One-line text input
|
|
||||||
|
|
||||||
required
|
One-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
| `selectOptions` | Array of options. Strings or `[{value, label}]` objects |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### textarea
|
### textarea
|
||||||
textarea input
|
|
||||||
|
|
||||||
required
|
Multi-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### slug
|
### slug
|
||||||
Slug input. Generates automatically if left empty
|
|
||||||
|
|
||||||
required
|
Slug input. Auto-generates from a source field if left empty.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `source`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **source**: The source field from which it generates
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `source` | Field name to generate the slug from |
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### rich
|
### rich
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
WYSIWYG rich text editor.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
### markdown
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
Markdown editor.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### number
|
### number
|
||||||
Any numeric value
|
|
||||||
|
|
||||||
required
|
Numeric input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `decimals` | Number of decimal places. Default: `0` |
|
||||||
|
| `min` | Minimum value |
|
||||||
|
| `max` | Maximum value |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **decimals**: default is 0
|
---
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### checkbox
|
### checkbox
|
||||||
True or false
|
|
||||||
|
|
||||||
required
|
Boolean true/false toggle.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### color
|
### color
|
||||||
Color picker
|
|
||||||
|
|
||||||
required
|
Color picker.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `selectOptions` | Restrict to a predefined palette |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### date
|
### date
|
||||||
Date select
|
|
||||||
|
|
||||||
required
|
Date selector. Stores as a date string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum date |
|
||||||
|
| `max` | Maximum date |
|
||||||
|
| `selectOptions` | Predefined date options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### datetime
|
### datetime
|
||||||
Date and time selector
|
|
||||||
|
|
||||||
required
|
Date and time selector. Stores as an ISO 8601 string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum datetime |
|
||||||
|
| `max` | Maximum datetime |
|
||||||
|
| `selectOptions` | Predefined datetime options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
### uuid
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
---
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### json
|
### json
|
||||||
Json data
|
|
||||||
|
|
||||||
required
|
Raw JSON data field. Accepts either a JSON string or a plain array — both are stored as JSON.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### file
|
### file
|
||||||
Upload or select files
|
|
||||||
|
|
||||||
required
|
Upload or select files from a files schema. Files can be uploaded directly or browsed from previously uploaded files. Image files (jpeg, png, webp, gif, tiff) automatically get a 300×300 thumbnail generated on upload.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
- **mime**: The mime types allowed to select
|
| `collections` | Array of file schema names to choose from |
|
||||||
- **nullable**: Can the field be saved as null
|
| `mime` | Allowed MIME types e.g. `["image/*"]` |
|
||||||
- **min**: Minimum files
|
| `min` | Minimum number of files |
|
||||||
- **max**: Maximum files
|
| `max` | Maximum number of files |
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### reference
|
### reference
|
||||||
Reference other records
|
|
||||||
|
|
||||||
required
|
Reference records from another collection.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `collections` | Array of collection schema names to reference |
|
||||||
|
| `min` | Minimum number of references |
|
||||||
|
| `max` | Maximum number of references |
|
||||||
|
|
||||||
- **nullable**: Can the field be saved as null
|
---
|
||||||
- **min**: Minimum files
|
|
||||||
- **max**: Maximum files
|
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
## Example Field
|
||||||
|
|
||||||
### block
|
```json
|
||||||
The block editor
|
{
|
||||||
|
"ui": "text",
|
||||||
required
|
"name": "title",
|
||||||
|
"label": "Title",
|
||||||
- **name**: The id of the field
|
"required": true,
|
||||||
- **label**: The friendly name of the field
|
"min": 3,
|
||||||
- **schema**: The block schema name
|
"max": 200,
|
||||||
|
"help": "The main title of the post"
|
||||||
optional
|
}
|
||||||
|
```
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
)
|
|
||||||
## Available fields for the Block Editor
|
|
||||||
|
|
||||||
### heading
|
|
||||||
Single-line text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### textarea
|
|
||||||
Multiline text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### rich
|
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### file
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **mime**: The mime types allowed to select
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### reference
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|||||||
+133
-52
@@ -5,88 +5,169 @@ include_toc: true
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
|
|
||||||
## Graph or Tree
|
The `Query` class is the main way to fetch records. Inject it via the Laravel container.
|
||||||
|
|
||||||
Queries can return results in 2 formats. A graph or a tree.
|
|
||||||
|
|
||||||
Graphs results are a collection of records (nodes) and a collection of edges. This format is more useful for network visualization.
|
|
||||||
|
|
||||||
The tree format is more straightforward as it returns a collection of records. Each record has a **_children** and a **_parents** field and the tree can continue upwards or downwards according to the depth you have requested.
|
|
||||||
|
|
||||||
For example to request records with their children and their children's children:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->childrenDepth(2);
|
public function __construct(private Query $query) {}
|
||||||
```
|
```
|
||||||
Maybe you only want to get a specific type of relationship:
|
|
||||||
|
|
||||||
```php
|
## Return Formats
|
||||||
$query->childrenDepth(2)->childrenFields(["categories"]);
|
|
||||||
```
|
Queries return results in two formats:
|
||||||
|
|
||||||
|
**Graph** — via `->run()`. Returns a `Graph` object with `records` and `edges` collections. Useful for network-style data.
|
||||||
|
|
||||||
|
**Tree** — via `->tree()`. Returns a flat `Collection` of records where each record has `_children` and `_parents` arrays populated based on the requested depth. This is the most common format.
|
||||||
|
|
||||||
|
## Chaining Methods
|
||||||
|
|
||||||
|
All methods return `$this` and can be chained:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `->filter(array)` | Add an AND filter |
|
||||||
|
| `->orFilter(array)` | Add an OR filter (grouped) |
|
||||||
|
| `->limit(int)` | Max number of root records to return |
|
||||||
|
| `->skip(int)` | Offset for pagination |
|
||||||
|
| `->sort(string)` | Sort by field. Prefix with `-` for descending e.g. `"-_sys.updatedAt"` |
|
||||||
|
| `->status(array)` | Filter by status array e.g. `["published", "draft"]` |
|
||||||
|
| `->onlyPublished()` | Shorthand for `->status(["published"])` |
|
||||||
|
| `->childrenDepth(int)` | How many levels of children to load |
|
||||||
|
| `->childrenLimit(int)` | Max children per record |
|
||||||
|
| `->childrenFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->parentsDepth(int)` | How many levels of parents to load |
|
||||||
|
| `->parentsLimit(int)` | Max parents per record |
|
||||||
|
| `->parentFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->notLinked(string)` | Return only records with no parents |
|
||||||
|
| `->run()` | Execute and return a `Graph` |
|
||||||
|
| `->tree()` | Execute and return a `Collection` (tree format) |
|
||||||
|
| `->runWithCount()` | Execute and return a `Graph` with a `total` count |
|
||||||
|
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
You can filter your query with the following format:
|
Filter keys use the format `field_operator`. When no operator suffix is given, `eq` is assumed.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->filter(["field_operator" => "value"]);
|
$query->filter(["field_operator" => "value"]);
|
||||||
|
|
||||||
// example:
|
// No operator = eq
|
||||||
|
$query->filter(["schema" => "blogPosts"]);
|
||||||
|
|
||||||
$query->filter(["date_lt" => "2020-09-15"]);
|
// With operator
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Or filters are also available:
|
### Operator Reference
|
||||||
|
|
||||||
|
| Operator | Description |
|
||||||
|
|---|---|
|
||||||
|
| _(none)_ or `eq` | Equals (string) |
|
||||||
|
| `ne` | Not equals (string) |
|
||||||
|
| `eqnum` | Equals (numeric) |
|
||||||
|
| `neqnum` | Not equals (numeric) |
|
||||||
|
| `eqtrue` | Equals `true` |
|
||||||
|
| `eqfalse` | Equals `false` |
|
||||||
|
| `netrue` | Not equals `true` |
|
||||||
|
| `nefalse` | Not equals `false` |
|
||||||
|
| `regex` | Regular expression match |
|
||||||
|
| `in` | Value is in array (strings) |
|
||||||
|
| `nin` | Value is not in array (strings) |
|
||||||
|
| `innum` | Value is in array (numeric) |
|
||||||
|
| `ninnum` | Value is not in array (numeric) |
|
||||||
|
| `lt` | Less than |
|
||||||
|
| `lte` | Less than or equal |
|
||||||
|
| `gt` | Greater than |
|
||||||
|
| `gte` | Greater than or equal |
|
||||||
|
| `null` | Field is null |
|
||||||
|
| `nnull` | Field is not null |
|
||||||
|
| `exists` | Field key exists |
|
||||||
|
| `nexists` | Field key does not exist |
|
||||||
|
| `filter` | Raw filter passthrough |
|
||||||
|
|
||||||
|
### OR Filters
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query
|
$query
|
||||||
->filter(["price_eqn" => 10])
|
->filter(["schema" => "blogPosts"])
|
||||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"])
|
->orFilter(["title_regex" => "search", "slug_regex" => "search"]);
|
||||||
;
|
|
||||||
```
|
```
|
||||||
## Operator List
|
|
||||||
|
|
||||||
- regex
|
Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together.
|
||||||
- eq
|
|
||||||
- ne
|
|
||||||
- eqnum
|
|
||||||
- neqnum
|
|
||||||
- filter
|
|
||||||
- eqtrue
|
|
||||||
- eqfalse
|
|
||||||
- netrue
|
|
||||||
- nefalse
|
|
||||||
- in:
|
|
||||||
- innum
|
|
||||||
- nin
|
|
||||||
- ninnum
|
|
||||||
- lt
|
|
||||||
- lte
|
|
||||||
- gt
|
|
||||||
- gte
|
|
||||||
- null
|
|
||||||
- nnull
|
|
||||||
- exists
|
|
||||||
- nexists
|
|
||||||
|
|
||||||
## Example
|
### Cross-Schema (Children) Filters
|
||||||
|
|
||||||
Get 10 posts from the sports category as a tree
|
You can filter records by properties of their related records using the `children.` prefix:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query->filter([
|
||||||
|
"schema" => "blogPosts",
|
||||||
|
"children.categories.data.slug" => "sports",
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns `blogPosts` that have a child in `categories` where `slug` equals `sports`.
|
||||||
|
|
||||||
|
|
||||||
|
## Nested Data Fields
|
||||||
|
|
||||||
|
Use dot notation to filter on JSON data fields and system fields:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Filter on a data field
|
||||||
|
$query->filter(["data.status_eq" => "draft"]);
|
||||||
|
|
||||||
|
// Filter on a system field
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Get published blog posts, newest first:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$posts = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->onlyPublished()
|
||||||
|
->sort("-_sys.updatedAt")
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get posts from a specific category with children loaded:**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$posts = $query
|
$posts = $query
|
||||||
->filter([
|
->filter([
|
||||||
"schema" => "posts",
|
"schema" => "blogPosts",
|
||||||
"children.categories.data.slug" => "sports",
|
"children.categories.data.slug" => "sports",
|
||||||
]
|
])
|
||||||
)
|
|
||||||
->childrenDepth(1)
|
->childrenDepth(1)
|
||||||
|
->childrenFields(["categories"])
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->tree();
|
->tree();
|
||||||
|
|
||||||
$posts->map(...)->toArray();
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Paginate records:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$graph = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->limit(20)
|
||||||
|
->skip(40)
|
||||||
|
->runWithCount();
|
||||||
|
|
||||||
|
$posts = $graph->tree();
|
||||||
|
$total = $graph->total;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Search across fields:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$results = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->orFilter(["title_regex" => $term, "slug_regex" => $term])
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|||||||
+95
-62
@@ -5,86 +5,119 @@ include_toc: true
|
|||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
||||||
Schemas define both the shape of your data and how the UI on the admin will behave.
|
Schemas define both the shape of your data and how the admin UI behaves.
|
||||||
|
|
||||||
There are 3 types of schemas
|
There are 2 types of schemas:
|
||||||
|
|
||||||
- Collections: Normal data
|
- **collection** — Regular data records
|
||||||
- Files: Images and files
|
- **files** — Images and file uploads
|
||||||
- Block: Used in the block editor
|
|
||||||
|
|
||||||
|
|
||||||
## Collection Reference
|
## Collection Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "collection"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` |
|
||||||
- **visible**: An array of field id to show on the content browser _optional_
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"collection"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
|
| `visible` | no | Field IDs to show in the content browser list |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `sortBy` | no | Default sort in browser. Prefix with `-` for descending e.g. `-_sys.updatedAt` |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardTitle` | no | Mustache template for the record preview title e.g. `{{name}} - {{slug}}` |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
|
| `revisions` | no | Number of revisions to keep per record. Default: `0` (disabled) |
|
||||||
|
| `read` | no | Roles with read access. Empty means all roles can read |
|
||||||
|
| `write` | no | Roles with write access. Empty means all roles can write |
|
||||||
|
|
||||||
|
|
||||||
## Files Reference
|
## Files Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "files"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` |
|
||||||
- **path**: The relative directory that these files will be stored.
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"files"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
| `sortBy` | no | Default sort in browser |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `cardTitle` | no | Mustache template for the record preview title |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `revisions` | no | Number of revisions to keep per record |
|
||||||
|
| `read` | no | Roles with read access |
|
||||||
|
| `write` | no | Roles with write access |
|
||||||
|
|
||||||
|
### File Metadata
|
||||||
|
|
||||||
|
Every uploaded file is automatically stored with the following metadata (stored in `lucent_files`):
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `id` | Unique file ID |
|
||||||
|
| `recordId` | ID of the record this file belongs to |
|
||||||
|
| `originalName` | Original filename as uploaded |
|
||||||
|
| `mime` | MIME type e.g. `image/webp` |
|
||||||
|
| `path` | Storage path e.g. `files/{recordId}/{filename}` |
|
||||||
|
| `size` | File size in bytes |
|
||||||
|
| `width` | Image width in pixels (0 for non-images) |
|
||||||
|
| `height` | Image height in pixels (0 for non-images) |
|
||||||
|
| `checksum` | SHA-1 hash of the file contents |
|
||||||
|
|
||||||
|
Image files (jpeg, png, webp, gif, tiff) also get a 300×300 thumbnail generated automatically at `thumbs/{path}`, plus any image filter presets configured in `lucent.imageFilters`.
|
||||||
|
|
||||||
|
|
||||||
## Block Reference
|
## System Fields
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
Every record automatically has these read-only system fields available in queries:
|
||||||
- **label**: The friendly name of the schema
|
|
||||||
- **type**: The type of the collection. Should be "block"
|
| Field | Description |
|
||||||
- **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` |
|
||||||
|
|
||||||
|
|
||||||
A full Collection example without the fields:
|
## Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"label": "Departments",
|
"schemas": [
|
||||||
"name": "departments",
|
{
|
||||||
"isEntry": true,
|
"label": "Blog Posts",
|
||||||
"type": "collection",
|
"name": "blogPosts",
|
||||||
"visible": [
|
"isEntry": true,
|
||||||
"slug",
|
"type": "collection",
|
||||||
"cover",
|
"visible": [
|
||||||
"_sys.updatedAt",
|
"title",
|
||||||
"status"
|
"slug",
|
||||||
|
"_sys.updatedAt",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"Content",
|
||||||
|
"SEO"
|
||||||
|
],
|
||||||
|
"sortBy": "-_sys.createdAt",
|
||||||
|
"cardTitle": "{{title}}",
|
||||||
|
"cardImage": "cover",
|
||||||
|
"revisions": 15,
|
||||||
|
"read": [
|
||||||
|
"admin",
|
||||||
|
"editors",
|
||||||
|
"reviewers"
|
||||||
|
],
|
||||||
|
"write": [
|
||||||
|
"admin",
|
||||||
|
"editors"
|
||||||
|
],
|
||||||
|
"fields": []
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"groups": [
|
"roles": ["admin", "editors", "reviewers"]
|
||||||
"Extra Info",
|
|
||||||
"Metadata",
|
|
||||||
"SEO"
|
|
||||||
],
|
|
||||||
"sortBy": "-_sys.createdAt",
|
|
||||||
"titleTemplate": "{{name}} {{slug}}",
|
|
||||||
"revisions": 15,
|
|
||||||
"read": [
|
|
||||||
"admin",
|
|
||||||
"editors",
|
|
||||||
"reviewers"
|
|
||||||
],
|
|
||||||
"write": [
|
|
||||||
"admin",
|
|
||||||
"editors"
|
|
||||||
],
|
|
||||||
"fields": []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
+84
-25
@@ -5,51 +5,94 @@ To generate the static content of the website, lucent provides a helper class.
|
|||||||
|
|
||||||
## Laravel Command
|
## Laravel Command
|
||||||
|
|
||||||
Just create a command and name it as you like. Make sure to inject the StaticGenerator class.
|
Create an Artisan command and inject `StaticGenerator`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function __construct(
|
class GenerateStatic extends Command
|
||||||
public StaticGenerator $staticGenerator,
|
{
|
||||||
) {
|
protected $signature = 'generate:static';
|
||||||
parent::__construct();
|
|
||||||
|
public function __construct(
|
||||||
|
public StaticGenerator $staticGenerator,
|
||||||
|
public Context $ctx,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$this->staticGenerator->run('generate:static', function ($writer) {
|
||||||
|
$writer->save("/", $this->ctx->render("homepage"));
|
||||||
|
$writer->save("/about", $this->ctx->render("about"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Redirect command
|
`run(string $signature, callable $callback)` — the first argument must match the command's artisan signature. This is used to stream live build logs in the Lucent UI. The callback receives a `Writer` instance.
|
||||||
|
|
||||||
There are cases which is useful to create a redirect like when you have a multilingual website:
|
|
||||||
|
## Writer: save
|
||||||
|
|
||||||
|
Writes an HTML file at the given path:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$this->staticGenerator->run(function ($writer) {
|
$writer->save("/blog/my-post", $html);
|
||||||
$writer->createRedirect("/", "/el");
|
// writes to: storage/lucent/build/blog/my-post/index.html
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## The Writer save command
|
An optional third argument changes the file extension (default `"html"`).
|
||||||
|
|
||||||
In order to create an html file, you have to use the writer's save command
|
|
||||||
|
|
||||||
The first argument is the url and the second is the rendered HTML. That's where the page classes do come handy.
|
## Writer: createRedirect
|
||||||
|
|
||||||
|
Generates an HTML meta-refresh redirect. Useful for root-level locale redirects:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$this->staticGenerator->run(function ($writer) {
|
$writer->createRedirect("/", "/el");
|
||||||
$writer->save("/", $this->ctx->render("homepage"));
|
$writer->createRedirect("/", "/el", "Redirecting", "Please wait...");
|
||||||
$writer->save("/about", $this->ctx->render("about"));
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Arguments: `from`, `to`, `title` (default `"Redirecting"`), `message` (default `"Redirecting Soon..."`).
|
||||||
|
|
||||||
|
|
||||||
|
## Writer: recordIterator
|
||||||
|
|
||||||
|
Iterates over all records in paginated batches — useful when there are too many records to load at once:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$writer->recordIterator(
|
||||||
|
query: fn(int $limit, int $skip) => $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->onlyPublished()
|
||||||
|
->limit($limit)
|
||||||
|
->skip($skip)
|
||||||
|
->tree(),
|
||||||
|
parser: function ($records) use ($writer) {
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$writer->save(
|
||||||
|
"/blog/" . $record->data->slug,
|
||||||
|
$this->ctx->render("blogPost", $record),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Signature: `recordIterator(callable $query, callable $parser, int $skip = 0, int $limit = 100): int`
|
||||||
|
|
||||||
|
The `$query` callable receives `($limit, $skip)` and must return a collection. Iteration stops automatically when a batch returns zero records.
|
||||||
|
|
||||||
|
|
||||||
## Storage and Permissions
|
## Storage and Permissions
|
||||||
|
|
||||||
All the generated html is stored on `storage/lucent/live`
|
All generated HTML is written to `storage/lucent/build` during the run, then atomically swapped to `storage/lucent/live` on completion. Create the public symlink with:
|
||||||
In order to make it accessible you have to create symlink:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan lucent:livelink
|
php artisan lucent:livelink
|
||||||
```
|
```
|
||||||
|
|
||||||
Now your static website is accessible in http://localhost:8000/live
|
The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost:
|
||||||
|
|
||||||
But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost
|
|
||||||
|
|
||||||
```nginxconf
|
```nginxconf
|
||||||
location / {
|
location / {
|
||||||
@@ -57,14 +100,30 @@ location / {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Artisan Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent:setup-db` | Create all Lucent database tables |
|
||||||
|
| `lucent:schemas` | Compile schema JSON files |
|
||||||
|
| `lucent:livelink` | Create the `public/live` symlink |
|
||||||
|
| `lucent:rebuild:thumbnails` | Regenerate thumbnails for all uploaded images |
|
||||||
|
| `lucent:removeOrphanEdges` | Remove edges pointing to deleted records |
|
||||||
|
| `lucent:generate:collection {name}` | Scaffold a new collection schema JSON file |
|
||||||
|
|
||||||
|
|
||||||
## Build from the Lucent UI
|
## Build from the Lucent UI
|
||||||
|
|
||||||
In your lucent.php config file you can define your command that is used to generate the static files:
|
Register your generate command in `config/lucent.php` so admin users can trigger it from the UI:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
"generateCommand" => "generate:static"
|
"commands" => [
|
||||||
|
"generate:static" => "Generate Static Site",
|
||||||
|
],
|
||||||
```
|
```
|
||||||
That way, the users will be able to initiate the build command through the user interface.
|
|
||||||
|
Only roles listed in `canBuild` can trigger commands from the UI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,3 +99,113 @@ class Context
|
|||||||
```
|
```
|
||||||
|
|
||||||
Add the middleware inside the HTTP Kernel file.
|
Add the middleware inside the HTTP Kernel file.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
After publishing the config (`php artisan vendor:publish --tag=lucent-config`), configure `config/lucent.php` via `.env`:
|
||||||
|
|
||||||
|
| Key | Env var | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `env` | `LUCENT_ENV` | `production` | `production` or `development` |
|
||||||
|
| `auth` | `LUCENT_AUTH` | `lucent` | Auth driver: `lucent` or `lunar` |
|
||||||
|
| `disk` | `LUCENT_DISK` | `public` | Laravel filesystem disk for uploads |
|
||||||
|
| `schemas_path` | `LUCENT_SCHEMAS_PATH` | `resources/lucent/schemas` | Where schema JSON files live |
|
||||||
|
| `database` | `LUCENT_DB_CONNECTION` | `DB_CONNECTION` | Database connection name |
|
||||||
|
| `name` | `LUCENT_NAME` | `Lucent` | CMS display name |
|
||||||
|
| `url` | `LUCENT_URL` | `APP_URL` | Base URL of the CMS |
|
||||||
|
| `previewTarget` | `LUCENT_PREVIEW_TARGET` | `previewTarget` | Preview route parameter |
|
||||||
|
| `commands` | — | `[]` | Artisan commands exposed to admin users (see [Static Generator](Static%20Generator.md)) |
|
||||||
|
| `imageFilters` | — | `[]` | Image filter presets applied to uploads (see below) |
|
||||||
|
| `canInvite` | — | `["admin"]` | Roles that can invite new users |
|
||||||
|
| `canBuild` | — | `["admin"]` | Roles that can trigger static builds |
|
||||||
|
| `systemUserId` | — | `""` | User ID used for console-initiated record writes |
|
||||||
|
|
||||||
|
|
||||||
|
## Image Filters
|
||||||
|
|
||||||
|
Image filters are Intervention Image filter classes applied to every uploaded image, producing additional versions (e.g. a cropped or watermarked variant). Each preset is stored at `templates/{name}/{original-path}` on the configured disk.
|
||||||
|
|
||||||
|
Define filters in `config/lucent.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
"imageFilters" => [
|
||||||
|
"hero" => \App\ImageFilters\HeroFilter::class,
|
||||||
|
"thumb" => \App\ImageFilters\ThumbFilter::class,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
A filter class must implement `Intervention\Image\Filters\FilterInterface`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\ImageFilters;
|
||||||
|
|
||||||
|
use Intervention\Image\Filters\FilterInterface;
|
||||||
|
use Intervention\Image\Image;
|
||||||
|
|
||||||
|
class HeroFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function applyFilter(Image $image): Image
|
||||||
|
{
|
||||||
|
return $image->fit(1200, 600)->encode('webp', 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every uploaded image automatically gets a 300×300 WebP thumbnail at `thumbs/{path}` in addition to any configured presets.
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Lucent uses **email-link (magic link) login** — no passwords. Users receive a time-limited link by email.
|
||||||
|
|
||||||
|
### Auth modes
|
||||||
|
|
||||||
|
Set `LUCENT_AUTH` to choose how users are stored:
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent` | Users stored in the `lucent_users` table. Supports roles. |
|
||||||
|
| `lunar` | Delegates to Lunar's `lunar_staff` table. Roles not supported. |
|
||||||
|
|
||||||
|
### Login flow
|
||||||
|
|
||||||
|
1. User submits their email at `/lucent/login`
|
||||||
|
2. A 32-character token is stored against the user with a timestamp
|
||||||
|
3. Lucent emails a login link containing the token
|
||||||
|
4. User clicks the link → token is validated (must be used within **1 hour**)
|
||||||
|
5. Session is established; token is cleared
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
|
||||||
|
The `/lucent/register` route is only available when **no users exist** in the system. Once the first admin registers, the route returns a redirect to `/lucent/login`. Registration automatically assigns the `admin` role and sends a login link.
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
Two middleware aliases are available for your routes:
|
||||||
|
|
||||||
|
| Alias | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent.auth` | Requires an active Lucent session |
|
||||||
|
| `lucent.guest` | Redirects authenticated users away |
|
||||||
|
|
||||||
|
### Roles
|
||||||
|
|
||||||
|
Roles are defined in your channel config. Only roles listed there are valid — anything else is silently stripped on assignment. The `canInvite` and `canBuild` config keys control which roles can invite users and trigger builds respectively.
|
||||||
|
|
||||||
|
On console commands and non-`/lucent` routes, `currentUserId()` returns `config("lucent.systemUserId")` instead of the session user.
|
||||||
|
|
||||||
|
|
||||||
|
## Database Tables
|
||||||
|
|
||||||
|
All Lucent-managed tables use the `lucent_` prefix:
|
||||||
|
|
||||||
|
| Table | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent_records` | All collection and file schema records |
|
||||||
|
| `lucent_files` | Uploaded file metadata |
|
||||||
|
| `lucent_edges` | Relationships between records |
|
||||||
|
| `lucent_revisions` | Record revision history |
|
||||||
|
| `lucent_users` | Users (when using `lucent` auth mode) |
|
||||||
|
| `lucent_command_logs` | Background command execution logs |
|
||||||
|
|
||||||
|
These are created automatically by running `php artisan lucent:setup`.
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+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
+3
-6
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"main.js": {
|
"main.js": {
|
||||||
"file": "assets/main.7c3e8b7b.js",
|
"file": "assets/main-DH0OAeUr.js",
|
||||||
|
"name": "main",
|
||||||
"src": "main.js",
|
"src": "main.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main.587d6006.css"
|
"assets/main-BVNnoznq.css"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"main.css": {
|
|
||||||
"file": "assets/main.587d6006.css",
|
|
||||||
"src": "main.css"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
-48
@@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
|
||||||
* to our Laravel back-end. This library automatically handles sending the
|
|
||||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
+135
-16
@@ -1,32 +1,151 @@
|
|||||||
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().toString(36).substring(2, length + 2);
|
return Math.random()
|
||||||
|
.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;
|
||||||
|
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)) {
|
||||||
|
if (!isArray(ed_value) || db_value.length !== ed_value.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return db_value.reduce((c, v, i) => {
|
||||||
|
if (c === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(v, ed_value[i]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(db_value) && isEmpty(ed_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_value == ed_value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(fn, delay) {
|
||||||
|
let timer;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUnique(array) {
|
||||||
|
return array.filter((value, index) => array.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUniqueBy(items, uniqueBy) {
|
||||||
|
const ids = new Set(items.map((item) => item[uniqueBy]));
|
||||||
|
return [...ids].map((id) => items.find((i) => i[uniqueBy] === id));
|
||||||
|
}
|
||||||
|
export function arrayUniqueCb(items, aFilter) {
|
||||||
|
const cache = new Set();
|
||||||
|
return items.filter((item) => {
|
||||||
|
const cacheValue = aFilter(item);
|
||||||
|
if (cache.has(cacheValue)) return false;
|
||||||
|
cache.add(cacheValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-44
@@ -1,64 +1,48 @@
|
|||||||
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 * as bootstrap from "bootstrap";
|
// import Mustache from "mustache";
|
||||||
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 = {
|
||||||
account: Account,
|
account: Account,
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
};
|
};
|
||||||
|
|
||||||
let loadedComponents = [];
|
let loadedComponents = [];
|
||||||
|
|
||||||
let loadSvelte = function () {
|
let loadSvelte = function () {
|
||||||
loadedComponents.map((comp) => comp.$destroy());
|
loadedComponents.map((comp) => comp.$destroy());
|
||||||
loadedComponents = [];
|
loadedComponents = [];
|
||||||
|
|
||||||
const elements = document.body.querySelectorAll(".lucent-component");
|
const elements = document.body.querySelectorAll(".lucent-component");
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const loadElement = function (element) {
|
||||||
|
const componentId = element.attributes["data-layout"].value;
|
||||||
|
const [_, component] = Object.entries(entryComponents).find(
|
||||||
|
([key, _]) => componentId === key,
|
||||||
|
);
|
||||||
|
if (!component) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
const loadElement = function (element) {
|
|
||||||
const componentId = element.attributes["data-layout"].value;
|
|
||||||
const [_, component] = Object.entries(entryComponents).find(
|
|
||||||
([key, _]) => componentId === key
|
|
||||||
);
|
|
||||||
if (!component) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonData = document.getElementById(
|
const jsonData = document.getElementById("json-" + componentId).innerHTML;
|
||||||
"json-" + componentId
|
const props = JSON.parse(jsonData);
|
||||||
).innerHTML;
|
const compOptions = {
|
||||||
const props = JSON.parse(jsonData);
|
target: element,
|
||||||
props.axios = axiosInstance;
|
props: props,
|
||||||
const compOptions = {
|
|
||||||
target: element,
|
|
||||||
props: props,
|
|
||||||
};
|
|
||||||
|
|
||||||
loadedComponents = [...loadedComponents, new component(compOptions)];
|
|
||||||
};
|
};
|
||||||
Array.from(elements).map(loadElement);
|
|
||||||
|
loadedComponents = [...loadedComponents, new component(compOptions)];
|
||||||
|
};
|
||||||
|
Array.from(elements).map(loadElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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,10 +21,7 @@
|
|||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
</script>
|
</script>
|
||||||
<div class="text-center">
|
|
||||||
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name}</a></h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<svelte:component this={components[view]} {title} {...data}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<svelte:component this={components[view]} {channel} {title} {...data} />
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
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 "./Navbar.svelte";
|
import Navbar from "./layout/Navbar.svelte";
|
||||||
import HomeIndex from "./home/Index.svelte";
|
import HomeIndex from "./home/Index.svelte";
|
||||||
import BuildReport from "./build/Report.svelte";
|
import BuildReport from "./build/Report.svelte";
|
||||||
|
import Header from "./layout/Header.svelte";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
members: Members,
|
members: Members,
|
||||||
@@ -23,19 +24,23 @@
|
|||||||
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("readableSchemas", channel.schemas.filter((s) => readableSchemas.includes(s.name)));
|
setContext(
|
||||||
|
"readableSchemas",
|
||||||
|
channel.schemas.filter((s) => readableSchemas.includes(s.name)),
|
||||||
|
);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar {sidebar}/>
|
<div class="main-wrapper">
|
||||||
|
<div class="sidebar-content">
|
||||||
<svelte:component this={components[view]} {title} {...data}/>
|
<Navbar schema={data.schema} />
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
<Header />
|
||||||
|
<svelte:component this={components[view]} {title} {...data} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
<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,54 +1,68 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let email = "";
|
let email = "";
|
||||||
let message = "";
|
let submitted = false;
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/login", { email: email })
|
||||||
.post(channel.lucentUrl + "/login", {
|
.then(() => {
|
||||||
email: email,
|
submitted = true;
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.catch(() => {});
|
||||||
console.log(response)
|
|
||||||
message = "You will receive an email with a login link"
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="scope-login">
|
||||||
{#if message}
|
<div class="bg-image"></div>
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="login-form">
|
||||||
{message}
|
{#if submitted}
|
||||||
</div>
|
<div class="alert alert-info" role="alert">
|
||||||
|
<p>
|
||||||
{:else}
|
You will receive an email with a login link at <b>{email}</b
|
||||||
|
>.
|
||||||
<form on:submit={login}>
|
</p>
|
||||||
<div class="mb-3">
|
<p>Check your spam folder</p>
|
||||||
<label for="emailaddress" class="form-label">Email address</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
bind:value={email}
|
|
||||||
class="form-control"
|
|
||||||
id="emailaddress"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="form">
|
||||||
|
<h2 class="mb-5">Enter Lucent</h2>
|
||||||
|
|
||||||
|
<form on:submit={login}>
|
||||||
|
<p>
|
||||||
|
Submit your email address and you will receive a <b
|
||||||
|
>login link</b
|
||||||
|
> to your email
|
||||||
|
</p>
|
||||||
|
<p>Don't forget to check your spam folder</p>
|
||||||
|
<div class="mt-5 mb-3">
|
||||||
|
<label for="emailaddress" class="form-label"
|
||||||
|
>Email address</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
class="form-control"
|
||||||
|
id="emailaddress"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-5 d-block">
|
<button class="bt bt-primary">
|
||||||
<SpinnerButton label="Login"/>
|
Send email
|
||||||
|
<img
|
||||||
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
|
src="/img/spinner.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import Avatar from "./Avatar.svelte";
|
import Avatar from "./Avatar.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -16,16 +17,15 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-name", {
|
||||||
.post(channel.lucentUrl + "/account/update-name", {
|
name: name,
|
||||||
name: name,
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show();
|
successAlert.show();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data.error;
|
errorMessage = error.response?.data.error;
|
||||||
console.log({errorMessage});
|
console.log({ errorMessage });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,55 +33,55 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-email", {
|
||||||
.post(channel.lucentUrl + "/account/update-email", {
|
email: email,
|
||||||
email: email,
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show();
|
successAlert.show();
|
||||||
})
|
})
|
||||||
.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-3">
|
<div class="input-group mb-5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
class="form-control"
|
class="form-control mb-3"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update"/>
|
<SpinnerButton label="Update Name" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form on:submit={saveEmail}>
|
<form on:submit={saveEmail}>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-5">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
class="form-control"
|
class="form-control mb-3"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<SpinnerButton label="Update"/>
|
<SpinnerButton label="Update Email" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this
|
<a
|
||||||
device</a>
|
class="list-group-item list-group-item-action"
|
||||||
|
href="{channel.lucentUrl}/logout">Logout from this device</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let name = "";
|
let name = "";
|
||||||
@@ -12,48 +13,45 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/register", {
|
||||||
.post(channel.lucentUrl + "/register", {
|
name: name,
|
||||||
name: name,
|
email: email,
|
||||||
email: email,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
window.location = channel.lucentUrl + "/login";
|
window.location = channel.lucentUrl + "/login";
|
||||||
})
|
})
|
||||||
.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">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="name"
|
id="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email address</label>
|
<label for="email" class="form-label">Email address</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="email"
|
id="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="text-center mt-5 d-block">
|
<div class="text-center mt-5 d-block">
|
||||||
<SpinnerButton label="Register"/>
|
<SpinnerButton label="Register" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let email;
|
export let email;
|
||||||
export let token;
|
export let token;
|
||||||
let successAlert;
|
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/verify", {
|
||||||
.post(channel.lucentUrl + "/verify", {
|
email: email,
|
||||||
email: email,
|
token: token,
|
||||||
token: token,
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessAlert bind:this={successAlert}/>
|
<div class="scope-login">
|
||||||
<div class="wrapper-tiny">
|
<div class="bg-image"></div>
|
||||||
|
<div class="login-form">
|
||||||
<form on:submit={login}>
|
<div class="form">
|
||||||
<div class="mb-3 text-center">
|
<h2 class="mb-5">Welcome to Lucent</h2>
|
||||||
<h3>Login as {email}</h3>
|
<form on:submit={login}>
|
||||||
|
<button class="bt bt-primary">
|
||||||
|
Enter as {email}
|
||||||
|
<img
|
||||||
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
|
src="/img/spinner.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="form-errors"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-5 d-block">
|
|
||||||
<SpinnerButton label="Enter"/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle execution of a function. Especially useful for rate limiting
|
||||||
|
* execution of handlers on events like resize and scroll.
|
||||||
|
*
|
||||||
|
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
|
||||||
|
* are most useful.
|
||||||
|
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
|
||||||
|
* as-is, to `callback` when the throttled-function is executed.
|
||||||
|
* @param {object} [options] - An object to configure options.
|
||||||
|
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
|
||||||
|
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
|
||||||
|
* one final time after the last throttled-function call. (After the throttled-function has not been called for
|
||||||
|
* `delay` milliseconds, the internal counter is reset).
|
||||||
|
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
|
||||||
|
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
|
||||||
|
* callback will never executed if both noLeading = true and noTrailing = true.
|
||||||
|
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
|
||||||
|
* false (at end), schedule `callback` to execute after `delay` ms.
|
||||||
|
*
|
||||||
|
* @returns {Function} A new, throttled, function.
|
||||||
|
*/
|
||||||
|
export function throttle(delay, callback, options) {
|
||||||
|
const {
|
||||||
|
noTrailing = false,
|
||||||
|
noLeading = false,
|
||||||
|
debounceMode = undefined,
|
||||||
|
} = options || {};
|
||||||
|
/*
|
||||||
|
* After wrapper has stopped being called, this timeout ensures that
|
||||||
|
* `callback` is executed at the proper times in `throttle` and `end`
|
||||||
|
* debounce modes.
|
||||||
|
*/
|
||||||
|
let timeoutID;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Keep track of the last time `callback` was executed.
|
||||||
|
let lastExec = 0;
|
||||||
|
|
||||||
|
// Function to clear existing timeout
|
||||||
|
function clearExistingTimeout() {
|
||||||
|
if (timeoutID) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel next exec
|
||||||
|
function cancel(options) {
|
||||||
|
const { upcomingOnly = false } = options || {};
|
||||||
|
clearExistingTimeout();
|
||||||
|
cancelled = !upcomingOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The `wrapper` function encapsulates all of the throttling / debouncing
|
||||||
|
* functionality and when executed will limit the rate at which `callback`
|
||||||
|
* is executed.
|
||||||
|
*/
|
||||||
|
function wrapper(...arguments_) {
|
||||||
|
let self = this;
|
||||||
|
let elapsed = Date.now() - lastExec;
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute `callback` and update the `lastExec` timestamp.
|
||||||
|
function exec() {
|
||||||
|
lastExec = Date.now();
|
||||||
|
callback.apply(self, arguments_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `debounceMode` is true (at begin) this is used to clear the flag
|
||||||
|
* to allow future `callback` executions.
|
||||||
|
*/
|
||||||
|
function clear() {
|
||||||
|
timeoutID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noLeading && debounceMode && !timeoutID) {
|
||||||
|
/*
|
||||||
|
* Since `wrapper` is being called for the first time and
|
||||||
|
* `debounceMode` is true (at begin), execute `callback`
|
||||||
|
* and noLeading != true.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExistingTimeout();
|
||||||
|
|
||||||
|
if (debounceMode === undefined && elapsed > delay) {
|
||||||
|
if (noLeading) {
|
||||||
|
/*
|
||||||
|
* In throttle mode with noLeading, if `delay` time has
|
||||||
|
* been exceeded, update `lastExec` and schedule `callback`
|
||||||
|
* to execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
lastExec = Date.now();
|
||||||
|
if (!noTrailing) {
|
||||||
|
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
|
||||||
|
* `callback`.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
} else if (noTrailing !== true) {
|
||||||
|
/*
|
||||||
|
* In trailing throttle mode, since `delay` time has not been
|
||||||
|
* exceeded, schedule `callback` to execute `delay` ms after most
|
||||||
|
* recent execution.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is true (at begin), schedule `clear` to execute
|
||||||
|
* after `delay` ms.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is false (at end), schedule `callback` to
|
||||||
|
* execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
timeoutID = setTimeout(
|
||||||
|
debounceMode ? clear : exec,
|
||||||
|
debounceMode === undefined ? delay - elapsed : delay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.cancel = cancel;
|
||||||
|
|
||||||
|
// Return the wrapper function.
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(delay, callback, options) {
|
||||||
|
const { atBegin = false } = options || {};
|
||||||
|
return throttle(delay, callback, { debounceMode: atBegin !== false });
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<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}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<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,71 +1,81 @@
|
|||||||
<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(channel.lucentUrl + "/build-report-source");
|
const eventSource = new EventSource(
|
||||||
|
channel.lucentUrl + "/command-report-source/" + command.signature,
|
||||||
|
);
|
||||||
|
|
||||||
eventSource.onmessage = function (event) {
|
eventSource.onmessage = function (event) {
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
date = data.date;
|
date = data.date;
|
||||||
logs = data.logs;
|
logs = data.logs;
|
||||||
|
anchorEl.scrollIntoView();
|
||||||
}
|
};
|
||||||
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(
|
||||||
axios.post(channel.lucentUrl + "/build").then(response => {
|
(response) => {
|
||||||
connect()
|
connect();
|
||||||
})
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
connect()
|
connect();
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny transparent mb-5">
|
<div class="common-wrapper">
|
||||||
<div class="lx-card mt-5">
|
<div class="lx-card mt-5">
|
||||||
|
|
||||||
<h3 class="header-small mb-5">{title}</h3>
|
<h3 class="header-small mb-5">{title}</h3>
|
||||||
|
|
||||||
<button on:click={buildWebsite} class="btn btn-outline-primary btn-sm mb-3" disabled={inProgress}>Start Build
|
<button
|
||||||
|
on:click={buildWebsite}
|
||||||
|
class="button primary mb-3"
|
||||||
|
disabled={inProgress}
|
||||||
|
>Start
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#if inProgress}
|
{#if inProgress}
|
||||||
<span class="badge text-bg-warning">
|
<span class="badge text-bg-warning"> Action in progress </span>
|
||||||
Build in progress
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !inProgress && logs}
|
{#if !inProgress && logs}
|
||||||
<span class="badge text-bg-info">
|
<span class="badge text-bg-info"> Action completed </span>
|
||||||
Build completed
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre>{logs}</pre>
|
<pre class="logs">{logs}
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<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,23 +1,33 @@
|
|||||||
<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,7 +3,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="notice notice-error" role="alert">
|
||||||
{message}
|
<div class="title">Submission Errors</div>
|
||||||
|
<div class="content"> {message}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -105,7 +105,29 @@
|
|||||||
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 = "";
|
||||||
@@ -116,15 +138,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
class="bi"
|
class="bi"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{width}
|
{width}
|
||||||
{height}
|
{height}
|
||||||
viewBox={selectedIcon.viewBox}
|
viewBox={selectedIcon.viewBox}
|
||||||
aria-labelledby={icon}
|
aria-labelledby={icon}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
{stroke}
|
{stroke}
|
||||||
{fill}
|
{fill}
|
||||||
|
|
||||||
>
|
>
|
||||||
{@html selectedIcon.path}
|
{@html selectedIcon.path}
|
||||||
</svg>
|
</svg>
|
||||||
@@ -132,5 +155,6 @@
|
|||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<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="btn btn-primary btn-spinner" {disabled}>
|
<button type="submit" class="button secondary btn-spinner" {disabled}>
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,21 +12,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isVisible}
|
{#if isVisible}
|
||||||
<div
|
<div class="notice notice-success" transition:fly={{ duration: 500 }} role="alert">
|
||||||
transition:fly={{ duration: 500 }}
|
<div class="title">Success</div>
|
||||||
class="lx-alert text-white bg-success border-1 border rounded px-3 py-0 text-center"
|
<div class="content"> {message}</div>
|
||||||
role="alert"
|
</div>
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.lx-alert {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
top: 45px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
export let checked = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" {value} on:change
|
||||||
|
class="switch" {checked}/>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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,5 +1,6 @@
|
|||||||
<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;
|
||||||
@@ -8,10 +9,9 @@
|
|||||||
|
|
||||||
function deleteRecords(e) {
|
function deleteRecords(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/delete", {
|
||||||
.post(channel.lucentUrl + "/records/delete", {
|
ids: selected.map((s) => s.id),
|
||||||
ids: selected.map((s) => s.id),
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
})
|
})
|
||||||
@@ -21,11 +21,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeStatus(e, status) {
|
function changeStatus(e, status) {
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/status/" + status, {
|
||||||
.post(channel.lucentUrl + "/records/status/" + status, {
|
schemaName: schema.name,
|
||||||
schemaName: schema.name,
|
records: selected,
|
||||||
records: selected
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
})
|
})
|
||||||
@@ -35,49 +34,47 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div style="display: flex;align-items: center; gap: 8px">
|
||||||
<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
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
>Publish
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
>Make Draft
|
||||||
|
</button>
|
||||||
|
{#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="btn btn-sm btn-outline-primary">Publish
|
class="button"
|
||||||
</button
|
>Publish
|
||||||
>
|
</button>
|
||||||
<button
|
{#if schema.hasDrafts}
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary">Make Draft
|
|
||||||
</button
|
|
||||||
>
|
|
||||||
{#if filter["status_in"] === "trashed"}
|
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-primary">Publish
|
class="button"
|
||||||
</button
|
>Make Draft
|
||||||
>
|
</button>
|
||||||
{#if schema.hasDrafts}
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary">Make Draft
|
|
||||||
</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={deleteRecords}
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary">Delete forever
|
|
||||||
</button
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
|
||||||
class="btn btn-sm btn-outline-primary">Move to trash
|
|
||||||
</button
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<button
|
||||||
|
on:click|preventDefault={deleteRecords}
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
>Delete forever
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
||||||
|
class="button"
|
||||||
|
>Move to trash
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<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,16 +1,15 @@
|
|||||||
<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 Grid from "./Grid.svelte";
|
import { apiGet } from "../../helpers";
|
||||||
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;
|
||||||
@@ -27,18 +26,18 @@
|
|||||||
|
|
||||||
function refresh(e) {
|
function refresh(e) {
|
||||||
const newUrl = e.detail;
|
const newUrl = e.detail;
|
||||||
axios
|
apiGet(newUrl)
|
||||||
.get(newUrl)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
records = response.data.records;
|
records = response.records;
|
||||||
sortParam = response.data.sortParam;
|
sortParam = response.sortParam;
|
||||||
sortField = response.data.sortField;
|
sortField = response.sortField;
|
||||||
operators = response.data.operators;
|
operators = response.operators;
|
||||||
filter = response.data.filter;
|
filter = response.filter;
|
||||||
skip = response.data.skip;
|
skip = response.skip;
|
||||||
limit = response.data.limit;
|
limit = response.limit;
|
||||||
total = response.data.total;
|
total = response.total;
|
||||||
modalUrl = response.data.modalUrl;
|
modalUrl = response.modalUrl;
|
||||||
|
document.querySelector("dialog h3").scrollIntoView();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -46,59 +45,50 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-large transparent ">
|
<div class="">
|
||||||
<div class="lx-card mb-4 mt-0">
|
<div class={inModal ? "mt-0" : "mt-5"}>
|
||||||
<h3 class="header-normal mb-5 ">
|
<h3 class="header-normal mb-5">
|
||||||
{schema.label}
|
{schema.label}
|
||||||
</h3>
|
</h3>
|
||||||
{#if selected.length > 0 && !inModal && isWritable}
|
{#if selected.length > 0 && !inModal && isWritable}
|
||||||
<ActionsOnSelected {schema} {selected} {filter}/>
|
<ActionsOnSelected {schema} {selected} {filter} />
|
||||||
{:else}
|
{:else}
|
||||||
<Tools
|
<Tools
|
||||||
bind:schema
|
bind:schema
|
||||||
bind:records
|
bind:records
|
||||||
{systemFields}
|
{systemFields}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
{operators}
|
{operators}
|
||||||
{filter}
|
{filter}
|
||||||
{inModal}
|
{graph}
|
||||||
{modalUrl}
|
{inModal}
|
||||||
{isWritable}
|
{modalUrl}
|
||||||
on:refresh={refresh}
|
{isWritable}
|
||||||
|
on:refresh={refresh}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if schema.type === "collection"}
|
<Table
|
||||||
<Table
|
{records}
|
||||||
{records}
|
{graph}
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
{systemFields}
|
{systemFields}
|
||||||
{inModal}
|
{inModal}
|
||||||
{users}
|
{users}
|
||||||
{isWritable}
|
{isWritable}
|
||||||
bind:selected
|
bind:selected
|
||||||
/>
|
/>
|
||||||
{:else}
|
|
||||||
<Grid
|
|
||||||
{records}
|
|
||||||
{schema}
|
|
||||||
{isWritable}
|
|
||||||
bind:selected
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
{limit}
|
{limit}
|
||||||
{skip}
|
{skip}
|
||||||
{total}
|
{total}
|
||||||
on:refresh={refresh}
|
on:refresh={refresh}
|
||||||
{inModal}
|
{inModal}
|
||||||
{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 queryRecord;
|
export let graph;
|
||||||
|
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,40 +19,48 @@
|
|||||||
class="field-ui-{field.info.name}"
|
class="field-ui-{field.info.name}"
|
||||||
class:is-sort={field.name === sortField.name}
|
class:is-sort={field.name === sortField.name}
|
||||||
>
|
>
|
||||||
<RenderField {queryRecord} {field}/>
|
<RenderField {record} {schema} {graph} {field} />
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
{#if schema.visible.includes("status")}
|
{#if schema.visible?.includes("_sys.status")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
||||||
>
|
>
|
||||||
<Status status={queryRecord.record.status}/>
|
<Status status={record.status} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible.includes("_sys.createdBy")}
|
{#if schema.visible?.includes("_sys.createdBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
|
class:is-sort={"-_sys.createdBy" == sortParam ||
|
||||||
|
"_sys.createdBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, queryRecord.record._sys.createdBy)} side={24}/>
|
<Avatar name={usernameById(users, record.createdBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible.includes("_sys.updatedBy")}
|
{#if schema.visible?.includes("_sys.updatedBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
|
class:is-sort={"-_sys.updatedBy" == sortParam ||
|
||||||
|
"_sys.updatedBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, queryRecord.record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible.includes("_sys.createdAt")}
|
{#if schema.visible?.includes("_sys.createdAt")}
|
||||||
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(queryRecord.record._sys.createdAt)}
|
class:is-sort={"-_sys.createdAt" == sortParam ||
|
||||||
|
"_sys.createdAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible.includes("_sys.updatedAt")}
|
{#if schema.visible?.includes("_sys.updatedAt")}
|
||||||
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(queryRecord.record._sys.updatedAt)}
|
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
||||||
|
"_sys.updatedAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.updatedAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -29,13 +29,17 @@
|
|||||||
file: File,
|
file: File,
|
||||||
};
|
};
|
||||||
export let field;
|
export let field;
|
||||||
export let queryRecord;
|
export let schema;
|
||||||
|
export let record;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={renderElements[field.info.name]}
|
this={renderElements[field.info.name]}
|
||||||
value={queryRecord.record.data[field.name]}
|
value={record.data[field.name]}
|
||||||
{queryRecord}
|
{record}
|
||||||
|
{graph}
|
||||||
|
{schema}
|
||||||
{field}
|
{field}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import RecordRow from "./RecordRow.svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import {previewTitle} from "../records/Preview";
|
import { usernameById } from "../account/users";
|
||||||
import {usernameById} from "../account/users";
|
import { getContext } from "svelte";
|
||||||
import {getContext} from "svelte";
|
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {selectRecord, toggleAll} from "./functions/recordSelect.js";
|
import { selectRecord, toggleAll } from "./functions/recordSelect.js";
|
||||||
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
|
|
||||||
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;
|
||||||
@@ -19,105 +20,96 @@
|
|||||||
export let selected = [];
|
export let selected = [];
|
||||||
|
|
||||||
function eventToggleAll(e) {
|
function eventToggleAll(e) {
|
||||||
selected = toggleAll(e,records,selected)
|
selected = toggleAll(e, records, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(record) {
|
function select(record) {
|
||||||
selected = selectRecord(record, selected)
|
selected = selectRecord(record, selected);
|
||||||
}
|
}
|
||||||
|
console.log(schema);
|
||||||
$: visibleColumns = schema.fields.filter(c => schema.visible.includes(c.name))
|
$: visibleColumns = schema.fields.filter(
|
||||||
|
(c) => schema.visible?.includes(c.name) ?? [],
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="lx-table rounded">
|
<div class="table mt-5">
|
||||||
<table class="">
|
<table>
|
||||||
<thead class="table-light">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<th>
|
<th>
|
||||||
<input
|
<Checkbox
|
||||||
on:change|preventDefault={eventToggleAll}
|
value=""
|
||||||
|
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}
|
||||||
class="form-check-input"
|
></Checkbox>
|
||||||
type="checkbox"
|
</th>
|
||||||
/>
|
{/if}
|
||||||
</th>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each visibleColumns as field}
|
{#each visibleColumns as field}
|
||||||
<th
|
<th
|
||||||
class="field-ui-{field.info.name ?? field.ui}"
|
class="field-ui-{field.info.name ?? field.ui}"
|
||||||
class:is-sort={field.name === sortField.name}
|
class:is-sort={field.name === sortField.name}
|
||||||
scope="col"
|
scope="col"
|
||||||
title={field.help}
|
title={field.help}>{field.label}</th
|
||||||
data-bs-toggle="tooltip"
|
>
|
||||||
data-bs-placement="top">{field.label}</th
|
{/each}
|
||||||
>
|
{#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
|
||||||
{/each}
|
<th class:is-sort={sysField.name === sortField.name}
|
||||||
{#each systemFields.filter(c => schema.visible.includes(c.name)) as sysField}
|
>{sysField.label}</th
|
||||||
<th>{sysField.label}</th>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
<th></th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each records as queryRecord (queryRecord.record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title-td">
|
<td class="title-td">
|
||||||
<div
|
<div class="title-td-contents">
|
||||||
class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
|
|
||||||
>
|
|
||||||
<div class="d-flex align-items-center ">
|
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<div class="form-check">
|
<Checkbox
|
||||||
<input
|
on:change={() => select(record)}
|
||||||
on:change={() => select(queryRecord.record)}
|
checked={selected.find(
|
||||||
class="form-check-input "
|
(r) => r.id === record.id,
|
||||||
type="checkbox"
|
)}
|
||||||
checked={selected.find(
|
value={record}
|
||||||
(r) => r.id === queryRecord.record.id
|
></Checkbox>
|
||||||
)}
|
|
||||||
value={queryRecord}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
class="me-2 text-decoration-none text-dark fs-6"
|
target={inModal ? "_blank" : "_self"}
|
||||||
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
|
|
||||||
target={inModal ? "_blank" : "_self"}
|
|
||||||
title={previewTitle(queryRecord.record)}
|
|
||||||
data-bs-toggle="tooltip" data-bs-placement="left"
|
|
||||||
|
|
||||||
>
|
>
|
||||||
{previewTitle(queryRecord.record)}
|
{#if record.status === "draft"}
|
||||||
|
<span
|
||||||
|
style="text-transform: uppercase;font-size:10px"
|
||||||
|
>{record.status}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</td>
|
||||||
<Avatar
|
<RecordRow
|
||||||
name={usernameById(
|
{record}
|
||||||
users,
|
{graph}
|
||||||
queryRecord.record._sys.updatedBy
|
|
||||||
)}
|
|
||||||
side={24}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<RecordRow
|
|
||||||
{queryRecord}
|
|
||||||
{schema}
|
{schema}
|
||||||
{visibleColumns}
|
{visibleColumns}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
{users}
|
{users}
|
||||||
/>
|
/>
|
||||||
</tr>
|
<td>
|
||||||
{/each}
|
<Avatar
|
||||||
|
name={usernameById(users, record.updatedBy)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
|
|
||||||
export let queryRecord;
|
export let record;
|
||||||
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,16 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
|
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
|
||||||
|
|
||||||
export let queryRecord;
|
export let record;
|
||||||
export let field;
|
export let field;
|
||||||
$: recordEdges = queryRecord?._children[field.name];
|
export let schemas;
|
||||||
|
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="mr-3">
|
<span class="reference">
|
||||||
<PreviewCardSmall record={recordEdge}/>
|
<PreviewCardSmall {schemas} {graph} 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">
|
<li class="page-item" class:active={currentPage === i}>
|
||||||
{#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,7 +11,11 @@
|
|||||||
|
|
||||||
$: totalPages = Math.ceil(total / limit);
|
$: totalPages = Math.ceil(total / limit);
|
||||||
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
||||||
|
const range = (start, end, step = 1) =>
|
||||||
|
Array.from(
|
||||||
|
{ length: Math.ceil((end - start) / step) },
|
||||||
|
(_, i) => start + i * step,
|
||||||
|
);
|
||||||
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
||||||
return i > 0 && i <= totalPages;
|
return i > 0 && i <= totalPages;
|
||||||
});
|
});
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination">
|
||||||
{#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>
|
||||||
@@ -69,7 +73,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<p class="text-muted text-center">
|
<p style="display: flex;justify-content: center; gap: 4px">
|
||||||
Showing
|
Showing
|
||||||
<span class="font-medium">{+skip + 1}</span>
|
<span class="font-medium">{+skip + 1}</span>
|
||||||
to
|
to
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {previewTitle} from "../../records/Preview";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
export let operators;
|
export let operators;
|
||||||
@@ -10,8 +9,7 @@
|
|||||||
export let value;
|
export let value;
|
||||||
export let inModal;
|
export let inModal;
|
||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
export let records
|
export let graph;
|
||||||
|
|
||||||
let filter = {
|
let filter = {
|
||||||
label: "",
|
label: "",
|
||||||
operator: "",
|
operator: "",
|
||||||
@@ -19,10 +17,10 @@
|
|||||||
isReference: key.startsWith("children"),
|
isReference: key.startsWith("children"),
|
||||||
};
|
};
|
||||||
|
|
||||||
filter = [
|
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
|
||||||
extractOperator(key),
|
(mem, fn) => fn(mem),
|
||||||
extractLabel(schema, key),
|
filter,
|
||||||
].reduce((mem, fn) => fn(mem), filter);
|
);
|
||||||
|
|
||||||
function extractOperator(key) {
|
function extractOperator(key) {
|
||||||
return (filter) => {
|
return (filter) => {
|
||||||
@@ -47,17 +45,16 @@
|
|||||||
const filterField = schema.fields.find((f) => f.name === fieldName);
|
const filterField = schema.fields.find((f) => f.name === fieldName);
|
||||||
filter.label = filterField?.label ?? fieldName;
|
filter.label = filterField?.label ?? fieldName;
|
||||||
return filter;
|
return filter;
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterRecord = extractFilterRecord(records, value);
|
const filterRecord = extractFilterRecord(graph, value);
|
||||||
|
|
||||||
function extractFilterRecord(records, value) {
|
function extractFilterRecord(graph, value) {
|
||||||
if (!filter.isReference) {
|
if (!filter.isReference) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return records.find(r => r.id === value);
|
return graph.records.find((r) => r.id === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFilter(k) {
|
function removeFilter(k) {
|
||||||
@@ -73,30 +70,22 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="applied-filter d-inline-block border border-primary rounded lx-small-text me-1 px-2 py-1">
|
<span class="applied-filter">
|
||||||
<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 ?? ""}
|
||||||
{/if}
|
{operators.find((o) => o.name === filter.operator)?.hasValue
|
||||||
<button
|
? value
|
||||||
on:click|preventDefault={() => removeFilter(key)}
|
: ""}
|
||||||
type="button"
|
{/if}
|
||||||
class="btn-close btn-close ms-1"
|
|
||||||
aria-label="Close"
|
<button
|
||||||
/>
|
on:click|preventDefault={() => removeFilter(key)}
|
||||||
</div>
|
type="button"
|
||||||
|
class="button-text"
|
||||||
|
aria-label="Close"
|
||||||
|
><Icon width={12} height={12} icon="close"></Icon></button
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style>
|
|
||||||
.applied-filter {
|
|
||||||
background-color: #fff;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.applied-filter:hover {
|
|
||||||
opacity: .8;
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<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 FilterReferenceInput from "./FilterReferenceInput.svelte";
|
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
import FilterReferenceInput from "./FilterReferenceInput.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
@@ -14,50 +14,10 @@
|
|||||||
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] ?? "";
|
||||||
@@ -79,63 +39,195 @@
|
|||||||
} 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">
|
|
||||||
<Dropdown bind:this={dropdown} width="300">
|
|
||||||
<div slot="button">
|
|
||||||
<Icon icon="filter"/>
|
|
||||||
<span class="ms-1">Filter</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-3 py-1 d-flex align-items-center">
|
<div>
|
||||||
<select bind:value={selectedField} class="form-select">
|
<Dropdown bind:this={dropdown}>
|
||||||
{#each filterableFields as field}
|
<div slot="button">
|
||||||
<option value={field}>{field.label}</option>
|
<Icon icon="filter"/>
|
||||||
{/each}
|
<span class="ms-1">Filter</span>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
<div class:hide={activeMenu !== "main"}>
|
||||||
<div class="px-3 py-1 d-flex align-items-center">
|
<button class="dropdown-item button" on:click={e => activeMenu = "byField" }>
|
||||||
<select class="form-select" bind:value={selectedOperator}>
|
Filter by field
|
||||||
{#each operatorsFiltered as operator}
|
</button>
|
||||||
<option value={operator}>{operator.label}</option>
|
<button class="dropdown-item button" on:click={e => activeMenu = "byReference" }>
|
||||||
{/each}
|
Filter by Reference
|
||||||
</select>
|
</button>
|
||||||
</div>
|
<button class="dropdown-item button" on:click={e => activeMenu = "advanced" }>
|
||||||
<div class="px-3 py-1 d-flex align-items-center">
|
Advanced filter
|
||||||
{#if selectedField?.info?.name === "reference" && selectedOperator.name === "eq"}
|
</button>
|
||||||
<FilterReferenceInput field={selectedField} bind:value={selectedInput} on:addFilter={addFilter}/>
|
</div>
|
||||||
{:else}
|
<div class:hide={activeMenu !== "byField"}>
|
||||||
|
{#if !activeField}
|
||||||
|
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
||||||
|
<Icon icon="arrow-left"></Icon>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
{#each dataFields as field}
|
||||||
|
<button class="dropdown-item button" on:click={e => selectField(e,field)}>
|
||||||
|
{field.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeField && !activeOperator}
|
||||||
|
<button class="dropdown-item button" on:click={e => activeField = null }>
|
||||||
|
<Icon icon="arrow-left"></Icon>
|
||||||
|
Back
|
||||||
|
</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={addFilter}
|
on:click={applyFilter}
|
||||||
class="btn btn-outline-primary"
|
class="button applied-filter"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Add filter
|
Add filter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<hr/>
|
</div>
|
||||||
<div><h6 class="dropdown-header">Advanced filters</h6></div>
|
<div class:hide={activeMenu !== "byReference"}>
|
||||||
<form on:submit={submitSearch}>
|
{#if !activeReference}
|
||||||
<div class="px-3 py-1 d-flex align-items-center">
|
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
||||||
<input
|
<Icon icon="arrow-left"></Icon>
|
||||||
bind:value={search}
|
Back
|
||||||
type="search"
|
</button>
|
||||||
class="form-control"
|
{#each referenceFields as field}
|
||||||
placeholder="Advanced filters"
|
<button class="dropdown-item button" on:click={e => selectReference(e,field)}>
|
||||||
required
|
{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>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
</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}>
|
||||||
|
<input
|
||||||
|
bind:value={search}
|
||||||
|
type="search"
|
||||||
|
class="mb-2 mt-2"
|
||||||
|
placeholder="Advanced filters"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="button applied-filter">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {debounce} from "lodash";
|
import { apiGet, debounce } from "../../../helpers";
|
||||||
import {previewTitle} from "../../records/Preview";
|
|
||||||
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -10,22 +8,20 @@
|
|||||||
export let value = "";
|
export let value = "";
|
||||||
export let field;
|
export let field;
|
||||||
|
|
||||||
let search = ""
|
let search = "";
|
||||||
$: searchOptions = []
|
$: searchOptions = [];
|
||||||
|
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/records/suggestions", {
|
||||||
.get(channel.lucentUrl + "/records/suggestions", {
|
params: {
|
||||||
params: {
|
schema: field.collections[0],
|
||||||
schema: field.collections[0],
|
field: "search",
|
||||||
field: "search",
|
value: search,
|
||||||
value: search,
|
ui: "search",
|
||||||
ui: "search",
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = response.data;
|
searchOptions = response;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
@@ -35,44 +31,36 @@
|
|||||||
|
|
||||||
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>
|
||||||
|
|
||||||
<input
|
<div class="reference-tags">
|
||||||
|
<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)}
|
||||||
>
|
>
|
||||||
<span class="dropdown-item">
|
{option.data.name}
|
||||||
{previewTitle( option)}
|
</div>
|
||||||
</span>
|
{:else}
|
||||||
</div>
|
<div class="start-typing">Start typing...</div>
|
||||||
|
{/each}
|
||||||
{:else}
|
{/if}
|
||||||
|
</div>
|
||||||
Start typing...
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<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;
|
||||||
@@ -31,101 +32,91 @@
|
|||||||
|
|
||||||
function sortAsc(e, field) {
|
function sortAsc(e, field) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
|
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
|
||||||
return triggerSortField(prefix + field.name);
|
return triggerSortField(prefix + field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortDesc(e, field) {
|
function sortDesc(e, field) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
|
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
|
||||||
return triggerSortField("-" + prefix + field.name);
|
return triggerSortField("-" + prefix + field.name);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" ">
|
|
||||||
<button
|
<Dropdown>
|
||||||
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
|
<div slot="button">
|
||||||
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>
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu" style="width:auto;max-width:800px;">
|
|
||||||
<div class="row">
|
|
||||||
{#each sortableFields as field}
|
|
||||||
<div class="col-4 px-3 py-1 d-flex align-items-center">
|
|
||||||
<div class="btn-group w-100">
|
|
||||||
<button
|
|
||||||
on:click={(e) => sortAsc(e, field)}
|
|
||||||
title="Sort Ascending"
|
|
||||||
class="btn btn-sm {field.name == sortField.name && !sortParam.startsWith("-")
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-outline-primary'} "
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-up-short-wide"/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
on:click={(e) => sortDesc(e, field)}
|
|
||||||
title="Sort Descending"
|
|
||||||
class="btn btn-sm {field.name == sortField.name && sortParam.startsWith("-")
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-outline-primary'} "
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-down-wide-short"/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Sort Ascending"
|
|
||||||
on:click={(e) => sortAsc(e, field)}
|
|
||||||
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
|
|
||||||
style="overflow: hidden;"
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<h6 class="dropdown-header px-0">System</h6>
|
|
||||||
<div class="row">
|
|
||||||
{#each systemFieldsFiltered as field}
|
|
||||||
<div class="col-4 px-3 py-1 d-flex align-items-center">
|
|
||||||
<div class="btn-group w-100">
|
|
||||||
<button
|
|
||||||
on:click={(e) => sortAsc(e, field)}
|
|
||||||
title="Sort Ascending"
|
|
||||||
class="btn btn-sm {field.name == sortParam
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-outline-primary'} "
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-up-short-wide"/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
on:click={(e) => sortDesc(e, field)}
|
|
||||||
title="Sort Descending"
|
|
||||||
class="btn btn-sm {'-' + field.name == sortParam
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-outline-primary'} "
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-down-wide-short"/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Sort Ascending"
|
|
||||||
on:click={(e) => sortAsc(e, field)}
|
|
||||||
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
|
|
||||||
style="overflow: hidden;"
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
{#each sortableFields as field}
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<button
|
||||||
|
on:click={(e) => sortAsc(e, field)}
|
||||||
|
title="Sort Ascending"
|
||||||
|
class="button button-icon {field.name == sortField.name && !sortParam.startsWith("-")
|
||||||
|
? 'active'
|
||||||
|
: ''} "
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={(e) => sortDesc(e, field)}
|
||||||
|
title="Sort Descending"
|
||||||
|
class="button button-icon {field.name == sortField.name && sortParam.startsWith("-")
|
||||||
|
? 'active'
|
||||||
|
: ''} "
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Sort Ascending"
|
||||||
|
on:click={(e) => sortAsc(e, field)}
|
||||||
|
class="button"
|
||||||
|
>
|
||||||
|
{field.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<h6 class="dropdown-header">System</h6>
|
||||||
|
{#each systemFieldsFiltered as field}
|
||||||
|
<div class="dropdown-item">
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={(e) => sortAsc(e, field)}
|
||||||
|
title="Sort Ascending"
|
||||||
|
class="button button-icon {field.name == sortParam
|
||||||
|
? 'active'
|
||||||
|
: ''} "
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={(e) => sortDesc(e, field)}
|
||||||
|
title="Sort Descending"
|
||||||
|
class="button button-icon {'-' + field.name == sortParam
|
||||||
|
? 'active'
|
||||||
|
: ''} "
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Sort Ascending"
|
||||||
|
on:click={(e) => sortAsc(e, field)}
|
||||||
|
class="button"
|
||||||
|
>
|
||||||
|
{field.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<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 {getContext, createEventDispatcher} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
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 = [];
|
||||||
|
|
||||||
@@ -36,125 +38,101 @@
|
|||||||
if (inModal) {
|
if (inModal) {
|
||||||
dispatch("refresh", url);
|
dispatch("refresh", url);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = url;
|
window.location = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadComplete(e) {
|
|
||||||
records = e.detail;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-3 d-flex align-items-center justify-content-between">
|
<div class="toolbar">
|
||||||
<div class=" d-flex align-items-center">
|
<div class="toolbar-filters">
|
||||||
|
|
||||||
<SortFields
|
<SortFields
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
{sortField}
|
{sortField}
|
||||||
{systemFields}
|
{systemFields}
|
||||||
{inModal}
|
{inModal}
|
||||||
{modalUrl}
|
{modalUrl}
|
||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FilterFields
|
<FilterFields
|
||||||
bind:schema
|
bind:schema
|
||||||
{systemFields}
|
{systemFields}
|
||||||
{operators}
|
{operators}
|
||||||
{filter}
|
{filter}
|
||||||
{inModal}
|
{inModal}
|
||||||
{modalUrl}
|
{modalUrl}
|
||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<form method="GET" on:submit={search}>
|
<form method="GET" on:submit={search}>
|
||||||
<input type="search" name="filter[search_regex]" placeholder="Search"
|
<input
|
||||||
class="form-control" required>
|
type="search"
|
||||||
|
name="filter[search_regex]"
|
||||||
|
placeholder="Search"
|
||||||
|
class="search"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center ">
|
<div style="display:flex;align-items: center;gap:4px">
|
||||||
{#if schema.type === "collection"}
|
{#if !inModal && isWritable}
|
||||||
{#if !inModal && isWritable}
|
<a
|
||||||
<a
|
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
class="button"
|
||||||
class="btn btn-sm btn-primary"
|
>
|
||||||
>
|
New Record
|
||||||
New Record
|
</a>
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{:else }
|
|
||||||
<div class="d-inline-block ms-1">
|
|
||||||
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !inModal}
|
{#if !inModal}
|
||||||
<div class="dropdown d-inline-block">
|
<Dropdown orientation="right">
|
||||||
<button
|
<div slot="button">
|
||||||
class="btn btn-link btn-sm"
|
<Icon icon="ellipsis-vertical" />
|
||||||
type="button"
|
</div>
|
||||||
data-bs-toggle="dropdown"
|
{#if filter["status_in"] === "trashed"}
|
||||||
aria-expanded="false"
|
{#if isWritable}
|
||||||
>
|
<a
|
||||||
<Icon icon="ellipsis-vertical"/>
|
class="dropdown-item"
|
||||||
</button>
|
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
||||||
|
>
|
||||||
<ul class="dropdown-menu">
|
Empty trash
|
||||||
{#if filter["status_in"] === "trashed"}
|
</a>
|
||||||
{#if isWritable}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
|
||||||
>
|
|
||||||
Empty trash
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href={csvUrl}
|
|
||||||
>Export to CSV</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
|
||||||
>View trashed records</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
</ul>
|
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
||||||
</div>
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
||||||
|
>View trashed records</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
|
||||||
|
>View unlinked records</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="applied-filters">
|
||||||
{#if Object.entries(filter).length > 0}
|
<AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
|
||||||
<div class=" d-flex mb-3">
|
></AppliedFilterNotLinked>
|
||||||
|
{#if Object.entries(filter).length > 0}
|
||||||
{#each Object.entries(filter) as [k, v]}
|
{#each Object.entries(filter) as [k, v]}
|
||||||
<AppliedFilter
|
<AppliedFilter
|
||||||
{schema}
|
{schema}
|
||||||
{operators}
|
{operators}
|
||||||
key={k}
|
key={k}
|
||||||
value={v}
|
value={v}
|
||||||
{inModal}
|
{inModal}
|
||||||
{modalUrl}
|
{modalUrl}
|
||||||
{records}
|
{graph}
|
||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import FileIndex from "./FileIndex.svelte";
|
||||||
|
|
||||||
|
let dialogEl;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const channel = getContext("channel");
|
||||||
|
$: files = [];
|
||||||
|
$: selectedRecords = [];
|
||||||
|
// onMount(() => {
|
||||||
|
// load();
|
||||||
|
// });
|
||||||
|
|
||||||
|
export function close(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogEl.close();
|
||||||
|
selectedRecords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(recordId) {
|
||||||
|
fetch(channel.lucentUrl + "/records/files/?recordId=" + recordId)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
files = json;
|
||||||
|
})
|
||||||
|
.catch((error) => console.log(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("insert_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("replace_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function open(recordId) {
|
||||||
|
dialogEl.showModal();
|
||||||
|
load(recordId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog bind:this={dialogEl}>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={insert}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Insert
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={replace}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
|
{#if selectedRecords.length > 0}
|
||||||
|
<span class="">
|
||||||
|
{selectedRecords.length} records selected
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={close}
|
||||||
|
type="button"
|
||||||
|
class="button close"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<Icon icon="close"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<FileIndex {files} bind:selected={selectedRecords}></FileIndex>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import { fileurl } from "../files/imageserver";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
export let files = [];
|
||||||
|
export let selected = [];
|
||||||
|
export let isWritable = true;
|
||||||
|
|
||||||
|
function eventToggleAll(e) {
|
||||||
|
selected = toggleAll(e, files, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(file) {
|
||||||
|
selected = selectFile(file, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleAll = (e, files, selected) => {
|
||||||
|
if (selected.length === files.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
e.currentTarget.checked = selected.length > 0;
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectFile = (file, selected) => {
|
||||||
|
let fileExists = selected.find((r) => r.id === file.id);
|
||||||
|
if (fileExists) {
|
||||||
|
return selected.filter((r) => r.id !== file.id);
|
||||||
|
}
|
||||||
|
return [...selected, file];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table mt-5">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#if isWritable}
|
||||||
|
<th>
|
||||||
|
<Checkbox
|
||||||
|
value=""
|
||||||
|
on:change={eventToggleAll}
|
||||||
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < files.length}
|
||||||
|
checked={selected.length === files.length}
|
||||||
|
></Checkbox>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each files as file (file.id)}
|
||||||
|
<tr>
|
||||||
|
<td class="title-td">
|
||||||
|
<div class="title-td-contents">
|
||||||
|
{#if isWritable}
|
||||||
|
<Checkbox
|
||||||
|
on:change={() => select(file)}
|
||||||
|
checked={selected.find(
|
||||||
|
(s) => s.id === file.id,
|
||||||
|
)}
|
||||||
|
value={file}
|
||||||
|
></Checkbox>
|
||||||
|
{/if}
|
||||||
|
<div class="file-table-row">
|
||||||
|
<Preview
|
||||||
|
{file}
|
||||||
|
size={file.width > 0 ? "medium" : "small"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{file.filename}
|
||||||
|
<span
|
||||||
|
>{(file.size / 1024).toFixed(1)}kB</span
|
||||||
|
>
|
||||||
|
|
||||||
|
{#if file.width > 0}
|
||||||
|
<span
|
||||||
|
>{file.width +
|
||||||
|
"x" +
|
||||||
|
file.height}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<a
|
||||||
|
href={fileurl(channel, file)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<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,21 +1,29 @@
|
|||||||
export function sortByField(from, to, queryRecords, fieldName) {
|
export function sortByField(from, to, edges, fieldName, references) {
|
||||||
if (from === to) {
|
if (from === to) {
|
||||||
return queryRecords;
|
return edges;
|
||||||
}
|
}
|
||||||
let edgesTosort = queryRecords?.filter((qr) => qr.edge.field === fieldName && qr.edge.depth === 1 ) ?? [];
|
let referenceIds = references.map((r) => r.id);
|
||||||
let remainingEdge = queryRecords?.filter((qr) => !(qr.edge.field === fieldName && qr.edge.depth === 1)) ?? [];
|
let edgesTosort =
|
||||||
|
edges?.filter(
|
||||||
|
(ed) =>
|
||||||
|
ed.field === fieldName &&
|
||||||
|
ed.depth === 1 &&
|
||||||
|
referenceIds.includes(ed.target),
|
||||||
|
) ?? [];
|
||||||
|
let remainingEdge =
|
||||||
|
edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
||||||
|
|
||||||
edgesTosort = array_move(edgesTosort,from, to);
|
edgesTosort = array_move(edgesTosort, from, to);
|
||||||
return [...remainingEdge, ...edgesTosort];
|
return [...remainingEdge, ...edgesTosort];
|
||||||
}
|
}
|
||||||
|
|
||||||
function array_move(arr, old_index, new_index) {
|
export function array_move(arr, old_index, new_index) {
|
||||||
if (new_index >= arr.length) {
|
if (new_index >= arr.length) {
|
||||||
var k = new_index - arr.length + 1;
|
var k = new_index - arr.length + 1;
|
||||||
while (k--) {
|
while (k--) {
|
||||||
arr.push(undefined);
|
arr.push(undefined);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
}
|
||||||
return arr; // for testing
|
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
||||||
};
|
return arr; // for testing
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import {imgurl} from "../files/imageserver";
|
import { imgurl } from "./imageserver.js";
|
||||||
import {getContext} from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export let record;
|
export let file;
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let size = "small";
|
export let size = "small";
|
||||||
export let showFilename = false;
|
export let showFilename = false;
|
||||||
let imageSide;
|
let imageSide;
|
||||||
let fileSide;
|
let fileSide;
|
||||||
let fontSize;
|
let fontSize;
|
||||||
|
|
||||||
|
console.log({ channel });
|
||||||
if (size == "large") {
|
if (size == "large") {
|
||||||
imageSide = 256;
|
imageSide = 256;
|
||||||
fileSide = 32;
|
fileSide = 32;
|
||||||
@@ -29,40 +31,48 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if record}
|
<div style="display: flex;align-items: center;gap: 5px;">
|
||||||
{#if record._file.mime.startsWith("image")}
|
{#if file}
|
||||||
<!-- href={imgurl(record)} -->
|
{#if file.mime.startsWith("image")}
|
||||||
<a
|
<!-- href={imgurl(record)} -->
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
<a
|
||||||
title={record._file.path}
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
class="d-flex align-items-center justify-content-center "
|
title={file.filename}
|
||||||
style="width:{imageSide}px;height:{imageSide}px"
|
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
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
class="rounded w-100"
|
||||||
|
src={imgurl(channel, file)}
|
||||||
|
alt={file.path}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
|
title={file.path}
|
||||||
|
class="file-preview-small"
|
||||||
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
|
>
|
||||||
|
<Icon icon="file" width={fileSide} height={fileSide} />
|
||||||
|
<span class="ms-2"
|
||||||
|
>.{file.path.split(".").pop().toLowerCase()}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if showFilename}
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
|
title={file.path}
|
||||||
|
class="preview-file-filename lx-small-text text-decoration-none"
|
||||||
|
>{file.path}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{#if showFilename}
|
|
||||||
<a
|
<style>
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
img {
|
||||||
title={record._file.path}
|
border-radius: 12px;
|
||||||
class="preview-file-filename lx-small-text text-decoration-none"
|
padding: 4px;
|
||||||
>{record._file.path}</a
|
}
|
||||||
>
|
</style>
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher, getContext} from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let recordId;
|
||||||
let mimeTypes = "";
|
let mimeTypes = "";
|
||||||
let files = [];
|
let files = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
@@ -17,42 +17,41 @@
|
|||||||
files = e.target.files ? [...e.target.files] : [];
|
files = e.target.files ? [...e.target.files] : [];
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
formData.append("schema", schema.name);
|
formData.append("recordId", recordId);
|
||||||
Array.from(files).forEach(function (file) {
|
Array.from(files).forEach(function (file) {
|
||||||
formData.append("files[]", file);
|
formData.append("files[]", file);
|
||||||
});
|
});
|
||||||
dispatch("beforeUpload", files);
|
dispatch("beforeUpload", files);
|
||||||
axios
|
fetch(channel.lucentUrl + "/files/upload", {
|
||||||
.post(channel.lucentUrl + "/files/upload", formData, {
|
method: "POST",
|
||||||
headers: {
|
body: formData,
|
||||||
"Content-Type": "multipart/form-data",
|
headers: {
|
||||||
},
|
"X-CSRF-TOKEN": document.querySelector(
|
||||||
})
|
'meta[name="csrf-token"]',
|
||||||
.then((response) => {
|
).content,
|
||||||
if (response.data.error) {
|
},
|
||||||
dispatch("uploadError", response.data.error);
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
dispatch("uploadError", data.error);
|
||||||
} else {
|
} else {
|
||||||
dispatch("uploadComplete", response.data);
|
dispatch("uploadComplete", data);
|
||||||
}
|
}
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
isLoading = false;
|
|
||||||
console.log(error.response.data);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset disabled={isLoading}>
|
<fieldset class="upload-button" disabled={isLoading}>
|
||||||
<label class="btn btn-primary btn-sm btn-spinner ">
|
<label class="button primary 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"
|
||||||
>
|
/>
|
||||||
<span class="visually-hidden">Loading...</span>
|
Upload file
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
on:input={upload}
|
on:input={upload}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import {getContext} from "svelte";
|
export function imgurl(channel, file) {
|
||||||
|
if (file.mime === "image/svg+xml") {
|
||||||
|
return fileurl(channel, file);
|
||||||
|
}
|
||||||
|
return channel.filesUrl + `/thumbs/${file.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function imgurl(record) {
|
export function fileurl(channel, file) {
|
||||||
|
return channel.filesUrl + `/${file.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
if(record._file.mime === "image/svg+xml"){
|
export function htmlurl(channel, file, preset) {
|
||||||
return fileurl(record);
|
let html = "";
|
||||||
|
let url = fileurl(channel, file);
|
||||||
|
|
||||||
|
if (file.width > 0) {
|
||||||
|
let presetUrl = url;
|
||||||
|
if (preset) {
|
||||||
|
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
|
||||||
}
|
}
|
||||||
const channel = getContext("channel")
|
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
||||||
return channel.filesUrl + `/thumbs/${record._file.path}`;
|
} else if (file.mime === "image/svg+xml") {
|
||||||
}
|
html = `<img src="${url}" alt="${file.path}"/>`;
|
||||||
|
} else {
|
||||||
|
html = `<a href="${url}">${file.originalName}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
export function fileurl(record) {
|
return html;
|
||||||
const channel = getContext("channel")
|
|
||||||
return channel.filesUrl + `/${record._file.path}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<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,19 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { getContext, onMount } from "svelte";
|
||||||
import {getContext, onMount} from "svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import RecordRow from "./RecordRow.svelte"
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let records = [];
|
let records = [];
|
||||||
let graph = null;
|
let graph = null;
|
||||||
let users = [];
|
let users = [];
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/home/records")
|
||||||
.get(channel.lucentUrl + "/home/records")
|
.then((data) => {
|
||||||
.then((response) => {
|
records = data.records;
|
||||||
records = response.data.records;
|
users = data.users;
|
||||||
graph = response.data.graph;
|
|
||||||
users = response.data.users;
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -21,24 +19,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-normal transparent">
|
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
||||||
|
{#if records.length > 0}
|
||||||
<h3 class="header-small mb-4 ">Latest Content changes</h3>
|
<div class="table">
|
||||||
{#if records.length > 0}
|
<table class="">
|
||||||
<div class="lx-card mb-4">
|
<tbody>
|
||||||
<div class="lx-table p-0">
|
{#each records as record (record.id)}
|
||||||
<table class="">
|
<tr>
|
||||||
<tbody>
|
<RecordRow {record} {users} />
|
||||||
{#each records as record (record.id)}
|
</tr>
|
||||||
<tr>
|
{/each}
|
||||||
<RecordRow {graph} {record} {users}/>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
{/each}
|
</div>
|
||||||
</tbody>
|
{/if}
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,48 +1,38 @@
|
|||||||
<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(
|
let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.updatedAt), {
|
||||||
parseJSON(record._sys.updatedAt),
|
addSuffix: true,
|
||||||
{addSuffix: true}
|
});
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{#if schema.type === "files"}
|
<div class="row-name">
|
||||||
<Preview {record} size="tiny"/>
|
{#if record.status === "draft"}
|
||||||
{:else}
|
<span class="status">DRAFT</span>
|
||||||
<a
|
{/if}
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
{#if schema.type === "files"}
|
||||||
class="text-decoration-none text-dark d-block"
|
<Preview {record} size="tiny" showFilename={true} />
|
||||||
>
|
{:else}
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
<a href="{channel.lucentUrl}/records/{record.id}">
|
||||||
</a>
|
{record.data.name}
|
||||||
{/if}
|
</a>
|
||||||
|
{/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 class="d-flex">
|
<div style="display: flex;gap: 14px">
|
||||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
{frieldlyUpdatedAt}
|
{frieldlyUpdatedAt}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<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,20 +1,42 @@
|
|||||||
<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 {onMount, onDestroy} from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import {basicSetup, EditorView} from "codemirror";
|
import { basicSetup, EditorView } from "codemirror";
|
||||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||||
import {EditorState, Compartment} from "@codemirror/state";
|
import { Compartment, EditorState } from "@codemirror/state";
|
||||||
import {keymap} from "@codemirror/view";
|
import { keymap } from "@codemirror/view";
|
||||||
import {indentWithTab} from "@codemirror/commands";
|
import { indentWithTab } from "@codemirror/commands";
|
||||||
import {markdown} from "@codemirror/lang-markdown";
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
import {lintKeymap} from "@codemirror/lint";
|
import { lintKeymap } from "@codemirror/lint";
|
||||||
|
|
||||||
let parentElement;
|
let parentElement;
|
||||||
let codeMirrorView;
|
let codeMirrorView;
|
||||||
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();
|
||||||
@@ -23,11 +45,7 @@
|
|||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
keymap.of([
|
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
||||||
indentWithTab,
|
|
||||||
...lintKeymap,
|
|
||||||
...completionKeymap
|
|
||||||
]),
|
|
||||||
language.of(markdown()),
|
language.of(markdown()),
|
||||||
markdown(),
|
markdown(),
|
||||||
autocompletion(),
|
autocompletion(),
|
||||||
@@ -40,18 +58,14 @@
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.contentAttributes.of({spellcheck: "true"})
|
EditorView.contentAttributes.of({ spellcheck: "true" }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
codeMirrorView = new EditorView({
|
codeMirrorView = new EditorView({
|
||||||
state,
|
state,
|
||||||
parent: parentElement,
|
parent: parentElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -61,4 +75,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="is-editable-{editable}" bind:this={parentElement}/>
|
<div class="is-editable-{editable}" bind:this={parentElement} />
|
||||||
|
|||||||
@@ -1,59 +1,39 @@
|
|||||||
<script>
|
<script>
|
||||||
import Sortable from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
import { onMount, createEventDispatcher } from "svelte";
|
import {createEventDispatcher, onMount} 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 = null;
|
export let sortableInstance;
|
||||||
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}
|
||||||
|
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {onDestroy, onMount} from "svelte";
|
|
||||||
|
|
||||||
import tinymce from "tinymce/tinymce";
|
|
||||||
import "tinymce/models/dom";
|
|
||||||
import "tinymce/icons/default";
|
|
||||||
import "tinymce/themes/silver";
|
|
||||||
import "tinymce/skins/ui/oxide/skin.css";
|
|
||||||
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css";
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<script>
|
||||||
|
import {onDestroy, onMount} from 'svelte';
|
||||||
|
import {Editor} from '@tiptap/core'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
|
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import Heading from '@tiptap/extension-heading'
|
||||||
|
import HardBreak from '@tiptap/extension-hard-break'
|
||||||
|
import Blockquote from '@tiptap/extension-blockquote';
|
||||||
|
import CodeBlock from '@tiptap/extension-code-block';
|
||||||
|
import Bold from '@tiptap/extension-bold';
|
||||||
|
import BulletList from '@tiptap/extension-bullet-list';
|
||||||
|
import Code from '@tiptap/extension-code';
|
||||||
|
import History from '@tiptap/extension-history';
|
||||||
|
import Italic from '@tiptap/extension-italic';
|
||||||
|
import ListItem from '@tiptap/extension-list-item';
|
||||||
|
import OrderedList from '@tiptap/extension-ordered-list';
|
||||||
|
import Strike from '@tiptap/extension-strike';
|
||||||
|
import Table from '@tiptap/extension-table';
|
||||||
|
import TableRow from '@tiptap/extension-table-row';
|
||||||
|
import TableCell from '@tiptap/extension-table-cell';
|
||||||
|
import TableHeader from '@tiptap/extension-table-header';
|
||||||
|
import Underline from '@tiptap/extension-underline';
|
||||||
|
import Image from '@tiptap/extension-image';
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
let element;
|
||||||
|
let editor;
|
||||||
|
export let value = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editor = new Editor({
|
||||||
|
element: element,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Bold,
|
||||||
|
ListItem,
|
||||||
|
BulletList,
|
||||||
|
Code,
|
||||||
|
CodeBlock,
|
||||||
|
History,
|
||||||
|
Italic,
|
||||||
|
HardBreak,
|
||||||
|
OrderedList,
|
||||||
|
Strike,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
Underline,
|
||||||
|
Dropcursor,
|
||||||
|
Image,
|
||||||
|
Heading.configure({
|
||||||
|
levels: [1, 2, 3],
|
||||||
|
}),
|
||||||
|
Blockquote
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
editable: true,
|
||||||
|
onTransaction: () => {
|
||||||
|
// force re-render so `editor.isActive` works as expected
|
||||||
|
editor = editor;
|
||||||
|
},
|
||||||
|
onUpdate: ({editor}) => {
|
||||||
|
value = editor.getHTML()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function insertMedia(info){
|
||||||
|
editor.chain().focus().setImage({ src: info.url }).run()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if editor}
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
class:active={editor.isActive('heading', { level: 1 })}
|
||||||
|
>
|
||||||
|
H1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
class:active={editor.isActive('heading', { level: 2 })}
|
||||||
|
>
|
||||||
|
H2
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
class:active={editor.isActive('bold')}
|
||||||
|
>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
class:active={editor.isActive('italic')}
|
||||||
|
>
|
||||||
|
<em>IT</em>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
class:active={editor.isActive('underline')}
|
||||||
|
>
|
||||||
|
<u>U</u>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
class:active={editor.isActive('strike')}
|
||||||
|
>
|
||||||
|
<s>S</s>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.commands.unsetAllMarks()}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
class:active={editor.isActive('code')}
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
class:active={editor.isActive('bulletList')}
|
||||||
|
>
|
||||||
|
<Icon icon="list"></Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
class:active={editor.isActive('orderedList')}
|
||||||
|
>
|
||||||
|
<Icon icon="ordered-list"></Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
class:active={editor.isActive('blockquote')}
|
||||||
|
>
|
||||||
|
""
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
class:active={editor.isActive('codeBlock')}
|
||||||
|
>
|
||||||
|
cb
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div bind:this={element} class="content"/>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<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,6 +2,7 @@
|
|||||||
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;
|
||||||
@@ -35,61 +36,50 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 200 }}
|
transition:fly={{ duration: 200 }}
|
||||||
class="d-flex justify-content-between align-items-center mb-3 "
|
class="member-item"
|
||||||
>
|
>
|
||||||
<div class="d-flex align-items-center status-{member.roles.includes('removed') ? 'removed' : 'active'}">
|
<div class="member-name 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>
|
||||||
{member.email}
|
<div>
|
||||||
</div>
|
{member.email}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="dropdown dropdown-center">
|
|
||||||
<button
|
|
||||||
class=" dropdown-toggle btn btn-light"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Roles
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu">
|
|
||||||
<h6 class="dropdown-header">Remove role</h6>
|
|
||||||
{#each roles as role}
|
|
||||||
{#if member.roles.includes(role)}
|
|
||||||
<button
|
|
||||||
class="dropdown-item text-capitalize"
|
|
||||||
on:click={(e) => removeFrom(e,role)}
|
|
||||||
>
|
|
||||||
{role}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
<div>
|
|
||||||
<hr class="dropdown-divider">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h6 class="dropdown-header">Add role</h6>
|
|
||||||
{#each roles as role}
|
|
||||||
{#if !member.roles.includes(role)}
|
|
||||||
<button
|
|
||||||
class="dropdown-item text-capitalize"
|
|
||||||
on:click={(e) => addTo(e,role)}
|
|
||||||
>
|
|
||||||
{role}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<Dropdown orientation="right">
|
||||||
|
<div slot="button">
|
||||||
|
Roles
|
||||||
|
</div>
|
||||||
|
<h6 class="dropdown-header">Remove role</h6>
|
||||||
|
{#each roles as role}
|
||||||
|
{#if member.roles.includes(role)}
|
||||||
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => removeFrom(e,role)}
|
||||||
|
>
|
||||||
|
{role}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
|
||||||
|
<h6 class="dropdown-header">Add role</h6>
|
||||||
|
{#each roles as role}
|
||||||
|
{#if !member.roles.includes(role)}
|
||||||
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => addTo(e,role)}
|
||||||
|
>
|
||||||
|
{role}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.status-removed {
|
.status-removed {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
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;
|
||||||
@@ -23,15 +22,14 @@
|
|||||||
function invite(newName, newEmail, newRole) {
|
function invite(newName, newEmail, newRole) {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/invite", {
|
||||||
.post(channel.lucentUrl + "/members/invite", {
|
name: newName,
|
||||||
name: newName,
|
email: newEmail,
|
||||||
email: newEmail,
|
roles: [newRole],
|
||||||
roles: [newRole],
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("User was invited");
|
successAlert.show("User was invited");
|
||||||
users = [...users, response.data.user];
|
users = [...users, response.user];
|
||||||
name = null;
|
name = null;
|
||||||
email = null;
|
email = null;
|
||||||
role = null;
|
role = null;
|
||||||
@@ -45,14 +43,13 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/update", {
|
||||||
.post(channel.lucentUrl + "/members/update", {
|
id: e.detail.user,
|
||||||
id: e.detail.user,
|
roles: e.detail.roles,
|
||||||
roles: e.detail.roles,
|
})
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("Users updated");
|
successAlert.show("Users updated");
|
||||||
users = response.data.users;
|
users = response.users;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data?.error ?? "";
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
@@ -60,17 +57,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny transparent mb-5">
|
<div class="common-wrapper">
|
||||||
<div class="lx-card mt-5">
|
<div class="lx-card mt-5">
|
||||||
<h3 class="header-small mb-5">Invite people</h3>
|
<h3 class="header-small mb-5">Invite people</h3>
|
||||||
<ErrorAlert message={errorMessage}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
<SuccessAlert bind:this={successAlert}/>
|
<SuccessAlert bind:this={successAlert} />
|
||||||
|
|
||||||
<form on:submit={submitInvite}>
|
<form on:submit={submitInvite}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="inviteeName" class="form-label"
|
<label for="inviteeName" class="form-label">Invitee Name</label>
|
||||||
>Invitee Name</label
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
@@ -82,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="inviteeEmail" class="form-label"
|
<label for="inviteeEmail" class="form-label"
|
||||||
>Invitee Email Address</label
|
>Invitee Email Address</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
@@ -95,24 +90,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
<select bind:value={role}>
|
||||||
<Radio
|
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||||
bind:group={role}
|
<option value={arole}>{arole}</option>
|
||||||
value={arole}
|
{/each}
|
||||||
name="role"
|
</select>
|
||||||
label={arole}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</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="lx-card mt-3">
|
<div class="member-list">
|
||||||
<h3 class="header-small mb-5">Members</h3>
|
<h3 class="header-small mb-5 mt-5">Members</h3>
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
<MemberSettingsCard
|
<MemberSettingsCard
|
||||||
member={user}
|
member={user}
|
||||||
@@ -121,7 +113,5 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<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}-->
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<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}
|
|
||||||
@@ -1,60 +1,123 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import { afterUpdate, getContext, onMount } from "svelte";
|
||||||
import Manager from "./Manager.svelte";
|
import EditHeader from "./header/EditHeader.svelte";
|
||||||
import FilePreview from "./FilePreview.svelte"
|
import ContentTabs from "./header/ContentTabs.svelte";
|
||||||
import Form from "./form/Form.svelte";
|
import FormField from "./FormField.svelte";
|
||||||
import axios from "axios";
|
import Graph from "./Graph.svelte";
|
||||||
|
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 = {
|
||||||
export let recordHistory;
|
records: [],
|
||||||
|
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 = null;
|
$: errorMessage = validationErrors
|
||||||
|
? `Record submission failed. ${
|
||||||
|
Object.entries(validationErrors).length
|
||||||
|
} error(s)`
|
||||||
|
: null;
|
||||||
|
|
||||||
let form;
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
|
|
||||||
|
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
|
||||||
let replaceEdges = graph
|
graph.edges = graph.edges?.filter(
|
||||||
.map((queryRecord) => queryRecord.edge)
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
);
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
axios
|
record: record,
|
||||||
.post(channel.lucentUrl + "/records", {
|
edges: graph.edges,
|
||||||
schemaName: record.schema,
|
isCreateMode: isCreateMode,
|
||||||
updateEdges: true,
|
})
|
||||||
id: record.id,
|
|
||||||
data: record.data,
|
|
||||||
edges: replaceEdges,
|
|
||||||
status: status,
|
|
||||||
isCreateMode: isCreateMode,
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED");
|
console.log("SAVE: SAVED");
|
||||||
|
|
||||||
if (isCreateMode) {
|
if (isCreateMode) {
|
||||||
window.location.href = channel.lucentUrl + "/records/" + record.id;
|
window.location =
|
||||||
|
channel.lucentUrl + "/records/" + record.id;
|
||||||
} else {
|
} else {
|
||||||
record = response.data.record ?? null;
|
record = response.records[0] ?? null;
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// means trashed
|
// means trashed
|
||||||
|
hasUnsavedData = false;
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
graph = [...response.data.graph];
|
graph = response;
|
||||||
form.setOriginalData();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -66,36 +129,72 @@
|
|||||||
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="wrapper-normal transparent">
|
<div class="record-edit">
|
||||||
<Manager managerRecords={recordHistory} {graph}/>
|
<div class="tools-header">
|
||||||
|
<!-- <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} />
|
||||||
|
|
||||||
<FilePreview {record} {schema}/>
|
<ErrorAlert message={errorMessage} />
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<Form
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
bind:this={form}
|
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||||
data={record.data}
|
{#each activeFields as field (field.name)}
|
||||||
status={record.status}
|
{#if activeContentTab === field.group}
|
||||||
bind:graph
|
<FormField
|
||||||
{schema}
|
bind:data={record.data}
|
||||||
{record}
|
bind:graph
|
||||||
{isCreateMode}
|
{field}
|
||||||
{errorMessage}
|
{schema}
|
||||||
{validationErrors}
|
{record}
|
||||||
on:save={save}
|
{validationErrors}
|
||||||
/>
|
{isCreateMode}
|
||||||
<!-- <Graph {graph} {record}/>-->
|
/>
|
||||||
<!-- <Info {record} {graph} {users} {schema}/>-->
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{:else if activeContentTab === "_graph"}
|
||||||
|
<Graph {graph} {record} />
|
||||||
|
{:else if activeContentTab === "_info"}
|
||||||
|
<Info {record} {graph} {users} {schema} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
<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,55 +1,53 @@
|
|||||||
<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="row mb-4">
|
<div class="record-edit-file-preview">
|
||||||
<div class="col" style="max-width:276px">
|
<div>
|
||||||
<Preview {record} size="large"/>
|
<Preview {record} size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="file-details">
|
||||||
<ul class="list-group ">
|
<div class="file-details-item">
|
||||||
<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,23 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import Text from "./form/fields/Text.svelte";
|
import Text from "./elements/Text.svelte";
|
||||||
import Slug from "./form/fields/Slug.svelte";
|
import Slug from "./elements/Slug.svelte";
|
||||||
import Reference from "./elements/Reference.svelte";
|
import Reference from "./elements/Reference.svelte";
|
||||||
import ReferenceInline from "./elements/ReferenceInline.svelte";
|
import Color from "./elements/Color.svelte";
|
||||||
import Block from "./block/Block.svelte";
|
import Checkbox from "./elements/Checkbox.svelte";
|
||||||
import Color from "./form/fields/Color.svelte";
|
import Number from "./elements/Number.svelte";
|
||||||
import Checkbox from "./form/fields/Checkbox.svelte";
|
import Url from "./elements/Url.svelte";
|
||||||
import Number from "./form/fields/Number.svelte";
|
import Date from "./elements/Date.svelte";
|
||||||
import Date from "./form/fields/Date.svelte";
|
import UUID from "./elements/UUID.svelte";
|
||||||
import UUID from "./form/fields/UUID.svelte";
|
import File from "./elements/File.svelte";
|
||||||
import File from "./form/references/File.svelte";
|
import Textarea from "./elements/Textarea.svelte";
|
||||||
import Textarea from "./form/fields/Textarea.svelte";
|
import Datetime from "./elements/Datetime.svelte";
|
||||||
import Datetime from "./form/fields/Datetime.svelte";
|
import RichEditor from "./elements/RichEditor.svelte";
|
||||||
import RichEditor from "./form/fields/RichEditor.svelte";
|
import Json from "./elements/JSON.svelte";
|
||||||
import Json from "./form/fields/JSON.svelte";
|
import Markdown from "./elements/Markdown.svelte";
|
||||||
import Markdown from "./form/fields/Markdown.svelte";
|
import FieldHeader from "./elements/FieldHeader.svelte";
|
||||||
import FieldHeader from "./form/FieldHeader.svelte";
|
import ReferenceTags from "./elements/ReferenceTags.svelte";
|
||||||
import ReferenceTable from "./elements/ReferenceTable.svelte";
|
|
||||||
import ReferenceTags from "./form/references/ReferenceTags.svelte";
|
|
||||||
|
|
||||||
const formElements = {
|
const formElements = {
|
||||||
text: Text,
|
text: Text,
|
||||||
@@ -27,6 +25,7 @@
|
|||||||
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,
|
||||||
@@ -45,36 +44,15 @@
|
|||||||
const id = `field-${field.name}-${record.id}`;
|
const id = `field-${field.name}-${record.id}`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card editor-field">
|
<div class="editor-field">
|
||||||
<FieldHeader {field} {id}/>
|
<FieldHeader {field} {id} />
|
||||||
{#if field.info.name === "reference" && field.layout === "inline"}
|
{#if field.info.name === "reference" && field.layout === "tags"}
|
||||||
<ReferenceInline
|
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
||||||
bind:graph
|
{:else if field.info.name === "reference"}
|
||||||
{record}
|
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
||||||
{field}
|
{:else if field.info.name === "file"}
|
||||||
{validationErrors}
|
<!-- <File bind:graph {record} {field} {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}
|
||||||
@@ -91,11 +69,11 @@
|
|||||||
/>
|
/>
|
||||||
{:else if field.info.name === "slug"}
|
{:else if field.info.name === "slug"}
|
||||||
<Slug
|
<Slug
|
||||||
bind:value={data[field.name]}
|
bind:value={data[field.name]}
|
||||||
{field}
|
{field}
|
||||||
{id}
|
{id}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
{isCreateMode}
|
{isCreateMode}
|
||||||
/>
|
/>
|
||||||
{:else if field.info.name === "textarea"}
|
{:else if field.info.name === "textarea"}
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -105,6 +83,26 @@
|
|||||||
{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,15 +1,9 @@
|
|||||||
<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 {uniqBy} from "lodash";
|
import PreviewReference from "./previews/PreviewReference.svelte";
|
||||||
|
|
||||||
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(":");
|
||||||
@@ -18,121 +12,35 @@
|
|||||||
return schema.fields.find((f) => f.name === edgeField);
|
return schema.fields.find((f) => f.name === edgeField);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentEdgesByField = graph.parentEdges
|
let backlinks = graph.parentEdges.map(edge => {
|
||||||
.filter((edge) => edge.source !== record.id && edge.depth === 1)
|
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||||
.reduce((carry, edge) => {
|
let edgeField = findEdgeField(schema,edge.field);
|
||||||
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
if(!edgeField){
|
||||||
let edgeField = findEdgeField(schema,edge.field);
|
return null;
|
||||||
let schemaField = edge.sourceSchema + edgeField;
|
}
|
||||||
|
return {
|
||||||
let arecord = graph.records.find((n) => {
|
field: edgeField.label,
|
||||||
return n.id === edge.source;
|
record: graph.records.find( record => record.id === edge.source)
|
||||||
});
|
}
|
||||||
if (!carry[schemaField]) {
|
}).filter( edgeOrNull => !!edgeOrNull)
|
||||||
carry[schemaField] = {
|
|
||||||
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>
|
</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;
|
||||||
|
|
||||||
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
|
"
|
||||||
<div class="lx-card mt-3">
|
>In <i>{backlink.field}</i> of</span>
|
||||||
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
|
<PreviewReference
|
||||||
<span>{fieldData.schema.label}</span>
|
record={backlink.record}
|
||||||
<Icon icon="angle-right" width="12" height="12"/>
|
hasDelete={false}
|
||||||
<span>{fieldData.field.label}</span>
|
{graph}
|
||||||
</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>
|
</div>
|
||||||
|
{:else}
|
||||||
|
Nothing links to this record
|
||||||
{/each}
|
{/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>
|
</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>
|
|
||||||
{:else}
|
|
||||||
<div class="ms-2 mb-2">
|
|
||||||
<PreviewCardSmall {graph} record={node}/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import {friendlyDate} from "../../helpers";
|
import { friendlyDate, isEqual } from "../../helpers";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {usernameById} from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import {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
|
||||||
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
|
.filter((e) => e.depth === 1)
|
||||||
if (!c[e.field]) {
|
.reduce((c, e) => {
|
||||||
c[e.field] = {
|
if (!c[e.field]) {
|
||||||
record: [],
|
c[e.field] = {
|
||||||
revision: [],
|
record: [],
|
||||||
|
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._sys.version}`
|
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.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,29 +93,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">current version </span>
|
<span class="label text-end text-muted">current version </span>
|
||||||
{record._sys.version}
|
{record.version}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted"> created </span>
|
<span class="label text-end text-muted"> created </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.createdBy)}
|
name={usernameById(users, record.createdBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.createdAt)}
|
{friendlyDate(record.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">updated </span>
|
<span class="label text-end text-muted">updated </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.updatedBy)}
|
name={usernameById(users, record.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.updatedAt)}
|
{friendlyDate(record.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="label d-block text-muted "
|
<span class="label d-block text-muted">Rules for this schema </span>
|
||||||
>Rules for this schema
|
|
||||||
</span>
|
|
||||||
<small>
|
<small>
|
||||||
Each record maintains the last {schema.revisions}
|
Each record maintains the last {schema.revisions}
|
||||||
versions
|
versions
|
||||||
@@ -123,35 +121,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lx-card mt-4">
|
<div class="revisions">
|
||||||
{#if schema.revisions > 0}
|
{#if schema.revisions > 0}
|
||||||
<div class="header-small mb-3">Revisions</div>
|
<div class="header-small mb-3">Revisions</div>
|
||||||
{#each revisions as revision}
|
{#each revisions as revision}
|
||||||
{#if revision._sys.version != record._sys.version}
|
{#if revision.version !== record.version}
|
||||||
<div
|
<div
|
||||||
class="row p-2 rounded"
|
class="revision"
|
||||||
class:active={revision._sys.version ===
|
class:active={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
>
|
>
|
||||||
|
<div class="version">
|
||||||
<div class="col-2">version {revision._sys.version}</div>
|
<span>version {revision.version}</span>
|
||||||
<div class="col-5">
|
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, revision._sys.updatedBy)}
|
name={usernameById(users, revision.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(revision._sys.updatedAt)}
|
{friendlyDate(revision.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
<button
|
<button
|
||||||
disabled={revision._sys.version ===
|
disabled={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
class="btn btn-sm btn-outline-primary"
|
class="button"
|
||||||
on:click={(e) => compare(e, revision)}
|
on:click={(e) => compare(e, revision)}
|
||||||
>Compare
|
>Compare
|
||||||
</button
|
</button>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -164,20 +160,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div bind:this={revisionSection}>
|
<div bind:this={revisionSection}>
|
||||||
{#if selectedRevision}
|
{#if selectedRevision}
|
||||||
<div class="mt-4">
|
<div class="selected-revision">
|
||||||
{#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
|
<button on:click={rollback} class="button">
|
||||||
on:click={rollback}
|
Rollback to version {selectedRevision.version}
|
||||||
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}
|
||||||
@@ -188,35 +182,28 @@
|
|||||||
<!-- <div class="d-block" style="width:200px;">
|
<!-- <div class="d-block" style="width:200px;">
|
||||||
{field.label}
|
{field.label}
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div class="revision-field" style="overflow:hidden">
|
||||||
class="lx-card row p-4 mb-4 w-100"
|
<div class="compare-left">
|
||||||
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="col-2">
|
<div class="compare-center">
|
||||||
<div
|
<span class="me-1">{field.label}</span>
|
||||||
class="h-100 d-flex align-items-center justify-content-center text-secondary"
|
<Icon
|
||||||
>
|
icon="angle-right"
|
||||||
<span class="me-1">{field.label}</span>
|
width="12"
|
||||||
<Icon
|
height="12"
|
||||||
icon="angle-right"
|
/>
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-5">
|
<div class="compare-right">
|
||||||
<RevisionCell
|
<RevisionCell
|
||||||
edges={selectedRevision._edges}
|
edges={selectedRevision._edges}
|
||||||
{field}
|
{field}
|
||||||
side={selectedRevision.data[field.name]}
|
side={selectedRevision.data[field.name]}
|
||||||
colorClass="text-success"
|
colorClass="text-success"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,25 +217,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<p class="text-center fw-bold mb-3 mt-5">
|
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
|
||||||
Record References
|
|
||||||
</p>
|
|
||||||
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
||||||
<div
|
<div class="revision-references" style="overflow:hidden">
|
||||||
class="lx-card row p-4 mb-4 w-100"
|
<div class="reference-field">
|
||||||
style="overflow:hidden"
|
|
||||||
>
|
|
||||||
<div class="col-4">
|
|
||||||
{field}:
|
{field}:
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="reference-compare">
|
||||||
<p class="mb-2 text-danger">Record</p>
|
<p class="">Record</p>
|
||||||
{#each edges.record as edge}
|
{#each edges.record as edge}
|
||||||
<RevisionEdgeRow {edge} />
|
<RevisionEdgeRow {edge} />
|
||||||
{:else}
|
{:else}
|
||||||
<p>No references</p>
|
<p>No references</p>
|
||||||
{/each}
|
{/each}
|
||||||
<p class="mt-4 mb-2 text-success">Revision</p>
|
</div>
|
||||||
|
<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}
|
||||||
@@ -259,21 +243,5 @@
|
|||||||
{/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,12 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
|
import {
|
||||||
|
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 "./ContentTabs.svelte";
|
import ContentTabs from "./header/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();
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
edges: []
|
edges: [],
|
||||||
};
|
};
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
let originalContent;
|
let originalContent;
|
||||||
@@ -23,13 +29,11 @@
|
|||||||
$: validationErrors = null;
|
$: validationErrors = null;
|
||||||
$: errorMessage = validationErrors
|
$: errorMessage = validationErrors
|
||||||
? `Record submission failed. ${
|
? `Record submission failed. ${
|
||||||
Object.entries(validationErrors).length
|
Object.entries(validationErrors).length
|
||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
(f) => f.name !== "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
let tabname = "_default";
|
let tabname = "_default";
|
||||||
let fieldToTabs = schema.fields.reduce((c, f) => {
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
@@ -51,8 +55,6 @@
|
|||||||
data: JSON.parse(JSON.stringify(record.data)),
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
schema: record.schema,
|
schema: record.schema,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
|
||||||
_file: JSON.parse(JSON.stringify(record._file)),
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -85,8 +87,6 @@
|
|||||||
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,20 +112,21 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
|
graph.edges =
|
||||||
|
graph.edges?.filter(
|
||||||
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
axios
|
record: record,
|
||||||
.post(channel.lucentUrl + "/records", {
|
edges: graph.edges,
|
||||||
record: record,
|
isCreateMode: isCreateMode,
|
||||||
edges: graph.edges,
|
})
|
||||||
isCreateMode: isCreateMode,
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED INLINE");
|
console.log("SAVE: SAVED INLINE");
|
||||||
|
|
||||||
record = response.data.records[0];
|
record = response.records[0];
|
||||||
graph = response.data;
|
graph = response;
|
||||||
if (!isCreateMode) {
|
if (!isCreateMode) {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,6 @@
|
|||||||
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,85 +144,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 my-4">
|
<div class="inline-edit record-edit">
|
||||||
<ErrorAlert message={errorMessage}/>
|
<div class="tools-header">
|
||||||
|
<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-1">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<ContentTabs
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
{schema}
|
<FilePreview {record} {schema} />
|
||||||
{isCreateMode}
|
|
||||||
bind:active={activeContentTab}
|
|
||||||
{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={graph}
|
bind:graph
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
{isCreateMode}
|
{isCreateMode}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/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>
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<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,23 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {previewTitle} from "./Preview";
|
import { getContext } from "svelte";
|
||||||
import {getContext} from "svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
$: title = record.data.name;
|
||||||
|
|
||||||
$: title = previewTitle( record);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if record?.data}
|
{#if record?.data}
|
||||||
<a
|
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
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}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<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}/>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
|
|
||||||
|
|
||||||
|
|
||||||
export let block;
|
|
||||||
// export let id;
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
|
|
||||||
<Codemirror bind:value={block.value} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<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}
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Tinymce from "../../../libs/Tinymce.svelte";
|
|
||||||
|
|
||||||
export let block;
|
|
||||||
let additionalConfig = {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mb-0">
|
|
||||||
<Tinymce bind:value={block.value} {additionalConfig}/>
|
|
||||||
</div>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {onMount} from "svelte";
|
|
||||||
|
|
||||||
export let block;
|
|
||||||
let thisEl;
|
|
||||||
|
|
||||||
function resize(e) {
|
|
||||||
let el;
|
|
||||||
if (e.target) {
|
|
||||||
el = e.target;
|
|
||||||
} else {
|
|
||||||
el = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.style.overflow = "hidden";
|
|
||||||
el.style.height = "1px";
|
|
||||||
el.style.height = +el.scrollHeight + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
resize(thisEl);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mb-0">
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
bind:value={block.value}
|
|
||||||
bind:this={thisEl}
|
|
||||||
on:input={resize}
|
|
||||||
id={block.id}
|
|
||||||
class="form-control"
|
|
||||||
autocomplete="off"></textarea>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
textarea {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user