Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f74c850e01 | |||
| 9d6d39fe62 | |||
| a9851847fc | |||
| d1c896acf4 | |||
| f99aadee83 | |||
| 8cd80c016f | |||
| ef29e4d261 | |||
| 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
|
||||
/.idea
|
||||
/.vscode
|
||||
/.claude
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "/phpactor.schema.json",
|
||||
"language_server_phpstan.enabled": false
|
||||
}
|
||||
@@ -9,8 +9,8 @@ include_toc: true
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.2
|
||||
- Laravel 10
|
||||
- PHP 8.3
|
||||
- Laravel 11
|
||||
- Postgres or Sqlite database
|
||||
- ImageMagick
|
||||
|
||||
@@ -82,7 +82,9 @@ return [
|
||||
### Database
|
||||
|
||||
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:
|
||||
|
||||
@@ -90,6 +92,26 @@ Then run:
|
||||
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
|
||||
|
||||
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
-39
@@ -1,42 +1,39 @@
|
||||
{
|
||||
"name": "lexx27/lucent",
|
||||
"type": "library",
|
||||
"description": "Lucent cms",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"ext-xml": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"ext-imagick": "*",
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"intervention/image": "^2.7",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.6",
|
||||
"staudenmeir/laravel-cte": "^1.0",
|
||||
"ext-pdo": "*"
|
||||
"name": "lexx27/lucent",
|
||||
"type": "library",
|
||||
"description": "Lucent cms",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"ext-xml": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-pdo": "*",
|
||||
"php": "^8.4",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.8",
|
||||
"staudenmeir/laravel-cte": "^1.0",
|
||||
"intervention/image": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"laravel/framework": "^10.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lucent\\": "src/"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lucent\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/Response.php",
|
||||
"src/macros.php",
|
||||
"src/File/Uploader.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Lucent\\LucentServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
|
||||
"files": [
|
||||
"src/Response.php",
|
||||
"src/macros.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Lucent\\LucentServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
Generated
+3903
-993
File diff suppressed because it is too large
Load Diff
+145
-298
@@ -5,374 +5,221 @@ include_toc: true
|
||||
|
||||
# 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
|
||||
One-line text input
|
||||
|
||||
required
|
||||
One-line text input.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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 input
|
||||
|
||||
required
|
||||
Multi-line text input.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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 input. Generates automatically if left empty
|
||||
|
||||
required
|
||||
Slug input. Auto-generates from a source field if left empty.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
- **source**: The source field from which it generates
|
||||
**Required:** `name`, `label`, `source`
|
||||
|
||||
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
|
||||
WYSIWYG editor
|
||||
|
||||
required
|
||||
WYSIWYG rich text editor.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
### markdown
|
||||
|
||||
Markdown editor.
|
||||
|
||||
**Required:** `name`, `label`
|
||||
|
||||
| Property | Description |
|
||||
|---|---|
|
||||
| `min` | Minimum character count |
|
||||
| `max` | Maximum character count |
|
||||
|
||||
---
|
||||
|
||||
### number
|
||||
Any numeric value
|
||||
|
||||
required
|
||||
Numeric input.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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
|
||||
True or false
|
||||
|
||||
required
|
||||
Boolean true/false toggle.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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 picker
|
||||
|
||||
required
|
||||
Color picker.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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 select
|
||||
|
||||
required
|
||||
Date selector. Stores as a date string.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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
|
||||
Date and time selector
|
||||
|
||||
required
|
||||
Date and time selector. Stores as an ISO 8601 string.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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
|
||||
- **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
|
||||
---
|
||||
|
||||
### uuid
|
||||
|
||||
UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID.
|
||||
|
||||
**Required:** `name`, `label`
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
- **label**: The friendly name of the field
|
||||
**Required:** `name`, `label`
|
||||
|
||||
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
|
||||
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
|
||||
- **label**: The friendly name of the field
|
||||
- **collections**: File collections to choose from
|
||||
**Required:** `name`, `label`, `collections`
|
||||
|
||||
optional
|
||||
|
||||
- **mime**: The mime types allowed to select
|
||||
- **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
|
||||
| Property | Description |
|
||||
|---|---|
|
||||
| `collections` | Array of file schema names to choose from |
|
||||
| `mime` | Allowed MIME types e.g. `["image/*"]` |
|
||||
| `min` | Minimum number of files |
|
||||
| `max` | Maximum number of files |
|
||||
|
||||
---
|
||||
|
||||
### reference
|
||||
Reference other records
|
||||
|
||||
required
|
||||
Reference records from another collection.
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
- **collections**: Collections to choose from
|
||||
**Required:** `name`, `label`, `collections`
|
||||
|
||||
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
|
||||
The block editor
|
||||
|
||||
required
|
||||
|
||||
- **name**: The id of the field
|
||||
- **label**: The friendly name of the field
|
||||
- **schema**: The block schema name
|
||||
|
||||
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
|
||||
```json
|
||||
{
|
||||
"ui": "text",
|
||||
"name": "title",
|
||||
"label": "Title",
|
||||
"required": true,
|
||||
"min": 3,
|
||||
"max": 200,
|
||||
"help": "The main title of the post"
|
||||
}
|
||||
```
|
||||
|
||||
+133
-52
@@ -5,88 +5,169 @@ include_toc: true
|
||||
|
||||
# Queries
|
||||
|
||||
## Graph or Tree
|
||||
|
||||
Queries can return results in 2 formats. A graph or a tree.
|
||||
|
||||
Graphs results are a collection of records (nodes) and a collection of edges. This format is more useful for network visualization.
|
||||
|
||||
The tree format is more straightforward as it returns a collection of records. Each record has a **_children** and a **_parents** field and the tree can continue upwards or downwards according to the depth you have requested.
|
||||
|
||||
For example to request records with their children and their children's children:
|
||||
The `Query` class is the main way to fetch records. Inject it via the Laravel container.
|
||||
|
||||
```php
|
||||
$query->childrenDepth(2);
|
||||
public function __construct(private Query $query) {}
|
||||
```
|
||||
Maybe you only want to get a specific type of relationship:
|
||||
|
||||
```php
|
||||
$query->childrenDepth(2)->childrenFields(["categories"]);
|
||||
```
|
||||
## Return Formats
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
$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
|
||||
$query
|
||||
->filter(["price_eqn" => 10])
|
||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"])
|
||||
;
|
||||
->filter(["schema" => "blogPosts"])
|
||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"]);
|
||||
```
|
||||
## Operator List
|
||||
|
||||
- regex
|
||||
- eq
|
||||
- ne
|
||||
- eqnum
|
||||
- neqnum
|
||||
- filter
|
||||
- eqtrue
|
||||
- eqfalse
|
||||
- netrue
|
||||
- nefalse
|
||||
- in:
|
||||
- innum
|
||||
- nin
|
||||
- ninnum
|
||||
- lt
|
||||
- lte
|
||||
- gt
|
||||
- gte
|
||||
- null
|
||||
- nnull
|
||||
- exists
|
||||
- nexists
|
||||
Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together.
|
||||
|
||||
## 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
|
||||
$posts = $query
|
||||
->filter([
|
||||
"schema" => "posts",
|
||||
"schema" => "blogPosts",
|
||||
"children.categories.data.slug" => "sports",
|
||||
]
|
||||
)
|
||||
])
|
||||
->childrenDepth(1)
|
||||
->childrenFields(["categories"])
|
||||
->limit(10)
|
||||
->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();
|
||||
```
|
||||
|
||||
+96
-63
@@ -5,86 +5,119 @@ include_toc: true
|
||||
|
||||
# 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
|
||||
- Files: Images and files
|
||||
- Block: Used in the block editor
|
||||
- **collection** — Regular data records
|
||||
- **files** — Images and file uploads
|
||||
|
||||
|
||||
## Collection Reference
|
||||
|
||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||
- **label**: The friendly name of the schema
|
||||
- **type**: The type of the collection. Should be "collection"
|
||||
- **visible**: An array of field id to show on the content browser _optional_
|
||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
|
||||
- **sortBy**: The default sorting in the content browser _optional_
|
||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
||||
- **read**: Array of user groups that have read permissions _optional_
|
||||
- **write**: Array of user groups that have write permissions _optional_
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` |
|
||||
| `label` | yes | Friendly display name |
|
||||
| `type` | yes | Must be `"collection"` |
|
||||
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||
| `visible` | no | Field IDs to show in the content browser list |
|
||||
| `groups` | no | Group IDs to split fields into tabs |
|
||||
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||
| `sortBy` | no | Default sort in browser. Prefix with `-` for descending e.g. `-_sys.updatedAt` |
|
||||
| `cardTitle` | no | Mustache template for the record preview title e.g. `{{name}} - {{slug}}` |
|
||||
| `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
|
||||
|
||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||
- **label**: The friendly name of the schema
|
||||
- **type**: The type of the collection. Should be "files"
|
||||
- **path**: The relative directory that these files will be stored.
|
||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
||||
- **sortBy**: The default sorting in the content browser _optional_
|
||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
||||
- **read**: Array of user groups that have read permissions _optional_
|
||||
- **write**: Array of user groups that have write permissions _optional_
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` |
|
||||
| `label` | yes | Friendly display name |
|
||||
| `type` | yes | Must be `"files"` |
|
||||
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||
| `groups` | no | Group IDs to split fields into tabs |
|
||||
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||
| `sortBy` | no | Default sort in browser |
|
||||
| `cardTitle` | no | Mustache template for the record preview title |
|
||||
| `cardImage` | no | Field name to use as the preview image |
|
||||
| `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
|
||||
- **label**: The friendly name of the schema
|
||||
- **type**: The type of the collection. Should be "block"
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
Every record automatically has these read-only system fields available in queries:
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `_sys.createdAt` | ISO 8601 creation timestamp |
|
||||
| `_sys.updatedAt` | ISO 8601 last update timestamp |
|
||||
| `_sys.createdBy` | User ID who created the record |
|
||||
| `_sys.updatedBy` | User ID who last updated the record |
|
||||
| `_sys.version` | Revision version number |
|
||||
| `status` | `draft` or `published` |
|
||||
|
||||
|
||||
A full Collection example without the fields:
|
||||
## Example
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Departments",
|
||||
"name": "departments",
|
||||
"isEntry": true,
|
||||
"type": "collection",
|
||||
"visible": [
|
||||
"slug",
|
||||
"cover",
|
||||
"_sys.updatedAt",
|
||||
"status"
|
||||
"schemas": [
|
||||
{
|
||||
"label": "Blog Posts",
|
||||
"name": "blogPosts",
|
||||
"isEntry": true,
|
||||
"type": "collection",
|
||||
"visible": [
|
||||
"title",
|
||||
"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": [
|
||||
"Extra Info",
|
||||
"Metadata",
|
||||
"SEO"
|
||||
],
|
||||
"sortBy": "-_sys.createdAt",
|
||||
"titleTemplate": "{{name}} {{slug}}",
|
||||
"revisions": 15,
|
||||
"read": [
|
||||
"admin",
|
||||
"editors",
|
||||
"reviewers"
|
||||
],
|
||||
"write": [
|
||||
"admin",
|
||||
"editors"
|
||||
],
|
||||
"fields": []
|
||||
"roles": ["admin", "editors", "reviewers"]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
+84
-25
@@ -5,51 +5,94 @@ To generate the static content of the website, lucent provides a helper class.
|
||||
|
||||
## 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
|
||||
public function __construct(
|
||||
public StaticGenerator $staticGenerator,
|
||||
) {
|
||||
parent::__construct();
|
||||
class GenerateStatic extends Command
|
||||
{
|
||||
protected $signature = 'generate:static';
|
||||
|
||||
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
|
||||
$this->staticGenerator->run(function ($writer) {
|
||||
$writer->createRedirect("/", "/el");
|
||||
});
|
||||
$writer->save("/blog/my-post", $html);
|
||||
// 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
|
||||
$this->staticGenerator->run(function ($writer) {
|
||||
$writer->save("/", $this->ctx->render("homepage"));
|
||||
$writer->save("/about", $this->ctx->render("about"));
|
||||
});
|
||||
$writer->createRedirect("/", "/el");
|
||||
$writer->createRedirect("/", "/el", "Redirecting", "Please wait...");
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
All the generated html is stored on `storage/lucent/live`
|
||||
In order to make it accessible you have to create symlink:
|
||||
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:
|
||||
|
||||
```bash
|
||||
php artisan lucent:livelink
|
||||
```
|
||||
|
||||
Now your static website is accessible in http://localhost:8000/live
|
||||
|
||||
But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost
|
||||
The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost:
|
||||
|
||||
```nginxconf
|
||||
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
|
||||
|
||||
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
|
||||
"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.
|
||||
|
||||
|
||||
|
||||
|
||||
+111
-1
@@ -98,4 +98,114 @@ 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
+213
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": {
|
||||
"file": "assets/main.7c3e8b7b.js",
|
||||
"file": "assets/main-DtbuHUXl.js",
|
||||
"name": "main",
|
||||
"src": "main.js",
|
||||
"isEntry": true,
|
||||
"css": [
|
||||
"assets/main.587d6006.css"
|
||||
"assets/main-BadhVKbO.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) {
|
||||
return formatDistanceToNow(parseJSON(date), {addSuffix: true});
|
||||
return formatDistanceToNow(parseJSON(date), { addSuffix: true });
|
||||
}
|
||||
|
||||
export function readableDate(date) {
|
||||
if(!date){
|
||||
return "";
|
||||
}
|
||||
return format(parseJSON(date), "dd MMM yyyy");
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
return format(parseJSON(date), "dd MMM yyyy");
|
||||
}
|
||||
|
||||
export function readableDatetime(date) {
|
||||
if(!date){
|
||||
return "";
|
||||
}
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||
}
|
||||
|
||||
|
||||
export function stripHtml(html = "") {
|
||||
let tmp = document.createElement("div");
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || "";
|
||||
let tmp = document.createElement("div");
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || "";
|
||||
}
|
||||
|
||||
|
||||
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 Account from "./svelte/Account.svelte";
|
||||
import Channel from "./svelte/Channel.svelte";
|
||||
import * as bootstrap from "bootstrap";
|
||||
import Mustache from "mustache";
|
||||
// import Mustache from "mustache";
|
||||
|
||||
Mustache.escape = function (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);
|
||||
});
|
||||
}
|
||||
// Mustache.escape = function (value) {
|
||||
// return value;
|
||||
// };
|
||||
|
||||
// Define all components
|
||||
const entryComponents = {
|
||||
account: Account,
|
||||
channel: Channel,
|
||||
account: Account,
|
||||
channel: Channel,
|
||||
};
|
||||
|
||||
let loadedComponents = [];
|
||||
|
||||
let loadSvelte = function () {
|
||||
loadedComponents.map((comp) => comp.$destroy());
|
||||
loadedComponents = [];
|
||||
loadedComponents.map((comp) => comp.$destroy());
|
||||
loadedComponents = [];
|
||||
|
||||
const elements = document.body.querySelectorAll(".lucent-component");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
const elements = document.body.querySelectorAll(".lucent-component");
|
||||
if (elements.length === 0) {
|
||||
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(
|
||||
"json-" + componentId
|
||||
).innerHTML;
|
||||
const props = JSON.parse(jsonData);
|
||||
props.axios = axiosInstance;
|
||||
const compOptions = {
|
||||
target: element,
|
||||
props: props,
|
||||
};
|
||||
|
||||
loadedComponents = [...loadedComponents, new component(compOptions)];
|
||||
const jsonData = document.getElementById("json-" + componentId).innerHTML;
|
||||
const props = JSON.parse(jsonData);
|
||||
const compOptions = {
|
||||
target: element,
|
||||
props: props,
|
||||
};
|
||||
Array.from(elements).map(loadElement);
|
||||
|
||||
loadedComponents = [...loadedComponents, new component(compOptions)];
|
||||
};
|
||||
Array.from(elements).map(loadElement);
|
||||
};
|
||||
|
||||
// document.addEventListener("turbo:load", loadSvelte);
|
||||
document.addEventListener("DOMContentLoaded", loadSvelte);
|
||||
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Login from "./account/Login.svelte";
|
||||
import Verify from "./account/Verify.svelte";
|
||||
import Profile from "./account/Profile.svelte";
|
||||
import {setContext} from "svelte";
|
||||
import { setContext } from "svelte";
|
||||
|
||||
const components = {
|
||||
register: Register,
|
||||
@@ -21,10 +21,7 @@
|
||||
setContext("channel", channel);
|
||||
setContext("user", user);
|
||||
</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 RecordEdit from "./records/Edit.svelte";
|
||||
import ContentIndex from "./content/Index.svelte";
|
||||
import {setContext} from "svelte";
|
||||
import Navbar from "./Navbar.svelte";
|
||||
import { setContext } from "svelte";
|
||||
import Navbar from "./layout/Navbar.svelte";
|
||||
import HomeIndex from "./home/Index.svelte";
|
||||
import BuildReport from "./build/Report.svelte";
|
||||
import Header from "./layout/Header.svelte";
|
||||
|
||||
const components = {
|
||||
members: Members,
|
||||
@@ -24,18 +25,22 @@
|
||||
// export let layout;
|
||||
export let channel;
|
||||
|
||||
export let axios;
|
||||
export let readableSchemas;
|
||||
|
||||
|
||||
setContext("axios", axios);
|
||||
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);
|
||||
|
||||
</script>
|
||||
|
||||
<Navbar schema={data.schema}/>
|
||||
|
||||
<svelte:component this={components[view]} {title} {...data}/>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="sidebar-content">
|
||||
<Navbar schema={data.schema} />
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<Header />
|
||||
<svelte:component this={components[view]} {title} {...data} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<script>
|
||||
import Avatar from "./account/Avatar.svelte";
|
||||
import NavbarMenu from "./NavbarMenu.svelte";
|
||||
import {getContext} from "svelte";
|
||||
|
||||
export let schema;
|
||||
const channel = getContext("channel");
|
||||
const readableSchemas = getContext("readableSchemas");
|
||||
const user = getContext("user");
|
||||
|
||||
let contentIsOpen = false;
|
||||
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
|
||||
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
|
||||
|
||||
let filesIsActive = false;
|
||||
let otherIsActive = false;
|
||||
if(schema){
|
||||
filesIsActive = fileSchemas.filter(s => s.name === schema.name).length > 0;
|
||||
otherIsActive = otherSchemas.filter(s => s.name === schema.name).length > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</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 ">
|
||||
<a class="nav-item" href="{channel.lucentUrl}">{channel.name}</a>
|
||||
<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}
|
||||
<!-- <div>-->
|
||||
<!-- <form method="GET">-->
|
||||
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
|
||||
<!-- class="form-control" required/>-->
|
||||
<!-- </form>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div>
|
||||
<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-light-subtle" class:d-none={!contentIsOpen}
|
||||
style="padding-top:36px " data-bs-scroll="true"
|
||||
data-bs-backdrop="false"
|
||||
tabindex="-1" aria-labelledby="offcanvasScrollingLabel">
|
||||
<!-- <div class="offcanvas-header">-->
|
||||
<!-- <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Content</h5>-->
|
||||
<!-- </div>-->
|
||||
<div class="offcanvas-body">
|
||||
<button on:click={(e) => contentIsOpen = false} class="btn btn-primary btn-sm d-xxl-none mb-4">« close</button>
|
||||
<div class="accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingMain">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseMain" aria-expanded="true"
|
||||
aria-controls="panelsStayOpen-collapseMain">
|
||||
Main
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseMain" class="accordion-collapse collapse show"
|
||||
aria-labelledby="panelsStayOpen-headingMain">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if otherSchemas.length > 0}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingOther">
|
||||
<button class="accordion-button" class:collapsed={!otherIsActive} type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseOther" aria-expanded={otherIsActive}
|
||||
aria-controls="panelsStayOpen-collapseOther">
|
||||
Other
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseOther" class="accordion-collapse collapse"
|
||||
class:show={otherIsActive}
|
||||
aria-labelledby="panelsStayOpen-headingOther">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ otherSchemas}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if fileSchemas.length > 0}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingFS">
|
||||
<button class="accordion-button " class:collapsed={!filesIsActive} type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseFS" aria-expanded={filesIsActive}
|
||||
aria-controls="panelsStayOpen-collapseFS">
|
||||
Filesystem
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseFS" class="accordion-collapse collapse" class:show={filesIsActive}
|
||||
aria-labelledby="panelsStayOpen-headingFS">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ fileSchemas}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schemas;
|
||||
export let schema;
|
||||
</script>
|
||||
|
||||
<div class="list-group list-group-flush">
|
||||
|
||||
{#each schemas as aschema}
|
||||
<a class="list-group-item list-group-item-action" class:active={aschema.name === schema?.name}
|
||||
aria-current="page"
|
||||
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,54 +1,68 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
let email = "";
|
||||
let message = "";
|
||||
let submitted = false;
|
||||
|
||||
function login(e) {
|
||||
e.preventDefault();
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/login", {
|
||||
email: email,
|
||||
apiPost(channel.lucentUrl + "/login", { email: email })
|
||||
.then(() => {
|
||||
submitted = true;
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
message = "You will receive an email with a login link"
|
||||
})
|
||||
.catch((error) => {
|
||||
});
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper-tiny">
|
||||
{#if message}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
|
||||
<form on:submit={login}>
|
||||
<div class="mb-3">
|
||||
<label for="emailaddress" class="form-label">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
id="emailaddress"
|
||||
required
|
||||
/>
|
||||
<div class="scope-login">
|
||||
<div class="bg-image"></div>
|
||||
<div class="login-form">
|
||||
{#if submitted}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p>
|
||||
You will receive an email with a login link at <b>{email}</b
|
||||
>.
|
||||
</p>
|
||||
<p>Check your spam folder</p>
|
||||
</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">
|
||||
<SpinnerButton label="Login"/>
|
||||
<button class="bt bt-primary">
|
||||
Send email
|
||||
<img
|
||||
alt="indicator"
|
||||
id="indicator"
|
||||
class="htmx-indicator"
|
||||
src="/img/spinner.svg"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import Avatar from "./Avatar.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
|
||||
const user = getContext("user");
|
||||
const channel = getContext("channel");
|
||||
@@ -16,16 +17,15 @@
|
||||
e.preventDefault();
|
||||
errorMessage = "";
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/account/update-name", {
|
||||
name: name,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/account/update-name", {
|
||||
name: name,
|
||||
})
|
||||
.then((response) => {
|
||||
successAlert.show();
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage = error.response?.data.error;
|
||||
console.log({errorMessage});
|
||||
console.log({ errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,55 +33,55 @@
|
||||
e.preventDefault();
|
||||
errorMessage = "";
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/account/update-email", {
|
||||
email: email,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/account/update-email", {
|
||||
email: email,
|
||||
})
|
||||
.then((response) => {
|
||||
successAlert.show();
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage = error.response?.data.error;
|
||||
console.log({errorMessage});
|
||||
console.log({ errorMessage });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="wrapper-tiny">
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<ErrorAlert message={errorMessage} />
|
||||
<SuccessAlert bind:this={successAlert} />
|
||||
<h3 class="header-small mb-5">
|
||||
<Avatar name={user.name}/>
|
||||
<Avatar name={user.name} />
|
||||
</h3>
|
||||
<form on:submit={saveName}>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group mb-5">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
required
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control mb-3"
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
<SpinnerButton label="Update"/>
|
||||
<SpinnerButton label="Update Name" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form on:submit={saveEmail}>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group mb-5">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
placeholder="Email"
|
||||
required
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control mb-3"
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<SpinnerButton label="Update"/>
|
||||
<SpinnerButton label="Update Email" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="list-group">
|
||||
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this
|
||||
device</a>
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="{channel.lucentUrl}/logout">Logout from this device</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script>
|
||||
import { apiPost } from "../../helpers";
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
let name = "";
|
||||
@@ -12,48 +13,45 @@
|
||||
e.preventDefault();
|
||||
errorMessage = "";
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/register", {
|
||||
name: name,
|
||||
email: email,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/register", {
|
||||
name: name,
|
||||
email: email,
|
||||
})
|
||||
.then(() => {
|
||||
window.location = channel.lucentUrl + "/login";
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage = error.response?.data.error;
|
||||
console.log({errorMessage});
|
||||
console.log({ errorMessage });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper-tiny">
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<ErrorAlert message={errorMessage} />
|
||||
|
||||
<form on:submit={register}>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control"
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control"
|
||||
id="name"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
id="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-center mt-5 d-block">
|
||||
<SpinnerButton label="Register"/>
|
||||
<SpinnerButton label="Register" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let email;
|
||||
export let token;
|
||||
let successAlert;
|
||||
|
||||
function login(e) {
|
||||
e.preventDefault();
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/verify", {
|
||||
email: email,
|
||||
token: token,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/verify", {
|
||||
email: email,
|
||||
token: token,
|
||||
})
|
||||
.then((response) => {
|
||||
window.location = channel.lucentUrl;
|
||||
})
|
||||
.catch((error) => {
|
||||
});
|
||||
.catch((error) => {});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<SuccessAlert bind:this={successAlert}/>
|
||||
<div class="wrapper-tiny">
|
||||
|
||||
<form on:submit={login}>
|
||||
<div class="mb-3 text-center">
|
||||
<h3>Login as {email}</h3>
|
||||
|
||||
<div class="scope-login">
|
||||
<div class="bg-image"></div>
|
||||
<div class="login-form">
|
||||
<div class="form">
|
||||
<h2 class="mb-5">Welcome to Lucent</h2>
|
||||
<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 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>
|
||||
import {getContext, onMount} from "svelte";
|
||||
|
||||
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
const channel = getContext("channel");
|
||||
export let title;
|
||||
export let command;
|
||||
$: date = "";
|
||||
$: logs = "";
|
||||
|
||||
let anchorEl;
|
||||
let inProgress = false;
|
||||
|
||||
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) {
|
||||
inProgress = true;
|
||||
const data = JSON.parse(event.data);
|
||||
date = data.date;
|
||||
logs = data.logs;
|
||||
|
||||
}
|
||||
anchorEl.scrollIntoView();
|
||||
};
|
||||
eventSource.onerror = (e) => {
|
||||
console.log(e)
|
||||
console.log(e);
|
||||
eventSource.close();
|
||||
inProgress = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildWebsite(e) {
|
||||
e.preventDefault();
|
||||
inProgress = true;
|
||||
|
||||
axios.post(channel.lucentUrl + "/build").then(response => {
|
||||
connect()
|
||||
})
|
||||
|
||||
apiPost(channel.lucentUrl + "/command/" + command.signature).then(
|
||||
(response) => {
|
||||
connect();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
connect()
|
||||
})
|
||||
|
||||
connect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="wrapper-tiny transparent mb-5">
|
||||
<div class="common-wrapper">
|
||||
<div class="lx-card mt-5">
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
{#if inProgress}
|
||||
<span class="badge text-bg-warning">
|
||||
Build in progress
|
||||
</span>
|
||||
<span class="badge text-bg-warning"> Action in progress </span>
|
||||
{/if}
|
||||
{#if !inProgress && logs}
|
||||
<span class="badge text-bg-info">
|
||||
Build completed
|
||||
</span>
|
||||
<span class="badge text-bg-info"> Action completed </span>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<pre>{logs}</pre>
|
||||
<pre class="logs">{logs}
|
||||
<div bind:this={anchorEl}> </div>
|
||||
</pre>
|
||||
</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>
|
||||
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>
|
||||
<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>
|
||||
|
||||
{#if message}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{message}
|
||||
<div class="notice notice-error" role="alert">
|
||||
<div class="title">Submission Errors</div>
|
||||
<div class="content"> {message}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -56,32 +56,32 @@
|
||||
path: '<path d="M416 288h-95.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H416c17.67 0 32-14.33 32-32S433.7 288 416 288zM544 32h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 32 544 32zM352 416h-32c-17.67 0-32 14.33-32 32s14.33 32 32 32h32c17.67 0 31.1-14.33 31.1-32S369.7 416 352 416zM480 160h-159.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H480c17.67 0 32-14.33 32-32S497.7 160 480 160zM192.4 330.7L160 366.1V64.03C160 46.33 145.7 32 128 32S96 46.33 96 64.03v302L63.6 330.7c-6.312-6.883-14.94-10.38-23.61-10.38c-7.719 0-15.47 2.781-21.61 8.414c-13.03 11.95-13.9 32.22-1.969 45.27l87.1 96.09c12.12 13.26 35.06 13.26 47.19 0l87.1-96.09c11.94-13.05 11.06-33.31-1.969-45.27C224.6 316.8 204.4 317.7 192.4 330.7z"/>',
|
||||
viewBox: "0 0 576 512",
|
||||
},
|
||||
"filter": {
|
||||
filter: {
|
||||
path: '<path d="M3.853 54.87C10.47 40.9 24.54 32 40 32H472C487.5 32 501.5 40.9 508.1 54.87C514.8 68.84 512.7 85.37 502.1 97.33L320 320.9V448C320 460.1 313.2 471.2 302.3 476.6C291.5 482 278.5 480.9 268.8 473.6L204.8 425.6C196.7 419.6 192 410.1 192 400V320.9L9.042 97.33C-.745 85.37-2.765 68.84 3.854 54.87L3.853 54.87z"/>',
|
||||
viewBox: "0 0 512 512",
|
||||
},
|
||||
"calendar": {
|
||||
calendar: {
|
||||
path: '<path d="M96 32C96 14.33 110.3 0 128 0C145.7 0 160 14.33 160 32V64H288V32C288 14.33 302.3 0 320 0C337.7 0 352 14.33 352 32V64H400C426.5 64 448 85.49 448 112V160H0V112C0 85.49 21.49 64 48 64H96V32zM448 464C448 490.5 426.5 512 400 512H48C21.49 512 0 490.5 0 464V192H448V464z"/>',
|
||||
viewBox: "0 0 448 512",
|
||||
},
|
||||
"pencil": {
|
||||
pencil: {
|
||||
path: '<path d="M421.7 220.3L188.5 453.4L154.6 419.5L158.1 416H112C103.2 416 96 408.8 96 400V353.9L92.51 357.4C87.78 362.2 84.31 368 82.42 374.4L59.44 452.6L137.6 429.6C143.1 427.7 149.8 424.2 154.6 419.5L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3zM492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75z"/>',
|
||||
viewBox: "0 0 512 512",
|
||||
},
|
||||
"database": {
|
||||
database: {
|
||||
path: '<path d="M448 80V128C448 172.2 347.7 208 224 208C100.3 208 0 172.2 0 128V80C0 35.82 100.3 0 224 0C347.7 0 448 35.82 448 80zM393.2 214.7C413.1 207.3 433.1 197.8 448 186.1V288C448 332.2 347.7 368 224 368C100.3 368 0 332.2 0 288V186.1C14.93 197.8 34.02 207.3 54.85 214.7C99.66 230.7 159.5 240 224 240C288.5 240 348.3 230.7 393.2 214.7V214.7zM54.85 374.7C99.66 390.7 159.5 400 224 400C288.5 400 348.3 390.7 393.2 374.7C413.1 367.3 433.1 357.8 448 346.1V432C448 476.2 347.7 512 224 512C100.3 512 0 476.2 0 432V346.1C14.93 357.8 34.02 367.3 54.85 374.7z"/>',
|
||||
viewBox: "0 0 448 512",
|
||||
},
|
||||
"dice": {
|
||||
dice: {
|
||||
path: '<path d="M447.1 224c0-12.56-4.781-25.13-14.35-34.76l-174.9-174.9C249.1 4.786 236.5 0 223.1 0C211.4 0 198.9 4.786 189.2 14.35L14.35 189.2C4.783 198.9-.0011 211.4-.0011 223.1c0 12.56 4.785 25.17 14.35 34.8l174.9 174.9c9.625 9.562 22.19 14.35 34.75 14.35s25.13-4.783 34.75-14.35l174.9-174.9C443.2 249.1 447.1 236.6 447.1 224zM96 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S120 210.8 120 224S109.3 248 96 248zM224 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 376 224 376zM224 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S248 210.8 248 224S237.3 248 224 248zM224 120c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 120 224 120zM352 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S365.3 248 352 248zM591.1 192l-118.7 0c4.418 10.27 6.604 21.25 6.604 32.23c0 20.7-7.865 41.38-23.63 57.14l-136.2 136.2v46.37C320 490.5 341.5 512 368 512h223.1c26.5 0 47.1-21.5 47.1-47.1V240C639.1 213.5 618.5 192 591.1 192zM479.1 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S493.2 376 479.1 376z"/>',
|
||||
viewBox: "0 0 640 512",
|
||||
},
|
||||
|
||||
|
||||
"triangle-exclamation": {
|
||||
path: '<path d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/>',
|
||||
viewBox: "0 0 512 512",
|
||||
},
|
||||
"eye": {
|
||||
eye: {
|
||||
path: '<path d="M279.6 160.4C282.4 160.1 285.2 160 288 160C341 160 384 202.1 384 256C384 309 341 352 288 352C234.1 352 192 309 192 256C192 253.2 192.1 250.4 192.4 247.6C201.7 252.1 212.5 256 224 256C259.3 256 288 227.3 288 192C288 180.5 284.1 169.7 279.6 160.4zM480.6 112.6C527.4 156 558.7 207.1 573.5 243.7C576.8 251.6 576.8 260.4 573.5 268.3C558.7 304 527.4 355.1 480.6 399.4C433.5 443.2 368.8 480 288 480C207.2 480 142.5 443.2 95.42 399.4C48.62 355.1 17.34 304 2.461 268.3C-.8205 260.4-.8205 251.6 2.461 243.7C17.34 207.1 48.62 156 95.42 112.6C142.5 68.84 207.2 32 288 32C368.8 32 433.5 68.84 480.6 112.6V112.6zM288 112C208.5 112 144 176.5 144 256C144 335.5 208.5 400 288 400C367.5 400 432 335.5 432 256C432 176.5 367.5 112 288 112z"/>',
|
||||
viewBox: "0 0 576 512",
|
||||
},
|
||||
@@ -93,19 +93,44 @@
|
||||
path: '<path d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/>',
|
||||
viewBox: "0 0 512 512",
|
||||
},
|
||||
"expand": {
|
||||
expand: {
|
||||
path: '<path d="M128 32H32C14.31 32 0 46.31 0 64v96c0 17.69 14.31 32 32 32s32-14.31 32-32V96h64c17.69 0 32-14.31 32-32S145.7 32 128 32zM416 32h-96c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32V64C448 46.31 433.7 32 416 32zM128 416H64v-64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96c0 17.69 14.31 32 32 32h96c17.69 0 32-14.31 32-32S145.7 416 128 416zM416 320c-17.69 0-32 14.31-32 32v64h-64c-17.69 0-32 14.31-32 32s14.31 32 32 32h96c17.69 0 32-14.31 32-32v-96C448 334.3 433.7 320 416 320z"/>',
|
||||
viewBox: "0 0 448 512",
|
||||
},
|
||||
"compress": {
|
||||
compress: {
|
||||
path: '<path d="M128 320H32c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32v-96C160 334.3 145.7 320 128 320zM416 320h-96c-17.69 0-32 14.31-32 32v96c0 17.69 14.31 32 32 32s32-14.31 32-32v-64h64c17.69 0 32-14.31 32-32S433.7 320 416 320zM320 192h96c17.69 0 32-14.31 32-32s-14.31-32-32-32h-64V64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96C288 177.7 302.3 192 320 192zM128 32C110.3 32 96 46.31 96 64v64H32C14.31 128 0 142.3 0 160s14.31 32 32 32h96c17.69 0 32-14.31 32-32V64C160 46.31 145.7 32 128 32z"/>',
|
||||
viewBox: "0 0 448 512",
|
||||
},
|
||||
"check": {
|
||||
check: {
|
||||
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",
|
||||
},
|
||||
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",
|
||||
},
|
||||
upload: {
|
||||
path: '<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/> <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>',
|
||||
viewBox: "0 0 16 16",
|
||||
},
|
||||
};
|
||||
|
||||
export let width = 16;
|
||||
export let height = 16;
|
||||
export let icon = "";
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-spinner" {disabled}>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{label}
|
||||
</button>
|
||||
<button type="submit" class="button secondary btn-spinner" {disabled}>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
@@ -12,21 +12,10 @@
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
<div
|
||||
transition:fly={{ duration: 500 }}
|
||||
class="lx-alert text-white bg-success border-1 border rounded px-3 py-0 text-center"
|
||||
role="alert"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
<div class="notice notice-success" transition:fly={{ duration: 500 }} role="alert">
|
||||
<div class="title">Success</div>
|
||||
<div class="content"> {message}</div>
|
||||
</div>
|
||||
|
||||
{/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,5 +1,6 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let selected;
|
||||
@@ -8,10 +9,9 @@
|
||||
|
||||
function deleteRecords(e) {
|
||||
e.preventDefault();
|
||||
axios
|
||||
.post(channel.lucentUrl + "/records/delete", {
|
||||
ids: selected.map((s) => s.id),
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/records/delete", {
|
||||
ids: selected.map((s) => s.id),
|
||||
})
|
||||
.then((response) => {
|
||||
window.location.reload();
|
||||
})
|
||||
@@ -21,11 +21,10 @@
|
||||
}
|
||||
|
||||
function changeStatus(e, status) {
|
||||
axios
|
||||
.post(channel.lucentUrl + "/records/status/" + status, {
|
||||
schemaName: schema.name,
|
||||
records: selected
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/records/status/" + status, {
|
||||
schemaName: schema.name,
|
||||
records: selected,
|
||||
})
|
||||
.then((response) => {
|
||||
window.location.reload();
|
||||
})
|
||||
@@ -35,49 +34,47 @@
|
||||
}
|
||||
</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>
|
||||
<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
|
||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary">Publish
|
||||
</button
|
||||
>
|
||||
<button
|
||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary">Make Draft
|
||||
</button
|
||||
>
|
||||
{#if filter["status_in"] === "trashed"}
|
||||
class="button"
|
||||
>Publish
|
||||
</button>
|
||||
{#if schema.hasDrafts}
|
||||
<button
|
||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary">Publish
|
||||
</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
|
||||
>
|
||||
class="button"
|
||||
>Make Draft
|
||||
</button>
|
||||
{/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>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import {selectRecord} from "./functions/recordSelect.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
export let schema;
|
||||
export let records;
|
||||
export let isWritable;
|
||||
export let selected = [];
|
||||
|
||||
function select(record) {
|
||||
selected = selectRecord(record, selected)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<div class="row" style="max-width:1000px">
|
||||
{#each records as record (record.id)}
|
||||
<div class="col-6 col-md-4">
|
||||
<div
|
||||
class="file-wrapper rounded p-2 mb-4 bg-light"
|
||||
class:selected={selected.includes(record)}
|
||||
>
|
||||
{#if isWritable}
|
||||
<div class="form-check">
|
||||
<input
|
||||
on:change={() => select(record)}
|
||||
class="form-check-input "
|
||||
type="checkbox"
|
||||
checked={selected.find(
|
||||
(r) => r.id === record.id
|
||||
)}
|
||||
value={record}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="d-flex justify-content-center">
|
||||
<Preview {record} size="medium"/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
||||
style="
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
">{record._file.path}</a
|
||||
>
|
||||
<span
|
||||
class="lx-small-text text-muted d-block text-center"
|
||||
>{record._file.mime}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.form-check {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,10 +3,9 @@
|
||||
import Pagination from "./pagination/Pagination.svelte";
|
||||
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
||||
import Table from "./Table.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import Grid from "./Grid.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { apiGet } from "../../helpers";
|
||||
|
||||
const axios = getContext("axios");
|
||||
export let schema;
|
||||
export let users;
|
||||
export let records;
|
||||
@@ -27,18 +26,18 @@
|
||||
|
||||
function refresh(e) {
|
||||
const newUrl = e.detail;
|
||||
axios
|
||||
.get(newUrl)
|
||||
apiGet(newUrl)
|
||||
.then((response) => {
|
||||
records = response.data.records;
|
||||
sortParam = response.data.sortParam;
|
||||
sortField = response.data.sortField;
|
||||
operators = response.data.operators;
|
||||
filter = response.data.filter;
|
||||
skip = response.data.skip;
|
||||
limit = response.data.limit;
|
||||
total = response.data.total;
|
||||
modalUrl = response.data.modalUrl;
|
||||
records = response.records;
|
||||
sortParam = response.sortParam;
|
||||
sortField = response.sortField;
|
||||
operators = response.operators;
|
||||
filter = response.filter;
|
||||
skip = response.skip;
|
||||
limit = response.limit;
|
||||
total = response.total;
|
||||
modalUrl = response.modalUrl;
|
||||
document.querySelector("dialog h3").scrollIntoView();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
@@ -46,61 +45,50 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper-large transparent ">
|
||||
<div class="lx-card mb-4 {inModal ? 'mt-0' : 'mt-5'}">
|
||||
<h3 class="header-normal mb-5 ">
|
||||
<div class="">
|
||||
<div class={inModal ? "mt-0" : "mt-5"}>
|
||||
<h3 class="header-normal mb-5">
|
||||
{schema.label}
|
||||
</h3>
|
||||
{#if selected.length > 0 && !inModal && isWritable}
|
||||
<ActionsOnSelected {schema} {selected} {filter}/>
|
||||
<ActionsOnSelected {schema} {selected} {filter} />
|
||||
{:else}
|
||||
<Tools
|
||||
bind:schema
|
||||
bind:records
|
||||
{systemFields}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{operators}
|
||||
{filter}
|
||||
{graph}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
{isWritable}
|
||||
on:refresh={refresh}
|
||||
bind:schema
|
||||
bind:records
|
||||
{systemFields}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{operators}
|
||||
{filter}
|
||||
{graph}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
{isWritable}
|
||||
on:refresh={refresh}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if schema.type === "collection"}
|
||||
<Table
|
||||
{records}
|
||||
{graph}
|
||||
{schema}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{systemFields}
|
||||
{inModal}
|
||||
{users}
|
||||
{isWritable}
|
||||
bind:selected
|
||||
/>
|
||||
{:else}
|
||||
<Grid
|
||||
{records}
|
||||
{schema}
|
||||
{isWritable}
|
||||
bind:selected
|
||||
/>
|
||||
|
||||
{/if}
|
||||
<Table
|
||||
{records}
|
||||
{graph}
|
||||
{schema}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{systemFields}
|
||||
{inModal}
|
||||
{users}
|
||||
{isWritable}
|
||||
bind:selected
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
{limit}
|
||||
{skip}
|
||||
{total}
|
||||
on:refresh={refresh}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
{limit}
|
||||
{skip}
|
||||
{total}
|
||||
on:refresh={refresh}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import RenderField from "./RenderField.svelte";
|
||||
import Avatar from "../account/Avatar.svelte";
|
||||
import Status from "../records/Status.svelte";
|
||||
import {usernameById} from "../account/users";
|
||||
import {friendlyDate} from "../../helpers";
|
||||
import { usernameById } from "../account/users";
|
||||
import { friendlyDate } from "../../helpers";
|
||||
|
||||
export let schema;
|
||||
export let users;
|
||||
@@ -12,7 +12,6 @@
|
||||
export let sortParam;
|
||||
export let sortField;
|
||||
export let visibleColumns;
|
||||
|
||||
</script>
|
||||
|
||||
{#each visibleColumns as field, index}
|
||||
@@ -20,40 +19,48 @@
|
||||
class="field-ui-{field.info.name}"
|
||||
class:is-sort={field.name === sortField.name}
|
||||
>
|
||||
<RenderField {record} {schema} {graph} {field}/>
|
||||
<RenderField {record} {schema} {graph} {field} />
|
||||
</td>
|
||||
{/each}
|
||||
{#if schema.visible.includes("status")}
|
||||
{#if schema.visible?.includes("_sys.status")}
|
||||
<td
|
||||
class="text-center"
|
||||
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
||||
>
|
||||
<Status status={record.status}/>
|
||||
<Status status={record.status} />
|
||||
</td>
|
||||
{/if}
|
||||
{#if schema.visible.includes("_sys.createdBy")}
|
||||
{#if schema.visible?.includes("_sys.createdBy")}
|
||||
<td
|
||||
class="text-center"
|
||||
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
|
||||
class:is-sort={"-_sys.createdBy" == sortParam ||
|
||||
"_sys.createdBy" == sortParam}
|
||||
>
|
||||
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/>
|
||||
<Avatar name={usernameById(users, record.createdBy)} side={24} />
|
||||
</td>
|
||||
{/if}
|
||||
{#if schema.visible.includes("_sys.updatedBy")}
|
||||
{#if schema.visible?.includes("_sys.updatedBy")}
|
||||
<td
|
||||
class="text-center"
|
||||
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
|
||||
class:is-sort={"-_sys.updatedBy" == sortParam ||
|
||||
"_sys.updatedBy" == sortParam}
|
||||
>
|
||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||
</td>
|
||||
{/if}
|
||||
{#if schema.visible.includes("_sys.createdAt")}
|
||||
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
|
||||
{friendlyDate(record._sys.createdAt)}
|
||||
{#if schema.visible?.includes("_sys.createdAt")}
|
||||
<td
|
||||
class:is-sort={"-_sys.createdAt" == sortParam ||
|
||||
"_sys.createdAt" == sortParam}
|
||||
>
|
||||
{friendlyDate(record.createdAt)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if schema.visible.includes("_sys.updatedAt")}
|
||||
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
|
||||
{friendlyDate(record._sys.updatedAt)}
|
||||
{#if schema.visible?.includes("_sys.updatedAt")}
|
||||
<td
|
||||
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
||||
"_sys.updatedAt" == sortParam}
|
||||
>
|
||||
{friendlyDate(record.updatedAt)}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import RecordRow from "./RecordRow.svelte";
|
||||
import {previewTitle} from "../records/Preview";
|
||||
import {usernameById} from "../account/users";
|
||||
import {getContext} from "svelte";
|
||||
import { usernameById } from "../account/users";
|
||||
import { getContext } from "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");
|
||||
|
||||
@@ -20,95 +20,80 @@
|
||||
export let selected = [];
|
||||
|
||||
function eventToggleAll(e) {
|
||||
selected = toggleAll(e,records,selected)
|
||||
selected = toggleAll(e, records, selected);
|
||||
}
|
||||
|
||||
function select(record) {
|
||||
selected = selectRecord(record, selected)
|
||||
selected = selectRecord(record, selected);
|
||||
}
|
||||
|
||||
$: visibleColumns = schema.fields.filter(c => schema.visible.includes(c.name))
|
||||
|
||||
console.log(schema);
|
||||
$: visibleColumns = schema.fields.filter(
|
||||
(c) => schema.visible?.includes(c.name) ?? [],
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="lx-table rounded">
|
||||
<table class="">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
{#if isWritable}
|
||||
<th>
|
||||
<input
|
||||
on:change|preventDefault={eventToggleAll}
|
||||
<div class="table mt-5">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#if isWritable}
|
||||
<th>
|
||||
<Checkbox
|
||||
value=""
|
||||
on:change={eventToggleAll}
|
||||
indeterminate={selected.length > 0 &&
|
||||
selected.length < records.length}
|
||||
checked={selected.length == records.length}
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
</th>
|
||||
{/if}
|
||||
selected.length < records.length}
|
||||
checked={selected.length === records.length}
|
||||
></Checkbox>
|
||||
</th>
|
||||
{/if}
|
||||
|
||||
{#each visibleColumns as field}
|
||||
<th
|
||||
{#each visibleColumns as field}
|
||||
<th
|
||||
class="field-ui-{field.info.name ?? field.ui}"
|
||||
class:is-sort={field.name === sortField.name}
|
||||
scope="col"
|
||||
title={field.help}
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top">{field.label}</th
|
||||
>
|
||||
{/each}
|
||||
{#each systemFields.filter(c => schema.visible.includes(c.name)) as sysField}
|
||||
<th>{sysField.label}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
title={field.help}>{field.label}</th
|
||||
>
|
||||
{/each}
|
||||
{#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
|
||||
<th class:is-sort={sysField.name === sortField.name}
|
||||
>{sysField.label}</th
|
||||
>
|
||||
{/each}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each records as record (record.id)}
|
||||
<tr>
|
||||
<td class="title-td">
|
||||
<div
|
||||
class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
|
||||
>
|
||||
<div class="d-flex align-items-center ">
|
||||
{#each records as record (record.id)}
|
||||
<tr>
|
||||
<td class="title-td">
|
||||
<div class="title-td-contents">
|
||||
{#if isWritable}
|
||||
<div class="form-check">
|
||||
<input
|
||||
on:change={() => select(record)}
|
||||
class="form-check-input "
|
||||
type="checkbox"
|
||||
checked={selected.find(
|
||||
(r) => r.id === record.id
|
||||
)}
|
||||
value={record}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
on:change={() => select(record)}
|
||||
checked={selected.find(
|
||||
(r) => r.id === record.id,
|
||||
)}
|
||||
value={record}
|
||||
></Checkbox>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
|
||||
class="me-2 text-decoration-none text-dark fs-6"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
title={previewTitle(channel.schemas, record, graph)}
|
||||
data-bs-toggle="tooltip" data-bs-placement="left"
|
||||
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
>
|
||||
{previewTitle(channel.schemas, record, graph)}
|
||||
{#if record.status === "draft"}
|
||||
<span
|
||||
style="text-transform: uppercase;font-size:10px"
|
||||
>{record.status}</span
|
||||
>
|
||||
{/if}
|
||||
{record.data.name}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<Avatar
|
||||
name={usernameById(
|
||||
users,
|
||||
record._sys.updatedBy
|
||||
)}
|
||||
side={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<RecordRow
|
||||
</td>
|
||||
<RecordRow
|
||||
{record}
|
||||
{graph}
|
||||
{schema}
|
||||
@@ -116,10 +101,15 @@
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{users}
|
||||
/>
|
||||
</tr>
|
||||
{/each}
|
||||
/>
|
||||
<td>
|
||||
<Avatar
|
||||
name={usernameById(users, record.updatedBy)}
|
||||
side={24}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,13 +11,8 @@
|
||||
// if (edges[0]) {
|
||||
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
|
||||
// }
|
||||
|
||||
console.log(filePreviews)
|
||||
</script>
|
||||
|
||||
<!-- {#if firstRecord}
|
||||
<Preview record={firstRecord} size="tiny" />
|
||||
{/if} -->
|
||||
<div class="d-flex me-1">
|
||||
{#each filePreviews as file}
|
||||
<div class="me-1">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<div class="references">
|
||||
{#each recordEdges as recordEdge}
|
||||
<span class="mr-3">
|
||||
<span class="reference">
|
||||
<PreviewCardSmall {schemas} {graph} record={recordEdge}/>
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
</script>
|
||||
|
||||
{#each pages as i}
|
||||
<li class="page-item">
|
||||
{#if currentPage == i}
|
||||
<li class="page-item" class:active={currentPage === i}>
|
||||
{#if currentPage === i}
|
||||
<span class="page-link active">{i}</span>
|
||||
{:else}
|
||||
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
import { range } from "lodash";
|
||||
|
||||
import NavItem from "./NavItem.svelte";
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
$: totalPages = Math.ceil(total / limit);
|
||||
$: 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) => {
|
||||
return i > 0 && i <= totalPages;
|
||||
});
|
||||
@@ -43,7 +47,7 @@
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
<ul class="pagination">
|
||||
{#if totalPages > 1}
|
||||
<li class="page-item disabled" class:disabled={currentPage === 1}>
|
||||
<a on:click={first} href="/" class="page-link"> First </a>
|
||||
@@ -69,7 +73,7 @@
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
<p class="text-muted text-center">
|
||||
<p style="display: flex;justify-content: center; gap: 4px">
|
||||
Showing
|
||||
<span class="font-medium">{+skip + 1}</span>
|
||||
to
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import {previewTitle} from "../../records/Preview";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
export let schema;
|
||||
export let operators;
|
||||
@@ -11,7 +10,6 @@
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
export let graph;
|
||||
|
||||
let filter = {
|
||||
label: "",
|
||||
operator: "",
|
||||
@@ -19,10 +17,10 @@
|
||||
isReference: key.startsWith("children"),
|
||||
};
|
||||
|
||||
filter = [
|
||||
extractOperator(key),
|
||||
extractLabel(schema, key),
|
||||
].reduce((mem, fn) => fn(mem), filter);
|
||||
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
|
||||
(mem, fn) => fn(mem),
|
||||
filter,
|
||||
);
|
||||
|
||||
function extractOperator(key) {
|
||||
return (filter) => {
|
||||
@@ -47,8 +45,7 @@
|
||||
const filterField = schema.fields.find((f) => f.name === fieldName);
|
||||
filter.label = filterField?.label ?? fieldName;
|
||||
return filter;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const filterRecord = extractFilterRecord(graph, value);
|
||||
@@ -57,7 +54,7 @@
|
||||
if (!filter.isReference) {
|
||||
return null;
|
||||
}
|
||||
return graph.records.find(r => r.id === value);
|
||||
return graph.records.find((r) => r.id === value);
|
||||
}
|
||||
|
||||
function removeFilter(k) {
|
||||
@@ -73,30 +70,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="applied-filter d-inline-block border border-primary rounded lx-small-text me-1 px-2 py-1">
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
{#if filter.isReference && filterRecord}
|
||||
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
|
||||
{:else}
|
||||
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {value}
|
||||
{/if}
|
||||
<button
|
||||
on:click|preventDefault={() => removeFilter(key)}
|
||||
type="button"
|
||||
class="btn-close btn-close ms-1"
|
||||
aria-label="Close"
|
||||
/>
|
||||
</div>
|
||||
<span class="applied-filter">
|
||||
{#if filter.isReference && filterRecord}
|
||||
{filter.label} is {filterRecord.data.name}
|
||||
{:else}
|
||||
{filter.label}
|
||||
{operators.find((o) => o.name === filter.operator)?.symbol ?? ""}
|
||||
{operators.find((o) => o.name === filter.operator)?.hasValue
|
||||
? value
|
||||
: ""}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|preventDefault={() => removeFilter(key)}
|
||||
type="button"
|
||||
class="button-text"
|
||||
aria-label="Close"
|
||||
><Icon width={12} height={12} icon="close"></Icon></button
|
||||
>
|
||||
</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>
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import FilterReferenceInput from "./FilterReferenceInput.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import FilterReferenceInput from "./FilterReferenceInput.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let schema;
|
||||
@@ -14,50 +14,10 @@
|
||||
let dropdown;
|
||||
let search = "";
|
||||
let systemFieldsFiltered = systemFields;
|
||||
if (schema.type == "collection") {
|
||||
if (schema.type === "collection") {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
let filterKeyValue = search.split("=")[0] ?? "";
|
||||
@@ -79,63 +39,195 @@
|
||||
} else {
|
||||
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>
|
||||
|
||||
<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">
|
||||
<select bind:value={selectedField} class="form-select">
|
||||
{#each filterableFields as field}
|
||||
<option value={field}>{field.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="px-3 py-1 d-flex align-items-center">
|
||||
<select class="form-select" bind:value={selectedOperator}>
|
||||
{#each operatorsFiltered as operator}
|
||||
<option value={operator}>{operator.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="px-3 py-1 d-flex align-items-center">
|
||||
{#if selectedField?.info?.name === "reference" && selectedOperator.name === "eq"}
|
||||
<FilterReferenceInput field={selectedField} bind:value={selectedInput} on:addFilter={addFilter}/>
|
||||
{:else}
|
||||
<div>
|
||||
<Dropdown bind:this={dropdown}>
|
||||
<div slot="button">
|
||||
<Icon icon="filter"/>
|
||||
<span class="ms-1">Filter</span>
|
||||
</div>
|
||||
<div class:hide={activeMenu !== "main"}>
|
||||
<button class="dropdown-item button" on:click={e => activeMenu = "byField" }>
|
||||
Filter by field
|
||||
</button>
|
||||
<button class="dropdown-item button" on:click={e => activeMenu = "byReference" }>
|
||||
Filter by Reference
|
||||
</button>
|
||||
<button class="dropdown-item button" on:click={e => activeMenu = "advanced" }>
|
||||
Advanced filter
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={selectedInput}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="px-3 py-1 d-flex align-items-center">
|
||||
</div>
|
||||
<button
|
||||
on:click={addFilter}
|
||||
class="btn btn-outline-primary"
|
||||
on:click={applyFilter}
|
||||
class="button applied-filter"
|
||||
type="button"
|
||||
>
|
||||
Add filter
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr/>
|
||||
<div><h6 class="dropdown-header">Advanced filters</h6></div>
|
||||
<form on:submit={submitSearch}>
|
||||
<div class="px-3 py-1 d-flex align-items-center">
|
||||
<input
|
||||
bind:value={search}
|
||||
type="search"
|
||||
class="form-control"
|
||||
placeholder="Advanced filters"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class:hide={activeMenu !== "byReference"}>
|
||||
{#if !activeReference}
|
||||
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
|
||||
<Icon icon="arrow-left"></Icon>
|
||||
Back
|
||||
</button>
|
||||
{#each referenceFields as field}
|
||||
<button class="dropdown-item button" on:click={e => selectReference(e,field)}>
|
||||
{field.label}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if activeReference}
|
||||
<button class="dropdown-item button" on:click={e => activeReference = null }>
|
||||
<Icon icon="arrow-left"></Icon>
|
||||
Back
|
||||
</button>
|
||||
<div class="selected-filter">field: {activeReference.label}</div>
|
||||
<div class="mt-2">
|
||||
<FilterReferenceInput field={activeReference} bind:value={selectedInput}
|
||||
on:addFilter={applyFilter}/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</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>
|
||||
</Dropdown>
|
||||
|
||||
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import {debounce} from "lodash";
|
||||
import {previewTitle} from "../../records/Preview";
|
||||
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import { apiGet, debounce } from "../../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -10,22 +8,20 @@
|
||||
export let value = "";
|
||||
export let field;
|
||||
|
||||
let search = ""
|
||||
$: searchOptions = []
|
||||
|
||||
let search = "";
|
||||
$: searchOptions = [];
|
||||
|
||||
const updateResults = debounce((e) => {
|
||||
axios
|
||||
.get(channel.lucentUrl + "/records/suggestions", {
|
||||
params: {
|
||||
schema: field.collections[0],
|
||||
field: "search",
|
||||
value: search,
|
||||
ui: "search",
|
||||
},
|
||||
})
|
||||
apiGet(channel.lucentUrl + "/records/suggestions", {
|
||||
params: {
|
||||
schema: field.collections[0],
|
||||
field: "search",
|
||||
value: search,
|
||||
ui: "search",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
searchOptions = response.data;
|
||||
searchOptions = response;
|
||||
})
|
||||
.catch((error) => {
|
||||
searchOptions = [];
|
||||
@@ -35,44 +31,36 @@
|
||||
|
||||
function apply(e, newOption) {
|
||||
e.preventDefault();
|
||||
value = newOption.id
|
||||
value = newOption.id;
|
||||
dispatch("addFilter");
|
||||
value = ""
|
||||
|
||||
value = "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<input
|
||||
<div class="reference-tags">
|
||||
<input
|
||||
type="search"
|
||||
on:keyup={updateResults}
|
||||
class="form-control dropdown-toggle"
|
||||
bind:value={search}
|
||||
placeholder={"Search for "+field.label}
|
||||
data-bs-toggle="dropdown"
|
||||
placeholder={"Search for " + field.label}
|
||||
autocomplete="off"
|
||||
/>
|
||||
/>
|
||||
|
||||
<div class="dropdown-menu w-100">
|
||||
|
||||
{#if searchOptions}
|
||||
{#each searchOptions as option (option.id)}
|
||||
<div
|
||||
<div class="reference-tags-results">
|
||||
{#if searchOptions}
|
||||
{#each searchOptions as option (option.id)}
|
||||
<div
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => apply(e, option)}
|
||||
on:keypress={(e) => apply(e, option)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
{previewTitle(channel.schemas, option)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
|
||||
Start typing...
|
||||
{/each}
|
||||
{/if}
|
||||
>
|
||||
{option.data.name}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="start-typing">Start typing...</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let schema;
|
||||
@@ -31,101 +32,91 @@
|
||||
|
||||
function sortAsc(e, field) {
|
||||
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);
|
||||
}
|
||||
|
||||
function sortDesc(e, field) {
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class=" ">
|
||||
<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"
|
||||
>
|
||||
|
||||
<Dropdown>
|
||||
<div slot="button">
|
||||
{#if sortParam.startsWith("-")}
|
||||
<Icon icon="arrow-down-wide-short"/>
|
||||
{:else}
|
||||
<Icon icon="arrow-up-short-wide"/>
|
||||
{/if}
|
||||
<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>
|
||||
{#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>
|
||||
import FilterFields from "./FilterFields.svelte";
|
||||
import Uploader from "../../files/Uploader.svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import SortFields from "./SortFields.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");
|
||||
|
||||
@@ -39,123 +40,99 @@
|
||||
} else {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function uploadComplete(e) {
|
||||
records = e.detail;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-3 d-flex align-items-center justify-content-between">
|
||||
<div class=" d-flex align-items-center">
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-filters">
|
||||
<SortFields
|
||||
{schema}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{systemFields}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
{schema}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{systemFields}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
/>
|
||||
|
||||
|
||||
<FilterFields
|
||||
bind:schema
|
||||
{systemFields}
|
||||
{operators}
|
||||
{filter}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
bind:schema
|
||||
{systemFields}
|
||||
{operators}
|
||||
{filter}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
/>
|
||||
|
||||
<form method="GET" on:submit={search}>
|
||||
<input type="search" name="filter[search_regex]" placeholder="Search"
|
||||
class="form-control" required>
|
||||
<input
|
||||
type="search"
|
||||
name="filter[search_regex]"
|
||||
placeholder="Search"
|
||||
class="search"
|
||||
required
|
||||
/>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center ">
|
||||
{#if schema.type === "collection"}
|
||||
{#if !inModal && isWritable}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
class="btn btn-sm btn-primary"
|
||||
>
|
||||
New Record
|
||||
</a>
|
||||
{/if}
|
||||
{:else }
|
||||
<div class="d-inline-block ms-1">
|
||||
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
||||
</div>
|
||||
<div style="display:flex;align-items: center;gap:4px">
|
||||
{#if !inModal && isWritable}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
class="button"
|
||||
>
|
||||
New Record
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if !inModal}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Icon icon="ellipsis-vertical"/>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
{#if filter["status_in"] === "trashed"}
|
||||
{#if 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>
|
||||
<Dropdown orientation="right">
|
||||
<div slot="button">
|
||||
<Icon icon="ellipsis-vertical" />
|
||||
</div>
|
||||
{#if filter["status_in"] === "trashed"}
|
||||
{#if isWritable}
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
||||
>
|
||||
Empty trash
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if Object.entries(filter).length > 0}
|
||||
<div class=" d-flex mb-3">
|
||||
<div class="applied-filters">
|
||||
<AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
|
||||
></AppliedFilterNotLinked>
|
||||
{#if Object.entries(filter).length > 0}
|
||||
{#each Object.entries(filter) as [k, v]}
|
||||
<AppliedFilter
|
||||
{schema}
|
||||
{operators}
|
||||
key={k}
|
||||
value={v}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
{graph}
|
||||
on:refresh
|
||||
{schema}
|
||||
{operators}
|
||||
key={k}
|
||||
value={v}
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
{graph}
|
||||
on:refresh
|
||||
/>
|
||||
{/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,102 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import FileIndex from "./FileIndex.svelte";
|
||||
import Dropdown from "../common/Dropdown.svelte";
|
||||
|
||||
let dialogEl;
|
||||
export let presetMode = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
$: files = [];
|
||||
$: selectedFiles = [];
|
||||
// onMount(() => {
|
||||
// load();
|
||||
// });
|
||||
|
||||
export function close(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
dialogEl.close();
|
||||
selectedFiles = [];
|
||||
}
|
||||
|
||||
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, preset) {
|
||||
e.preventDefault();
|
||||
dispatch("insert_files", { files: selectedFiles, preset: preset });
|
||||
}
|
||||
|
||||
function replace(e) {
|
||||
e.preventDefault();
|
||||
dispatch("replace_files", selectedFiles);
|
||||
}
|
||||
|
||||
export function open(recordId) {
|
||||
dialogEl.showModal();
|
||||
load(recordId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialogEl}>
|
||||
<div class="dialog-header">
|
||||
{#if presetMode}
|
||||
<Dropdown>
|
||||
<div slot="button">Insert Preset</div>
|
||||
{#each channel.imagePresets as preset}
|
||||
<button
|
||||
class=" dropdown-item button"
|
||||
on:click={(e) => insert(e, preset)}
|
||||
>{preset.name}</button
|
||||
>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={insert}
|
||||
disabled={selectedFiles.length === 0}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={replace}
|
||||
disabled={selectedFiles.length === 0}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
{/if}
|
||||
{#if selectedFiles.length > 0}
|
||||
<span class="">
|
||||
{selectedFiles.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={selectedFiles}></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,21 +1,29 @@
|
||||
export function sortByField(from, to, edges, fieldName) {
|
||||
if (from === to) {
|
||||
return edges;
|
||||
}
|
||||
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 ) ?? [];
|
||||
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
||||
export function sortByField(from, to, edges, fieldName, references) {
|
||||
if (from === to) {
|
||||
return edges;
|
||||
}
|
||||
let referenceIds = references.map((r) => r.id);
|
||||
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);
|
||||
return [...remainingEdge, ...edgesTosort];
|
||||
edgesTosort = array_move(edgesTosort, from, to);
|
||||
return [...remainingEdge, ...edgesTosort];
|
||||
}
|
||||
|
||||
function array_move(arr, old_index, new_index) {
|
||||
if (new_index >= arr.length) {
|
||||
var k = new_index - arr.length + 1;
|
||||
while (k--) {
|
||||
arr.push(undefined);
|
||||
}
|
||||
export function array_move(arr, old_index, new_index) {
|
||||
if (new_index >= arr.length) {
|
||||
var k = new_index - arr.length + 1;
|
||||
while (k--) {
|
||||
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,16 @@
|
||||
<script>
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import {imgurl} from "../files/imageserver";
|
||||
import {getContext} from "svelte";
|
||||
import { fileurl, imgurl } from "./imageserver.js";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let record;
|
||||
export let file;
|
||||
const channel = getContext("channel");
|
||||
export let size = "small";
|
||||
export let showFilename = false;
|
||||
let imageSide;
|
||||
let fileSide;
|
||||
let fontSize;
|
||||
|
||||
if (size == "large") {
|
||||
imageSide = 256;
|
||||
fileSide = 32;
|
||||
@@ -29,40 +30,49 @@
|
||||
}
|
||||
</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 "
|
||||
<div style="display: flex;align-items: center;gap: 5px;">
|
||||
{#if file}
|
||||
{#if file.mime.startsWith("image")}
|
||||
<!-- href={imgurl(record)} -->
|
||||
<a
|
||||
target="_blank"
|
||||
href={fileurl(channel, file)}
|
||||
title={file.filename}
|
||||
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>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if showFilename}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
class="preview-file-filename lx-small-text text-decoration-none"
|
||||
>{record._file.path}</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
img {
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
export let recordId;
|
||||
let mimeTypes = "";
|
||||
let files = [];
|
||||
let isLoading = false;
|
||||
@@ -17,42 +18,42 @@
|
||||
files = e.target.files ? [...e.target.files] : [];
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append("schema", schema.name);
|
||||
formData.append("recordId", recordId);
|
||||
Array.from(files).forEach(function (file) {
|
||||
formData.append("files[]", file);
|
||||
});
|
||||
dispatch("beforeUpload", files);
|
||||
axios
|
||||
.post(channel.lucentUrl + "/files/upload", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.error) {
|
||||
dispatch("uploadError", response.data.error);
|
||||
fetch(channel.lucentUrl + "/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector(
|
||||
'meta[name="csrf-token"]',
|
||||
).content,
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
dispatch("uploadError", data.error);
|
||||
} else {
|
||||
dispatch("uploadComplete", response.data);
|
||||
dispatch("uploadComplete", data);
|
||||
}
|
||||
isLoading = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
isLoading = false;
|
||||
console.log(error.response.data);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset disabled={isLoading}>
|
||||
<label class="btn btn-primary btn-sm btn-spinner ">
|
||||
Upload file
|
||||
<fieldset class="upload-button" disabled={isLoading}>
|
||||
<label class="button primary btn-spinner">
|
||||
<Icon icon="upload"></Icon>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span>
|
||||
/>
|
||||
Upload file
|
||||
|
||||
<input
|
||||
on:input={upload}
|
||||
class="form-control"
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import {getContext} from "svelte";
|
||||
export function imgurl(channel, file) {
|
||||
if (file.mime === "image/svg+xml") {
|
||||
return fileurl(channel, file);
|
||||
}
|
||||
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
|
||||
return channel.filesUrl + `/thumbs/${webpPath}`;
|
||||
}
|
||||
|
||||
export function imgurl(record) {
|
||||
export function presetUrl(channel, file, preset) {
|
||||
if (file.mime === "image/svg+xml") {
|
||||
return fileurl(channel, file);
|
||||
}
|
||||
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
|
||||
return channel.filesUrl + `/templates/${preset}/${webpPath}`;
|
||||
}
|
||||
|
||||
if(record._file.mime === "image/svg+xml"){
|
||||
return fileurl(record);
|
||||
export function fileurl(channel, file) {
|
||||
return channel.filesUrl + `/${file.path}`;
|
||||
}
|
||||
|
||||
export function htmlurl(channel, file, preset) {
|
||||
let html = "";
|
||||
let url = fileurl(channel, file);
|
||||
|
||||
if (file.width > 0) {
|
||||
let presetUrl = url;
|
||||
if (preset) {
|
||||
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
|
||||
presetUrl = channel.filesUrl + `/templates/${preset}/${webpPath}`;
|
||||
}
|
||||
const channel = getContext("channel")
|
||||
return channel.filesUrl + `/thumbs/${record._file.path}`;
|
||||
}
|
||||
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
||||
} else if (file.mime === "image/svg+xml") {
|
||||
html = `<img src="${url}" alt="${file.path}"/>`;
|
||||
} else {
|
||||
html = `<a href="${url}">${file.filename}</a>`;
|
||||
}
|
||||
|
||||
export function fileurl(record) {
|
||||
const channel = getContext("channel")
|
||||
return channel.filesUrl + `/${record._file.path}`;
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
import {getContext, onMount} from "svelte";
|
||||
import RecordRow from "./RecordRow.svelte"
|
||||
import { getContext, onMount } from "svelte";
|
||||
import RecordRow from "./RecordRow.svelte";
|
||||
import { apiGet } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
let records = [];
|
||||
let graph = null;
|
||||
let users = [];
|
||||
onMount(() => {
|
||||
axios
|
||||
.get(channel.lucentUrl + "/home/records")
|
||||
.then((response) => {
|
||||
records = response.data.records;
|
||||
graph = response.data.graph;
|
||||
users = response.data.users;
|
||||
apiGet(channel.lucentUrl + "/home/records")
|
||||
.then((data) => {
|
||||
records = data.records;
|
||||
users = data.users;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
@@ -21,24 +19,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="wrapper-normal transparent">
|
||||
|
||||
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
||||
{#if records.length > 0}
|
||||
<div class="lx-card mb-4">
|
||||
<div class="lx-table p-0">
|
||||
<table class="">
|
||||
<tbody>
|
||||
{#each records as record (record.id)}
|
||||
<tr>
|
||||
<RecordRow {graph} {record} {users}/>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
||||
{#if records.length > 0}
|
||||
<div class="table">
|
||||
<table class="">
|
||||
<tbody>
|
||||
{#each records as record (record.id)}
|
||||
<tr>
|
||||
<RecordRow {record} {users} />
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,48 +1,38 @@
|
||||
<script>
|
||||
import {formatDistanceToNow, parseJSON} from "date-fns";
|
||||
import { formatDistanceToNow, parseJSON } from "date-fns";
|
||||
import Avatar from "../account/Avatar.svelte";
|
||||
import Status from "../records/Status.svelte";
|
||||
import {previewTitle} from "../records/Preview";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import {usernameById} from "../account/users";
|
||||
import {getContext} from "svelte";
|
||||
import { usernameById } from "../account/users";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let users;
|
||||
export let graph;
|
||||
export let record;
|
||||
let schema = channel.schemas.find((s) => s.name === record.schema);
|
||||
let frieldlyUpdatedAt = formatDistanceToNow(
|
||||
parseJSON(record._sys.updatedAt),
|
||||
{addSuffix: true}
|
||||
);
|
||||
let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.updatedAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<td>
|
||||
{#if schema.type === "files"}
|
||||
<Preview {record} size="tiny"/>
|
||||
{:else}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
class="text-decoration-none text-dark d-block"
|
||||
>
|
||||
{previewTitle(channel.schemas, record, graph)}
|
||||
</a>
|
||||
{/if}
|
||||
<div class="row-name">
|
||||
{#if record.status === "draft"}
|
||||
<span class="status">DRAFT</span>
|
||||
{/if}
|
||||
{#if schema.type === "files"}
|
||||
<Preview {record} size="tiny" showFilename={true} />
|
||||
{:else}
|
||||
<a href="{channel.lucentUrl}/records/{record.id}">
|
||||
{record.data.name}
|
||||
</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>
|
||||
<div class="d-flex">
|
||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||
<div style="display: flex;gap: 14px">
|
||||
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||
<div class="ms-2">
|
||||
{frieldlyUpdatedAt}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<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>
|
||||
{:else}
|
||||
<a href="/lunar">Store admin</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,48 @@
|
||||
<script>
|
||||
|
||||
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
||||
import {onMount, onDestroy} from "svelte";
|
||||
import {basicSetup, EditorView} from "codemirror";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { basicSetup, EditorView } from "codemirror";
|
||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import {EditorState, Compartment} from "@codemirror/state";
|
||||
import {keymap} from "@codemirror/view";
|
||||
import {indentWithTab} from "@codemirror/commands";
|
||||
import {markdown} from "@codemirror/lang-markdown";
|
||||
import {lintKeymap} from "@codemirror/lint";
|
||||
import { Compartment, EditorState } from "@codemirror/state";
|
||||
import { keymap } from "@codemirror/view";
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { lintKeymap } from "@codemirror/lint";
|
||||
import { fileurl, presetUrl } from "../files/imageserver";
|
||||
|
||||
let parentElement;
|
||||
let codeMirrorView;
|
||||
export let value;
|
||||
export let editable = true;
|
||||
|
||||
export function insertMedia(channel, files, presetPath) {
|
||||
const insertText = files.reduce((text, aFile) => {
|
||||
const url =
|
||||
aFile.width > 0
|
||||
? presetUrl(channel, aFile, presetPath)
|
||||
: fileurl(channel, aFile);
|
||||
|
||||
let addTest = ``;
|
||||
|
||||
return text + "\n" + addTest;
|
||||
}, "");
|
||||
|
||||
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(() => {
|
||||
let language = new Compartment();
|
||||
let tabSize = new Compartment();
|
||||
@@ -23,11 +51,7 @@
|
||||
doc: value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of([
|
||||
indentWithTab,
|
||||
...lintKeymap,
|
||||
...completionKeymap
|
||||
]),
|
||||
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
||||
language.of(markdown()),
|
||||
markdown(),
|
||||
autocompletion(),
|
||||
@@ -40,18 +64,14 @@
|
||||
}
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({spellcheck: "true"})
|
||||
EditorView.contentAttributes.of({ spellcheck: "true" }),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
codeMirrorView = new EditorView({
|
||||
state,
|
||||
parent: parentElement,
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -61,4 +81,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="is-editable-{editable}" bind:this={parentElement}/>
|
||||
<div class="is-editable-{editable}" bind:this={parentElement} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import Sortable from "sortablejs";
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import {createEventDispatcher, onMount} from "svelte";
|
||||
|
||||
export let sortableClass = "";
|
||||
// export let handle;
|
||||
export let isTable = false;
|
||||
@@ -10,50 +11,29 @@
|
||||
|
||||
onMount(() => {
|
||||
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
|
||||
easing: "cubic-bezier(1, 0, 0, 1)",
|
||||
direction: 'vertical',
|
||||
onUpdate: function (/**Event*/ evt) {
|
||||
// reorder(evt.oldIndex,evt.newIndex);
|
||||
console.log(evt)
|
||||
dispatch("update", {
|
||||
source: evt.oldIndex,
|
||||
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);
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
{#if isTable}
|
||||
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||
<slot />
|
||||
</tbody>
|
||||
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||
<slot/>
|
||||
</tbody>
|
||||
{:else}
|
||||
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||
<slot/>
|
||||
</div>
|
||||
{/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,67 @@
|
||||
<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(html) {
|
||||
console.log({ html });
|
||||
var attachment = new Trix.Attachment({ content: html });
|
||||
editor.editor.insertAttachment(attachment);
|
||||
// if (info.file.width > 0) {
|
||||
// var attachment = new Trix.Attachment({ content: html });
|
||||
// editor.editor.insertAttachment(attachment);
|
||||
// } else {
|
||||
// editor.editor.insertHTML(html);
|
||||
// }
|
||||
}
|
||||
|
||||
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 {fly} from "svelte/transition";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import Dropdown from "../common/Dropdown.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let member;
|
||||
@@ -35,61 +36,50 @@
|
||||
|
||||
<div
|
||||
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}/>
|
||||
<div class="ms-3 ">
|
||||
<div>
|
||||
<div>
|
||||
<span class="fs-5">
|
||||
{member.name}
|
||||
</span>
|
||||
{member.name}
|
||||
</div>
|
||||
{member.email}
|
||||
</div>
|
||||
</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>
|
||||
{member.email}
|
||||
</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>
|
||||
<style>
|
||||
.status-removed {
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import Radio from "../forms/Radio.svelte";
|
||||
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
||||
import {getContext} from "svelte";
|
||||
|
||||
import { getContext } from "svelte";
|
||||
import { apiPost } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let users;
|
||||
@@ -23,15 +22,14 @@
|
||||
function invite(newName, newEmail, newRole) {
|
||||
errorMessage = "";
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/members/invite", {
|
||||
name: newName,
|
||||
email: newEmail,
|
||||
roles: [newRole],
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/members/invite", {
|
||||
name: newName,
|
||||
email: newEmail,
|
||||
roles: [newRole],
|
||||
})
|
||||
.then((response) => {
|
||||
successAlert.show("User was invited");
|
||||
users = [...users, response.data.user];
|
||||
users = [...users, response.user];
|
||||
name = null;
|
||||
email = null;
|
||||
role = null;
|
||||
@@ -45,14 +43,13 @@
|
||||
e.preventDefault();
|
||||
errorMessage = "";
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/members/update", {
|
||||
id: e.detail.user,
|
||||
roles: e.detail.roles,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/members/update", {
|
||||
id: e.detail.user,
|
||||
roles: e.detail.roles,
|
||||
})
|
||||
.then((response) => {
|
||||
successAlert.show("Users updated");
|
||||
users = response.data.users;
|
||||
users = response.users;
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage = error.response?.data?.error ?? "";
|
||||
@@ -60,17 +57,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper-tiny transparent mb-5">
|
||||
<div class="common-wrapper">
|
||||
<div class="lx-card mt-5">
|
||||
<h3 class="header-small mb-5">Invite people</h3>
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<SuccessAlert bind:this={successAlert}/>
|
||||
<ErrorAlert message={errorMessage} />
|
||||
<SuccessAlert bind:this={successAlert} />
|
||||
|
||||
<form on:submit={submitInvite}>
|
||||
<div class="mb-3">
|
||||
<label for="inviteeName" class="form-label"
|
||||
>Invitee Name</label
|
||||
>
|
||||
<label for="inviteeName" class="form-label">Invitee Name</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
@@ -82,7 +77,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inviteeEmail" class="form-label"
|
||||
>Invitee Email Address</label
|
||||
>Invitee Email Address</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
@@ -95,24 +90,21 @@
|
||||
</div>
|
||||
|
||||
<div class="me-3">
|
||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||
<Radio
|
||||
bind:group={role}
|
||||
value={arole}
|
||||
name="role"
|
||||
label={arole}
|
||||
/>
|
||||
{/each}
|
||||
<select bind:value={role}>
|
||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||
<option value={arole}>{arole}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-block text-center">
|
||||
<SpinnerButton label="Invite"/>
|
||||
<SpinnerButton label="Invite" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="lx-card mt-3">
|
||||
<h3 class="header-small mb-5">Members</h3>
|
||||
<div class="member-list">
|
||||
<h3 class="header-small mb-5 mt-5">Members</h3>
|
||||
{#each users as user}
|
||||
<MemberSettingsCard
|
||||
member={user}
|
||||
@@ -121,7 +113,5 @@
|
||||
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script>
|
||||
import {afterUpdate, getContext, onMount} from "svelte";
|
||||
import {isEqual} from "lodash";
|
||||
import Manager from "./Manager.svelte";
|
||||
import EditHeader from "./EditHeader.svelte"
|
||||
import StatusSelect from "./StatusSelect.svelte"
|
||||
import FilePreview from "./FilePreview.svelte"
|
||||
import ContentTabs from "./ContentTabs.svelte"
|
||||
import FormField from "./FormField.svelte"
|
||||
import Graph from "./Graph.svelte"
|
||||
import Info from "./Info.svelte"
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte"
|
||||
import { afterUpdate, getContext, onMount } from "svelte";
|
||||
import EditHeader from "./header/EditHeader.svelte";
|
||||
import ContentTabs from "./header/ContentTabs.svelte";
|
||||
import FormField from "./FormField.svelte";
|
||||
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");
|
||||
|
||||
@@ -17,11 +15,11 @@
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
edges: []
|
||||
edges: [],
|
||||
};
|
||||
export let recordHistory;
|
||||
// export let recordHistory;
|
||||
export let isCreateMode;
|
||||
export let isWritable = false;
|
||||
// export let isWritable = false;
|
||||
export let users;
|
||||
let originalContent;
|
||||
let activeContentTab = "";
|
||||
@@ -29,14 +27,11 @@
|
||||
$: validationErrors = null;
|
||||
$: errorMessage = validationErrors
|
||||
? `Record submission failed. ${
|
||||
Object.entries(validationErrors).length
|
||||
} error(s)`
|
||||
Object.entries(validationErrors).length
|
||||
} error(s)`
|
||||
: null;
|
||||
|
||||
let activeFields = schema.fields.filter(
|
||||
(f) => f.name !== "id"
|
||||
);
|
||||
|
||||
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||
|
||||
onMount(() => {
|
||||
setOriginalContent();
|
||||
@@ -45,10 +40,7 @@
|
||||
function setOriginalContent() {
|
||||
originalContent = {
|
||||
data: JSON.parse(JSON.stringify(record.data)),
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
||||
_file: JSON.parse(JSON.stringify(record._file)),
|
||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||
};
|
||||
}
|
||||
@@ -77,12 +69,10 @@
|
||||
if (isCreateMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isEqual(originalContent, {
|
||||
data: record.data,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: record._sys,
|
||||
_file: record._file,
|
||||
edges: graph.edges,
|
||||
});
|
||||
}
|
||||
@@ -104,27 +94,29 @@
|
||||
}
|
||||
|
||||
// remove trashed edges
|
||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
||||
axios
|
||||
.post(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
isCreateMode: isCreateMode,
|
||||
})
|
||||
graph.edges = graph.edges?.filter(
|
||||
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||
);
|
||||
apiPost(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
isCreateMode: isCreateMode,
|
||||
})
|
||||
.then(function (response) {
|
||||
console.log("SAVE: SAVED");
|
||||
|
||||
if (isCreateMode) {
|
||||
window.location = channel.lucentUrl + "/records/" + record.id;
|
||||
window.location =
|
||||
channel.lucentUrl + "/records/" + record.id;
|
||||
} else {
|
||||
record = response.data.records[0] ?? null;
|
||||
record = response.records[0] ?? null;
|
||||
if (!record) {
|
||||
// means trashed
|
||||
hasUnsavedData = false;
|
||||
window.location = channel.lucentUrl;
|
||||
return;
|
||||
}
|
||||
graph = response.data;
|
||||
graph = response;
|
||||
setOriginalContent();
|
||||
}
|
||||
|
||||
@@ -137,7 +129,7 @@
|
||||
errorMessage = error.response.data.error;
|
||||
} else {
|
||||
validationErrors = error.response.data.error;
|
||||
console.log(validationErrors)
|
||||
console.log(validationErrors);
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
@@ -149,79 +141,60 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={beforeUnload}/>
|
||||
<svelte:window on:beforeunload={beforeUnload} />
|
||||
|
||||
<div class="wrapper-normal transparent">
|
||||
<Manager managerRecords={recordHistory} {graph}/>
|
||||
<EditHeader {schema} {record} {isCreateMode} {graph} bind:activeContentTab/>
|
||||
|
||||
{#if !["_graph", "_info"].includes(activeContentTab) && isWritable}
|
||||
<div class="shadow-lg "
|
||||
style="position:fixed;bottom:0;left:0px;width:100%;background: rgb(206, 223, 210);z-index:1050"
|
||||
>
|
||||
<div
|
||||
class="d-flex mt-3 mb-3 align-items-center justify-content-center"
|
||||
<div class="record-edit">
|
||||
<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}
|
||||
>
|
||||
<StatusSelect bind:status={record.status} {record} {schema}/>
|
||||
{#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"
|
||||
/>
|
||||
Create
|
||||
</button>
|
||||
{:else if hasUnsavedData}
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<Title {schema} {record} {isCreateMode} />
|
||||
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<ErrorAlert message={errorMessage} />
|
||||
|
||||
<div class=" mt-4" style="margin-bottom:150px">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
/>
|
||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||
<FilePreview {record} {schema}/>
|
||||
<!-- <fieldset disabled="disabled"> -->
|
||||
{#each activeFields as field (field.name)}
|
||||
{#if activeContentTab === field.group}
|
||||
<FormField
|
||||
bind:data={record.data}
|
||||
bind:graph={graph}
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:data={record.data}
|
||||
bind:graph
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- </fieldset> -->
|
||||
{:else if activeContentTab === "_graph"}
|
||||
<Graph {graph} {record}/>
|
||||
<Graph {graph} {record} />
|
||||
{:else if activeContentTab === "_info"}
|
||||
<Info {record} {graph} {users} {schema}/>
|
||||
<Info {record} {graph} {users} {schema} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import {previewTitle} from "./Preview";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
export let graph;
|
||||
export let record;
|
||||
export let isCreateMode;
|
||||
export let activeContentTab;
|
||||
|
||||
function clone(e) {
|
||||
e.preventDefault();
|
||||
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
|
||||
window.location = channel.lucentUrl + "/records/" + response.data.id;
|
||||
}).catch(error => {
|
||||
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="header-normal mt-5 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}
|
||||
{previewTitle(channel.schemas, record, graph)}
|
||||
{:else}
|
||||
New Record
|
||||
{/if}
|
||||
</span>
|
||||
{#if !isCreateMode}
|
||||
<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>
|
||||
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 schema;
|
||||
</script>
|
||||
|
||||
{#if schema.type === "files"}
|
||||
<div class="row mb-4">
|
||||
<div class="col" style="max-width:276px">
|
||||
<Preview {record} size="large"/>
|
||||
<div class="record-edit-file-preview">
|
||||
<div>
|
||||
<Preview {record} size="large" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<ul class="list-group ">
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Filename</span>
|
||||
<span>{record._file.path}</span>
|
||||
</li>
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Original name</span>
|
||||
<span>{record._file.originalName}</span>
|
||||
</li>
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Mime type</span>
|
||||
<span>{record._file.mime}</span>
|
||||
</li>
|
||||
{#if record._file.width}
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Dimensions</span>
|
||||
<span>{record._file.width}x{record._file.height}</span>
|
||||
</li>
|
||||
{/if}
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">File size</span>
|
||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
||||
</li>
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Checksum</span>
|
||||
<span>{record._file.checksum}</span>
|
||||
</li>
|
||||
<li class="list-group-item border-primary">
|
||||
<span class="text-muted">Download</span>
|
||||
<a href="{fileurl(record)}">{record._file.path}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="file-details">
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Filename</span>
|
||||
<span>{record._file.path}</span>
|
||||
</div>
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Original name</span>
|
||||
<span>{record._file.originalName}</span>
|
||||
</div>
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Mime type</span>
|
||||
<span>{record._file.mime}</span>
|
||||
</div>
|
||||
{#if record._file.width}
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Dimensions</span>
|
||||
<span>{record._file.width}x{record._file.height}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">File size</span>
|
||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
||||
</div>
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Checksum</span>
|
||||
<span>{record._file.checksum}</span>
|
||||
</div>
|
||||
<div class="file-details-item">
|
||||
<a
|
||||
class="button primary"
|
||||
target="_blank"
|
||||
style="display: inline-flex"
|
||||
href={fileurl(channel, record)}>Download</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.list-group {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import Text from "./elements/Text.svelte";
|
||||
import Slug from "./elements/Slug.svelte";
|
||||
import Reference from "./elements/Reference.svelte";
|
||||
import ReferenceInline from "./elements/ReferenceInline.svelte";
|
||||
import Block from "./block/Block.svelte";
|
||||
import Color from "./elements/Color.svelte";
|
||||
import Checkbox from "./elements/Checkbox.svelte";
|
||||
import Number from "./elements/Number.svelte";
|
||||
@@ -17,7 +15,6 @@
|
||||
import Json from "./elements/JSON.svelte";
|
||||
import Markdown from "./elements/Markdown.svelte";
|
||||
import FieldHeader from "./elements/FieldHeader.svelte";
|
||||
import ReferenceTable from "./elements/ReferenceTable.svelte";
|
||||
import ReferenceTags from "./elements/ReferenceTags.svelte";
|
||||
|
||||
const formElements = {
|
||||
@@ -47,44 +44,15 @@
|
||||
const id = `field-${field.name}-${record.id}`;
|
||||
</script>
|
||||
|
||||
<div class="card editor-field">
|
||||
<FieldHeader {schema} {field} {id}/>
|
||||
{#if field.info.name === "reference" && field.layout === "inline"}
|
||||
<ReferenceInline
|
||||
bind:graph
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
{: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}
|
||||
/>
|
||||
<div class="editor-field">
|
||||
<FieldHeader {field} {id} />
|
||||
{#if field.info.name === "reference" && field.layout === "tags"}
|
||||
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
||||
{:else if field.info.name === "reference"}
|
||||
<Reference
|
||||
bind:graph
|
||||
{id}
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
||||
{:else if field.info.name === "file"}
|
||||
<File bind:graph {record} {field} {validationErrors}/>
|
||||
{:else if field.info.name === "block"}
|
||||
<Block
|
||||
bind:graph
|
||||
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
|
||||
<File
|
||||
bind:value={data[field.name]}
|
||||
{record}
|
||||
{id}
|
||||
@@ -101,11 +69,11 @@
|
||||
/>
|
||||
{:else if field.info.name === "slug"}
|
||||
<Slug
|
||||
bind:value={data[field.name]}
|
||||
{field}
|
||||
{id}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:value={data[field.name]}
|
||||
{field}
|
||||
{id}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
/>
|
||||
{:else if field.info.name === "textarea"}
|
||||
<Textarea
|
||||
@@ -115,6 +83,23 @@
|
||||
{isCreateMode}
|
||||
{id}
|
||||
/>
|
||||
{:else if field.info.name === "rich"}
|
||||
<RichEditor
|
||||
bind:value={data[field.name]}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{record}
|
||||
/>
|
||||
{:else if field.info.name === "markdown"}
|
||||
<Markdown
|
||||
bind:value={data[field.name]}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:graph
|
||||
{record}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={formElement}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<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 {uniqBy} from "lodash";
|
||||
import PreviewReference from "./previews/PreviewReference.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let graph;
|
||||
export let record;
|
||||
|
||||
function findEdgeField(schema, edgeField){
|
||||
if(edgeField.includes(":")){
|
||||
let edgeFieldAr = edgeField.split(":");
|
||||
@@ -18,121 +12,35 @@
|
||||
return schema.fields.find((f) => f.name === edgeField);
|
||||
}
|
||||
|
||||
let parentEdgesByField = graph.parentEdges
|
||||
.filter((edge) => edge.source !== record.id && edge.depth === 1)
|
||||
.reduce((carry, edge) => {
|
||||
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||
let edgeField = findEdgeField(schema,edge.field);
|
||||
let schemaField = edge.sourceSchema + edgeField;
|
||||
|
||||
let arecord = graph.records.find((n) => {
|
||||
return n.id === edge.source;
|
||||
});
|
||||
if (!carry[schemaField]) {
|
||||
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;
|
||||
}, {});
|
||||
let backlinks = graph.parentEdges.map(edge => {
|
||||
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||
let edgeField = findEdgeField(schema,edge.field);
|
||||
if(!edgeField){
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
field: edgeField.label,
|
||||
record: graph.records.find( record => record.id === edge.source)
|
||||
}
|
||||
}).filter( edgeOrNull => !!edgeOrNull)
|
||||
</script>
|
||||
<div class="editor-field">
|
||||
{#each backlinks as backlink}
|
||||
<div style="margin: 0 0 15px;position: relative;">
|
||||
<span style="
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
|
||||
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
|
||||
<div class="lx-card mt-3">
|
||||
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
|
||||
<span>{fieldData.schema.label}</span>
|
||||
<Icon icon="angle-right" width="12" height="12"/>
|
||||
<span>{fieldData.field.label}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||
{#each fieldData.nodes as node}
|
||||
{#if node._file?.path}
|
||||
<div class="ms-2 mb-2" style="max-height:64px;">
|
||||
<Preview record={node} size="small"/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ms-2 mb-2">
|
||||
<PreviewCardSmall {graph} record={node}/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
|
||||
"
|
||||
>In <i>{backlink.field}</i> of</span>
|
||||
<PreviewReference
|
||||
record={backlink.record}
|
||||
hasDelete={false}
|
||||
{graph}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
Nothing links to this record
|
||||
{/each}
|
||||
{#if Object.entries(parentEdgesByField).length > 0}
|
||||
<div class="text-center my-4">
|
||||
<Icon icon="angles-down" width="32" height="32"/>
|
||||
</div>
|
||||
{/if}
|
||||
<div style="max-width:400px;margin:0 auto;">
|
||||
<PreviewCard {graph} record={record}/>
|
||||
</div>
|
||||
{#if Object.entries(childrenEdgesByField).length > 0}
|
||||
<div class="text-center my-4">
|
||||
<Icon icon="angles-down" width="32" height="32"/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
|
||||
<div class="lx-card mt-3">
|
||||
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
|
||||
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||
{#each fieldData.nodes as node}
|
||||
{#if fieldData.field.info.ui === "file"}
|
||||
<div
|
||||
class="ms-2 mb-2"
|
||||
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
|
||||
>
|
||||
<Preview
|
||||
record={node}
|
||||
size="small"
|
||||
showFilename={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ms-2 mb-2">
|
||||
<PreviewCardSmall {graph} record={node}/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
</style>
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import {friendlyDate} from "../../helpers";
|
||||
import { friendlyDate, isEqual } from "../../helpers";
|
||||
import Avatar from "../account/Avatar.svelte";
|
||||
import {usernameById} from "../account/users";
|
||||
import {isEqual, sortBy} from "lodash";
|
||||
import { usernameById } from "../account/users";
|
||||
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import RevisionCell from "./revisions/RevisionCell.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
@@ -30,27 +30,27 @@
|
||||
});
|
||||
|
||||
function getEdgesByField(fieldsWithDiff, revision) {
|
||||
|
||||
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
|
||||
if (!c[e.field]) {
|
||||
c[e.field] = {
|
||||
record: [],
|
||||
revision: [],
|
||||
edgeFieldsDiff = graph.edges
|
||||
.filter((e) => e.depth === 1)
|
||||
.reduce((c, e) => {
|
||||
if (!c[e.field]) {
|
||||
c[e.field] = {
|
||||
record: [],
|
||||
revision: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
c[e.field]["record"].push(e)
|
||||
return c;
|
||||
}, {});
|
||||
|
||||
c[e.field]["record"].push(e);
|
||||
return c;
|
||||
}, {});
|
||||
|
||||
edgeFieldsDiff = revision._edges.reduce((c, e) => {
|
||||
if (!c[e.field]) {
|
||||
c[e.field] = {
|
||||
record: [],
|
||||
revision: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
c[e.field]["revision"].push(e)
|
||||
c[e.field]["revision"].push(e);
|
||||
return c;
|
||||
}, edgeFieldsDiff);
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
fieldsWithDiff = schema.fields.filter((f) => {
|
||||
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
|
||||
});
|
||||
getEdgesByField(fieldsWithDiff, revision)
|
||||
getEdgesByField(fieldsWithDiff, revision);
|
||||
revisionSection.scrollIntoView();
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
rollbackError = "";
|
||||
axios
|
||||
.post(
|
||||
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
|
||||
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
|
||||
)
|
||||
.then((response) => {
|
||||
window.location.reload();
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lx-card ">
|
||||
<div class="lx-card">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div>
|
||||
@@ -93,29 +93,27 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="label text-end text-muted">current version </span>
|
||||
{record._sys.version}
|
||||
{record.version}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label text-end text-muted"> created </span>
|
||||
<Avatar
|
||||
name={usernameById(users, record._sys.createdBy)}
|
||||
side={24}
|
||||
name={usernameById(users, record.createdBy)}
|
||||
side={24}
|
||||
/>
|
||||
{friendlyDate(record._sys.createdAt)}
|
||||
{friendlyDate(record.createdAt)}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label text-end text-muted">updated </span>
|
||||
<span class="label text-end text-muted">updated </span>
|
||||
<Avatar
|
||||
name={usernameById(users, record._sys.updatedBy)}
|
||||
side={24}
|
||||
name={usernameById(users, record.updatedBy)}
|
||||
side={24}
|
||||
/>
|
||||
{friendlyDate(record._sys.updatedAt)}
|
||||
{friendlyDate(record.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="label d-block text-muted "
|
||||
>Rules for this schema
|
||||
</span>
|
||||
<span class="label d-block text-muted">Rules for this schema </span>
|
||||
<small>
|
||||
Each record maintains the last {schema.revisions}
|
||||
versions
|
||||
@@ -123,35 +121,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lx-card mt-4">
|
||||
<div class="revisions">
|
||||
{#if schema.revisions > 0}
|
||||
<div class="header-small mb-3">Revisions</div>
|
||||
<div class="header-small mb-3">Revisions</div>
|
||||
{#each revisions as revision}
|
||||
{#if revision._sys.version != record._sys.version}
|
||||
{#if revision.version !== record.version}
|
||||
<div
|
||||
class="row p-2 rounded"
|
||||
class:active={revision._sys.version ===
|
||||
selectedRevision?._sys.version}
|
||||
class="revision"
|
||||
class:active={revision.version ===
|
||||
selectedRevision?.version}
|
||||
>
|
||||
|
||||
<div class="col-2">version {revision._sys.version}</div>
|
||||
<div class="col-5">
|
||||
<div class="version">
|
||||
<span>version {revision.version}</span>
|
||||
<Avatar
|
||||
name={usernameById(users, revision._sys.updatedBy)}
|
||||
side={24}
|
||||
name={usernameById(users, revision.updatedBy)}
|
||||
side={24}
|
||||
/>
|
||||
{friendlyDate(revision._sys.updatedAt)}
|
||||
{friendlyDate(revision.updatedAt)}
|
||||
</div>
|
||||
|
||||
<div class="col-3 text-center">
|
||||
<button
|
||||
disabled={revision._sys.version ===
|
||||
selectedRevision?._sys.version}
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
on:click={(e) => compare(e, revision)}
|
||||
>Compare
|
||||
</button
|
||||
>
|
||||
disabled={revision.version ===
|
||||
selectedRevision?.version}
|
||||
class="button"
|
||||
on:click={(e) => compare(e, revision)}
|
||||
>Compare
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -164,20 +160,18 @@
|
||||
</div>
|
||||
<div bind:this={revisionSection}>
|
||||
{#if selectedRevision}
|
||||
<div class="mt-4">
|
||||
<div class="selected-revision">
|
||||
{#if fieldsWithDiff.length > 0}
|
||||
<p class="text-center fw-bold mb-3 mt-5">
|
||||
If you choose to rollback to this revision
|
||||
</p>
|
||||
<button
|
||||
on:click={rollback}
|
||||
class="btn btn-primary mb-5 d-block mx-auto"
|
||||
>
|
||||
Rollback to version {selectedRevision._sys.version}
|
||||
<button on:click={rollback} class="button">
|
||||
Rollback to version {selectedRevision.version}
|
||||
</button>
|
||||
|
||||
{#if rollbackError}
|
||||
<span class="d-block text-danger mt-3">{rollbackError}</span>
|
||||
<span class="d-block text-danger mt-3">{rollbackError}</span
|
||||
>
|
||||
{/if}
|
||||
<div class="mt-3">
|
||||
{#each fieldsWithDiff as field}
|
||||
@@ -188,35 +182,28 @@
|
||||
<!-- <div class="d-block" style="width:200px;">
|
||||
{field.label}
|
||||
</div> -->
|
||||
<div
|
||||
class="lx-card row p-4 mb-4 w-100"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<div class="col-5">
|
||||
<div class="revision-field" style="overflow:hidden">
|
||||
<div class="compare-left">
|
||||
<RevisionCell
|
||||
{field}
|
||||
side={record.data[field.name]}
|
||||
colorClass="text-danger"
|
||||
{field}
|
||||
side={record.data[field.name]}
|
||||
colorClass="text-danger"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div
|
||||
class="h-100 d-flex align-items-center justify-content-center text-secondary"
|
||||
>
|
||||
<span class="me-1">{field.label}</span>
|
||||
<Icon
|
||||
icon="angle-right"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
</div>
|
||||
<div class="compare-center">
|
||||
<span class="me-1">{field.label}</span>
|
||||
<Icon
|
||||
icon="angle-right"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<div class="compare-right">
|
||||
<RevisionCell
|
||||
edges={selectedRevision._edges}
|
||||
{field}
|
||||
side={selectedRevision.data[field.name]}
|
||||
colorClass="text-success"
|
||||
edges={selectedRevision._edges}
|
||||
{field}
|
||||
side={selectedRevision.data[field.name]}
|
||||
colorClass="text-success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,25 +217,22 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-3">
|
||||
<p class="text-center fw-bold mb-3 mt-5">
|
||||
Record References
|
||||
</p>
|
||||
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
|
||||
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
||||
<div
|
||||
class="lx-card row p-4 mb-4 w-100"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="revision-references" style="overflow:hidden">
|
||||
<div class="reference-field">
|
||||
{field}:
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<p class="mb-2 text-danger">Record</p>
|
||||
<div class="reference-compare">
|
||||
<p class="">Record</p>
|
||||
{#each edges.record as edge}
|
||||
<RevisionEdgeRow {edge} />
|
||||
{:else}
|
||||
<p>No references</p>
|
||||
{/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}
|
||||
<RevisionEdgeRow {edge} />
|
||||
{:else}
|
||||
@@ -259,21 +243,5 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</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>
|
||||
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
|
||||
import {
|
||||
afterUpdate,
|
||||
createEventDispatcher,
|
||||
getContext,
|
||||
onMount,
|
||||
} from "svelte";
|
||||
|
||||
import {isEqual} from "lodash";
|
||||
import FormField from "./FormField.svelte";
|
||||
import FilePreview from "./FilePreview.svelte";
|
||||
import ContentTabs from "./ContentTabs.svelte";
|
||||
import StatusSelect from "./StatusSelect.svelte";
|
||||
import ContentTabs from "./header/ContentTabs.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 dispatch = createEventDispatcher();
|
||||
@@ -14,7 +20,7 @@
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
edges: []
|
||||
edges: [],
|
||||
};
|
||||
export let isCreateMode;
|
||||
let originalContent;
|
||||
@@ -23,13 +29,11 @@
|
||||
$: validationErrors = null;
|
||||
$: errorMessage = validationErrors
|
||||
? `Record submission failed. ${
|
||||
Object.entries(validationErrors).length
|
||||
} error(s)`
|
||||
Object.entries(validationErrors).length
|
||||
} error(s)`
|
||||
: null;
|
||||
|
||||
let activeFields = schema.fields.filter(
|
||||
(f) => f.name !== "id"
|
||||
);
|
||||
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||
|
||||
let tabname = "_default";
|
||||
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||
@@ -51,8 +55,6 @@
|
||||
data: JSON.parse(JSON.stringify(record.data)),
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
||||
_file: JSON.parse(JSON.stringify(record._file)),
|
||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||
};
|
||||
}
|
||||
@@ -85,8 +87,6 @@
|
||||
data: record.data,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: record._sys,
|
||||
_file: record._file,
|
||||
edges: graph.edges,
|
||||
});
|
||||
}
|
||||
@@ -112,20 +112,21 @@
|
||||
return;
|
||||
}
|
||||
// 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,
|
||||
) ?? [];
|
||||
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
isCreateMode: isCreateMode,
|
||||
})
|
||||
apiPost(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
isCreateMode: isCreateMode,
|
||||
})
|
||||
.then(function (response) {
|
||||
console.log("SAVE: SAVED INLINE");
|
||||
|
||||
record = response.data.records[0];
|
||||
graph = response.data;
|
||||
record = response.records[0];
|
||||
graph = response;
|
||||
if (!isCreateMode) {
|
||||
setOriginalContent();
|
||||
}
|
||||
@@ -135,7 +136,6 @@
|
||||
resolve(null);
|
||||
})
|
||||
.catch(function (error) {
|
||||
// setOriginalContent();
|
||||
if (error.response) {
|
||||
if (typeof error.response.data.error === "string") {
|
||||
errorMessage = error.response.data.error;
|
||||
@@ -144,85 +144,60 @@
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
// msgSuccess = null;
|
||||
// msgError = error.response.data.error;
|
||||
// submitted = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={beforeUnload}/>
|
||||
<svelte:window on:beforeunload={beforeUnload} />
|
||||
|
||||
<div class="inline-edit my-4">
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<div class="inline-edit record-edit">
|
||||
<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">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
{record}
|
||||
/>
|
||||
<FilePreview {record} {schema}/>
|
||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||
<FilePreview {record} {schema} />
|
||||
<!-- <fieldset disabled="disabled"> -->
|
||||
{#each activeFields as field (field.name)}
|
||||
{#if activeContentTab === field.group}
|
||||
<FormField
|
||||
bind:data={record.data}
|
||||
bind:graph={graph}
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:data={record.data}
|
||||
bind:graph
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- </fieldset> -->
|
||||
</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>
|
||||
|
||||
<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,51 +0,0 @@
|
||||
import Mustache from "mustache";
|
||||
import {stripHtml} from "../../helpers";
|
||||
|
||||
export function previewTitle(schemas, record, graph) {
|
||||
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
|
||||
|
||||
if (!schema?.titleTemplate) {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
let recordData = record.data;
|
||||
let template = Mustache.parse(schema.titleTemplate);
|
||||
|
||||
let referencePreviews = template
|
||||
.filter(segment => segment[0] === "name") // keep only template tags
|
||||
.map((segment) => segment[1]) // map to fieldNames
|
||||
.filter(fieldName => { // keep only references
|
||||
let schemaField = schema.fields.find(f => f.name === fieldName)
|
||||
return schemaField?.info.name === "reference";
|
||||
}).reduce((carry, field) => { // map to records
|
||||
let edge = graph.edges.find(edge => edge.source === record.id && edge.field === field)
|
||||
let referenceRecord = graph.records.find(rec => rec.id === edge?.target)
|
||||
carry[field] = previewTitle(schemas, referenceRecord, graph);
|
||||
return carry;
|
||||
}, {});
|
||||
recordData = {...recordData, ...referencePreviews}
|
||||
|
||||
let render = Mustache.render(schema.titleTemplate, recordData);
|
||||
|
||||
if (!render || render === "") {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
return stripHtml(render.slice(0, 300));
|
||||
}
|
||||
|
||||
function noTemplate(schema, record) {
|
||||
if (schema?.type === "files") {
|
||||
return record._file.path;
|
||||
}
|
||||
|
||||
let title = stripHtml(
|
||||
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
||||
).slice(0, 300);
|
||||
|
||||
if(title == ""){
|
||||
return "Untitled";
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<script>
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import { previewTitle } from "./Preview";
|
||||
import Status from "./Status.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
export let graph;
|
||||
export let record;
|
||||
export let classes = "";
|
||||
export let hasDelete = false;
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(channel.schemas, record, graph);
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch("remove", record.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
class="card mb-2 bg-light {classes}"
|
||||
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
|
||||
>
|
||||
<div class="card-body d-flex">
|
||||
{#if schema.type === "files"}
|
||||
<div style="max-width:94px;margin-right:15px">
|
||||
<Preview {record} size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="overflow-hidden">
|
||||
<a
|
||||
class="title-link m-0 fs-5 text-decoration-none text-dark d-block"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={cardTitle}
|
||||
>
|
||||
{cardTitle}
|
||||
</a>
|
||||
<small class="text-muted">
|
||||
{schema.label}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{#if record.status === "draft"}
|
||||
<Status status={record.status} />
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasDelete}
|
||||
<div class="position-absolute 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>
|
||||
|
||||
<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,24 +1,13 @@
|
||||
<script>
|
||||
import {previewTitle} from "./Preview";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
export let graph;
|
||||
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
|
||||
$: title = previewTitle(channel.schemas, record, graph);
|
||||
$: title = record.data.name;
|
||||
</script>
|
||||
|
||||
{#if record?.data}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
class="text-decoration-none rounded py-1 px-2 d-inline-block"
|
||||
{title}
|
||||
style="border:2px solid {!schema.color
|
||||
? '#999'
|
||||
: schema.color}!important;white-space: nowrap;"
|
||||
>
|
||||
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
|
||||
{title}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {getStatus, getStatusList} from "./StatusText";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let status = "draft";
|
||||
export let record;
|
||||
export let schema;
|
||||
let dropdown;
|
||||
$: currentStatus = getStatus(status);
|
||||
const statusList = Object.values(getStatusList());
|
||||
|
||||
function updateStatus(e, statusValue) {
|
||||
// e.preventDefault();
|
||||
status = statusValue;
|
||||
dropdown.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Example split danger button -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="btn-group dropup">
|
||||
<button type="button" class="btn btn-{currentStatus.bg}"
|
||||
>{currentStatus.text}</button
|
||||
>
|
||||
<button
|
||||
bind:this={dropdown}
|
||||
type="button"
|
||||
class="btn btn-{currentStatus.bg} dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-header">Change status to</div>
|
||||
{#each statusList as astatus}
|
||||
{#if astatus.value !== status}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item my-2 rounded w-100 bg-{astatus.bg} text-{astatus.color}"
|
||||
on:click={(e) => updateStatus(e, astatus.value)}
|
||||
>
|
||||
{astatus.text}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{#if channel.previewTarget}
|
||||
<a href="{channel.previewTargetUrl}?schema={schema.name}&id={record.id}" target="_blank" class="btn btn-info ms-3">
|
||||
Preview
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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 "../../elements/reference";
|
||||
import BrowseModal from "../../elements/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 "../../elements/reference";
|
||||
|
||||
|
||||
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>
|
||||
@@ -1,114 +0,0 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import Index from "../../content/Index.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
$: data = {};
|
||||
let isOpen = false;
|
||||
let selectedRecords = [];
|
||||
// onMount(() => {
|
||||
// load();
|
||||
// });
|
||||
|
||||
export function open(schema) {
|
||||
isOpen = true;
|
||||
load(schema);
|
||||
}
|
||||
|
||||
export function close() {
|
||||
isOpen = false;
|
||||
selectedRecords = [];
|
||||
}
|
||||
|
||||
function load(schema) {
|
||||
axios
|
||||
.get(channel.lucentUrl + "/content/" + schema)
|
||||
.then((response) => {
|
||||
data = response.data;
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
dispatch("insert", {
|
||||
records: selectedRecords,
|
||||
action: "insert",
|
||||
});
|
||||
}
|
||||
|
||||
function replace(e) {
|
||||
e.preventDefault();
|
||||
dispatch("insert", {
|
||||
records: selectedRecords,
|
||||
action: "replace",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if data.schema}
|
||||
<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">
|
||||
<div class="d-flex align-items-center">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary me-1"
|
||||
on:click={insert}
|
||||
disabled={selectedRecords.length === 0}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary me-3"
|
||||
on:click={replace}
|
||||
disabled={selectedRecords.length === 0}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
{#if selectedRecords.length > 0}
|
||||
<span class="">
|
||||
{selectedRecords.length} records selected
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click|preventDefault={(e) => (isOpen = false)}
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<Index {...data} bind:selected={selectedRecords}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-dialog {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin: 40px auto;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="field-checkbox">
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
|
||||
export let field;
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
@@ -9,23 +10,21 @@
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="input-group ">
|
||||
<div style="width:64px;">
|
||||
<input
|
||||
<div style="display: flex; align-items: center;gap: 10px">
|
||||
<input
|
||||
type="color"
|
||||
{id}
|
||||
class="form-control form-control-color"
|
||||
style="border: none;background: transparent;padding: 0;width:64px;"
|
||||
disabled={field.readonly && !isCreateMode}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class:is-invalid={errorMessage}
|
||||
{id}
|
||||
class="form-control"
|
||||
bind:value
|
||||
readonly={field.readonly && !isCreateMode}
|
||||
type="text"
|
||||
class:is-invalid={errorMessage}
|
||||
{id}
|
||||
class="form-control"
|
||||
bind:value
|
||||
readonly={field.readonly && !isCreateMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {debounce} from "lodash";
|
||||
import {previewTitle} from "../Preview";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
|
||||
export let value;
|
||||
export let search;
|
||||
$: options = [];
|
||||
export const update = debounce((e) => {
|
||||
axios
|
||||
.get("/records/suggestions", {
|
||||
params: {
|
||||
schema: field.optionsFrom,
|
||||
field: field.optionsField,
|
||||
value: search,
|
||||
ui: field.ui,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
options = response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
function select(e, option) {
|
||||
e.preventDefault();
|
||||
value = option.data[field.optionsField];
|
||||
search = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if field.optionsFrom}
|
||||
{#each options as option (option.id)}
|
||||
<div
|
||||
on:click={(e) => select(e, option)}
|
||||
on:keypress={(e) => select(e, option)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
{previewTitle(channel.schemas, option)}
|
||||
<small class="text-muted "
|
||||
>{option.data[field.optionsField]}</small
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#if search && field.optionsSuggest}
|
||||
<div
|
||||
on:click={(e) => {
|
||||
value = search;
|
||||
search = "";
|
||||
}}
|
||||
on:keypress={(e) => {
|
||||
value = search;
|
||||
search = "";
|
||||
}}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
Add "{search}"
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
No results
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user