Compare commits
83 Commits
v1.1.3
...
0725366dd5
| Author | SHA1 | Date | |
|---|---|---|---|
| 0725366dd5 | |||
| a2bcd10607 | |||
| 37ed966ac3 | |||
| 085c307137 | |||
| d961d910d8 | |||
| 1dc6d541cc | |||
| 7fef89b778 | |||
| 6b713e4ffb | |||
| b52a91bf52 | |||
| e8d8340448 | |||
| 81371c41a7 | |||
| 3cf5f0173b | |||
| 65844e030d | |||
| ba7e4ab151 | |||
| ec4e578aee | |||
| 1a5f300a78 | |||
| a04cdd753d | |||
| daa4b268a6 | |||
| a5161cb6b4 | |||
| 48e32bfdcb | |||
| 639ee895cd | |||
| 8cf1dd9bfd | |||
| fcadc8d7a1 | |||
| 1e2f941f47 | |||
| eeee2afc05 | |||
| 98efb76f7b | |||
| dff3748623 | |||
| 93a16ee916 | |||
| 8b3a3964a5 | |||
| 43dd36e20e | |||
| 5587e8b4b6 | |||
| 16e50e2d49 | |||
| bd01e5c32c | |||
| e058ceadee | |||
| 4a7eb217a1 | |||
| 57b0727788 | |||
| 58b047edd2 | |||
| a78b699a5e | |||
| e910ae9878 | |||
| 362c649d36 | |||
| 852c4d608d | |||
| aa59e55a41 | |||
| 348bad80e0 | |||
| f0d4686141 | |||
| a482ab3c7e | |||
| c580882ec0 | |||
| 2cf8379cbe | |||
| c39ec469df | |||
| 232fcc8845 | |||
| 9d5d4dd930 | |||
| c507dc6031 | |||
| 843f560710 | |||
| 7574d67d80 | |||
| 19931cb4d1 | |||
| 6458c1e71d | |||
| 63232585ab | |||
| 6d15591601 | |||
| 32c8378020 | |||
| d0cd8228cc | |||
| c45a3847f8 | |||
| c0b3878674 | |||
| f868219981 | |||
| 8ac0567e66 | |||
| 02f8f5970a | |||
| 0cd4e08716 | |||
| cf3d621587 | |||
| 6fc0a65b6f | |||
| a73ee21568 | |||
| ff54bcc2ef | |||
| ab1517cc8f | |||
| 9f724a3243 | |||
| ae65ca47f6 | |||
| 74d2fcc4fa | |||
| 82174afdea | |||
| ffc39f078d | |||
| 7c4e19afbc | |||
| 7b10bfca1d | |||
| 0e5ac08641 | |||
| 1505aaa909 | |||
| d9e2c4954a | |||
| 97ad9de3d2 | |||
| 9e140be0ec | |||
| a737c2d571 |
@@ -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
-40
@@ -1,43 +1,39 @@
|
||||
{
|
||||
"name": "lexx27/lucent",
|
||||
"type": "library",
|
||||
"description": "Lucent cms",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"ext-xml": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"ext-imagick": "*",
|
||||
"php": "^8.3",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"intervention/image": "^2.7",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.6",
|
||||
"staudenmeir/laravel-cte": "^1.0",
|
||||
"ext-pdo": "*",
|
||||
"mustache/mustache": "^2.14"
|
||||
"name": "lexx27/lucent",
|
||||
"type": "library",
|
||||
"description": "Lucent cms",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"ext-xml": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-pdo": "*",
|
||||
"php": "^8.4",
|
||||
"intervention/image": "^2.7",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.6",
|
||||
"staudenmeir/laravel-cte": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"laravel/framework": "^10.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lucent\\": "src/"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"laravel/framework": "^10.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lucent\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/Response.php",
|
||||
"src/macros.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
+636
-985
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
-1
File diff suppressed because one or more lines are too long
Vendored
-198
File diff suppressed because one or more lines are too long
Vendored
+212
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"main.js": {
|
||||
"file": "assets/main-BtcBvcC_.js",
|
||||
"file": "assets/main-DH0OAeUr.js",
|
||||
"name": "main",
|
||||
"src": "main.js",
|
||||
"isEntry": true,
|
||||
"css": [
|
||||
"assets/main-BWRwkaBb.css"
|
||||
"assets/main-BVNnoznq.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
-51
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
|
||||
|
||||
loadHtmxFormsBehaviour();
|
||||
window.axios = axios;
|
||||
export const axiosInstance = axios;
|
||||
|
||||
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
|
||||
window.axios.interceptors.request.use(
|
||||
function (config) {
|
||||
let list;
|
||||
list = document.querySelectorAll(".btn-spinner");
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
list[i].classList.add("spinner-on");
|
||||
list[i].disabled = true;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
window.axios.interceptors.response.use(
|
||||
function (response) {
|
||||
let list;
|
||||
list = document.querySelectorAll(".btn-spinner");
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
list[i].classList.remove("spinner-on");
|
||||
list[i].disabled = false;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
let list;
|
||||
list = document.querySelectorAll(".btn-spinner");
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
list[i].classList.remove("spinner-on");
|
||||
list[i].disabled = false;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
+128
-29
@@ -1,52 +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)
|
||||
)
|
||||
}
|
||||
const handleClick = (event) => {
|
||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||
node.dispatchEvent(new CustomEvent("click_outside", node));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, true);
|
||||
document.addEventListener("click", handleClick, true);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export function loadHtmxFormsBehaviour(){
|
||||
document.querySelectorAll(".form").forEach(el => {
|
||||
initHtmxForm(el);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function initHtmxForm(el){
|
||||
el.addEventListener("htmx:responseError", (e) => {
|
||||
el.querySelector(".form-errors").innerHTML = e.detail.xhr.response;
|
||||
});
|
||||
|
||||
const formEl = el.querySelector("form");
|
||||
|
||||
if(!formEl.getAttribute("hx-redirect")){
|
||||
return;
|
||||
}
|
||||
el.addEventListener("htmx:afterOnLoad", (e) => {
|
||||
if(e.detail.successful){
|
||||
return window.location.href = formEl.getAttribute("hx-redirect");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
+28
-34
@@ -1,53 +1,47 @@
|
||||
import {axiosInstance} from "./bootstrap";
|
||||
import "../sass/app.scss";
|
||||
import Account from "./svelte/Account.svelte";
|
||||
import Channel from "./svelte/Channel.svelte";
|
||||
import Mustache from "mustache";
|
||||
import 'htmx.org';
|
||||
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
};
|
||||
// import Mustache from "mustache";
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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 style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)">
|
||||
<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,7 +3,7 @@
|
||||
import RecordNotFound from "./records/NotFound.svelte";
|
||||
import RecordEdit from "./records/Edit.svelte";
|
||||
import ContentIndex from "./content/Index.svelte";
|
||||
import {setContext} from "svelte";
|
||||
import { setContext } from "svelte";
|
||||
import Navbar from "./layout/Navbar.svelte";
|
||||
import HomeIndex from "./home/Index.svelte";
|
||||
import BuildReport from "./build/Report.svelte";
|
||||
@@ -25,27 +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>
|
||||
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="sidebar-content">
|
||||
<Navbar schema={data.schema}/>
|
||||
<div class="sidebar-content">
|
||||
<Navbar schema={data.schema} />
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<Header />
|
||||
<svelte:component this={components[view]} {title} {...data} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<Header />
|
||||
<svelte:component this={components[view]} {title} {...data}/>
|
||||
</div>
|
||||
</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-5">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control mb-3"
|
||||
placeholder="Name"
|
||||
required
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control mb-3"
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
<SpinnerButton label="Update Name"/>
|
||||
<SpinnerButton label="Update Name" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form on:submit={saveEmail}>
|
||||
<div class="input-group mb-5">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control mb-3"
|
||||
placeholder="Email"
|
||||
required
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control mb-3"
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<SpinnerButton label="Update Email"/>
|
||||
<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 });
|
||||
}
|
||||
@@ -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="common-wrapper">
|
||||
<div class="lx-card mt-5">
|
||||
|
||||
<h3 class="header-small mb-5">{title}</h3>
|
||||
|
||||
<button on:click={buildWebsite} class="button primary 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>
|
||||
|
||||
@@ -113,8 +113,21 @@
|
||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
"list": {
|
||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 8h10M9 12h10M9 16h10M4.99 8H5m-.02 4h.01m0 4H5"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
"ordered-list": {
|
||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6h8m-8 6h8m-8 6h8M4 16a2 2 0 1 1 3.321 1.5L4 20h5M4 5l2-1v6m-2 0h4"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
"italic": {
|
||||
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8.874 19 6.143-14M6 19h6.33m-.66-14H18"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export let width = 16;
|
||||
export let height = 16;
|
||||
export let icon = "";
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<button type="submit" class="button secondary 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>
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
@@ -38,44 +37,44 @@
|
||||
<div style="display: flex;align-items: center; gap: 8px">
|
||||
<span class="me-2">{selected.length} records selected</span>
|
||||
<button
|
||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||
type="button"
|
||||
class="button">Publish
|
||||
</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
|
||||
>
|
||||
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="button">Publish
|
||||
</button
|
||||
>
|
||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||
type="button"
|
||||
class="button"
|
||||
>Publish
|
||||
</button>
|
||||
{#if schema.hasDrafts}
|
||||
<button
|
||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||
type="button"
|
||||
class="button">Make Draft
|
||||
</button
|
||||
>
|
||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||
type="button"
|
||||
class="button"
|
||||
>Make Draft
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
on:click|preventDefault={deleteRecords}
|
||||
type="button"
|
||||
class="button">Delete forever
|
||||
</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
|
||||
>
|
||||
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,9 +3,9 @@
|
||||
import Pagination from "./pagination/Pagination.svelte";
|
||||
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
||||
import Table from "./Table.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { apiGet } from "../../helpers";
|
||||
|
||||
const axios = getContext("axios");
|
||||
export let schema;
|
||||
export let users;
|
||||
export let records;
|
||||
@@ -26,18 +26,17 @@
|
||||
|
||||
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) => {
|
||||
@@ -47,51 +46,49 @@
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<div class="{inModal ? 'mt-0' : 'mt-5'}">
|
||||
<h3 class="header-normal mb-5 ">
|
||||
<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}
|
||||
|
||||
<Table
|
||||
{records}
|
||||
{graph}
|
||||
{schema}
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{systemFields}
|
||||
{inModal}
|
||||
{users}
|
||||
{isWritable}
|
||||
bind:selected
|
||||
{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")}
|
||||
<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")}
|
||||
<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)}
|
||||
<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)}
|
||||
<td
|
||||
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
||||
"_sys.updatedAt" == sortParam}
|
||||
>
|
||||
{friendlyDate(record.updatedAt)}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
@@ -1,13 +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";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import {fileurl} from "../files/imageserver.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
@@ -23,100 +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="table mt-5 ">
|
||||
<div class="table mt-5">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#if isWritable}
|
||||
<th>
|
||||
<Checkbox
|
||||
<tr>
|
||||
{#if isWritable}
|
||||
<th>
|
||||
<Checkbox
|
||||
value=""
|
||||
on:change={eventToggleAll}
|
||||
indeterminate={selected.length > 0 && selected.length < records.length}
|
||||
indeterminate={selected.length > 0 &&
|
||||
selected.length < records.length}
|
||||
checked={selected.length === records.length}
|
||||
>
|
||||
</Checkbox>
|
||||
</th>
|
||||
{/if}
|
||||
></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}
|
||||
>{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>
|
||||
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"
|
||||
>
|
||||
{#if isWritable}
|
||||
<Checkbox
|
||||
{#each records as record (record.id)}
|
||||
<tr>
|
||||
<td class="title-td">
|
||||
<div class="title-td-contents">
|
||||
{#if isWritable}
|
||||
<Checkbox
|
||||
on:change={() => select(record)}
|
||||
checked={selected.find((r) => r.id === record.id)}
|
||||
checked={selected.find(
|
||||
(r) => r.id === record.id,
|
||||
)}
|
||||
value={record}
|
||||
>
|
||||
</Checkbox>
|
||||
></Checkbox>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
{#if record._file?.path}
|
||||
<div class="file-table-row">
|
||||
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
|
||||
<div>
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
>
|
||||
{previewTitle(channel.schemas, record, graph)}
|
||||
</a>
|
||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
||||
|
||||
{#if record._file.width > 0}
|
||||
<span>{record._file.width + "x" + record._file.height}</span>
|
||||
{/if}
|
||||
<a
|
||||
href="{fileurl(channel,record)}"
|
||||
target="_blank"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
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>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<RecordRow
|
||||
</div>
|
||||
</td>
|
||||
<RecordRow
|
||||
{record}
|
||||
{graph}
|
||||
{schema}
|
||||
@@ -124,19 +101,15 @@
|
||||
{sortParam}
|
||||
{sortField}
|
||||
{users}
|
||||
/>
|
||||
<td>
|
||||
<Avatar
|
||||
name={usernameById(
|
||||
users,
|
||||
record._sys.updatedBy
|
||||
)}
|
||||
side={24}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
<td>
|
||||
<Avatar
|
||||
name={usernameById(users, record.updatedBy)}
|
||||
side={24}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -1,9 +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;
|
||||
@@ -12,7 +10,6 @@
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
export let graph;
|
||||
|
||||
let filter = {
|
||||
label: "",
|
||||
operator: "",
|
||||
@@ -20,13 +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) => {
|
||||
@@ -51,8 +45,7 @@
|
||||
const filterField = schema.fields.find((f) => f.name === fieldName);
|
||||
filter.label = filterField?.label ?? fieldName;
|
||||
return filter;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const filterRecord = extractFilterRecord(graph, value);
|
||||
@@ -61,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) {
|
||||
@@ -78,19 +71,21 @@
|
||||
</script>
|
||||
|
||||
<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}
|
||||
|
||||
{#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="button-text"
|
||||
aria-label="Close"
|
||||
><Icon width={12} height={12} icon="close"></Icon></button>
|
||||
<button
|
||||
on:click|preventDefault={() => removeFilter(key)}
|
||||
type="button"
|
||||
class="button-text"
|
||||
aria-label="Close"
|
||||
><Icon width={12} height={12} icon="close"></Icon></button
|
||||
>
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
|
||||
|
||||
let dropdown;
|
||||
let search = "";
|
||||
let systemFieldsFiltered = systemFields;
|
||||
@@ -70,6 +69,13 @@
|
||||
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 = "";
|
||||
@@ -146,7 +152,7 @@
|
||||
<div class="selected-filter">field: {activeField.label}</div>
|
||||
|
||||
{#each activeOperators as operator}
|
||||
<button class="dropdown-item button" on:click={e => activeOperator = operator }>
|
||||
<button class="dropdown-item button" on:click={e => selectOperator(e,operator)}>
|
||||
{operator.label}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -214,8 +220,8 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<button class="button applied-filter">
|
||||
Submit
|
||||
<button class="button applied-filter">
|
||||
Submit
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -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,46 +31,36 @@
|
||||
|
||||
function apply(e, newOption) {
|
||||
e.preventDefault();
|
||||
value = newOption.id
|
||||
value = newOption.id;
|
||||
dispatch("addFilter");
|
||||
value = ""
|
||||
|
||||
value = "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="reference-tags">
|
||||
<input
|
||||
type="search"
|
||||
on:keyup={updateResults}
|
||||
bind:value={search}
|
||||
placeholder={"Search for "+field.label}
|
||||
autocomplete="off"
|
||||
type="search"
|
||||
on:keyup={updateResults}
|
||||
bind:value={search}
|
||||
placeholder={"Search for " + field.label}
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<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)}
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => apply(e, option)}
|
||||
on:keypress={(e) => apply(e, option)}
|
||||
>
|
||||
{previewTitle(channel.schemas, option)}
|
||||
{option.data.name}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div
|
||||
class="start-typing">
|
||||
Start typing...
|
||||
</div>
|
||||
<div class="start-typing">Start typing...</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<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 {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
|
||||
|
||||
@@ -41,120 +40,99 @@
|
||||
} else {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function uploadComplete(e) {
|
||||
records = e.detail;
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="search" required>
|
||||
<input
|
||||
type="search"
|
||||
name="filter[search_regex]"
|
||||
placeholder="Search"
|
||||
class="search"
|
||||
required
|
||||
/>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items: center;gap:4px">
|
||||
{#if schema.type === "collection"}
|
||||
{#if !inModal && isWritable}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
class="button"
|
||||
>
|
||||
New Record
|
||||
</a>
|
||||
{/if}
|
||||
{:else }
|
||||
<div>
|
||||
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
||||
</div>
|
||||
{#if !inModal && isWritable}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
class="button"
|
||||
>
|
||||
New Record
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if !inModal}
|
||||
<Dropdown orientation="right">
|
||||
<div slot="button">
|
||||
<Icon icon="ellipsis-vertical"/>
|
||||
<Icon icon="ellipsis-vertical" />
|
||||
</div>
|
||||
{#if filter["status_in"] === "trashed"}
|
||||
{#if isWritable}
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
|
||||
>
|
||||
Empty trash
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href={csvUrl}
|
||||
>Export to CSV</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}?filter[status_in]=trashed"
|
||||
>View trashed records</a
|
||||
>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
|
||||
>View unlinked records</a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
|
||||
>View unlinked records</a
|
||||
>
|
||||
{/if}
|
||||
</Dropdown>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="applied-filters">
|
||||
<AppliedFilterNotLinked
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
<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}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script>
|
||||
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import Index from "../content/Index.svelte";
|
||||
import axios from "axios";
|
||||
import { apiGet } from "../../helpers";
|
||||
|
||||
let dialogEl;
|
||||
|
||||
@@ -15,21 +14,19 @@
|
||||
// load();
|
||||
// });
|
||||
|
||||
|
||||
export function close(e) {
|
||||
if(e){
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
dialogEl.close()
|
||||
dialogEl.close();
|
||||
selectedRecords = [];
|
||||
}
|
||||
|
||||
function load(schema) {
|
||||
axios
|
||||
.get(channel.lucentUrl + "/content/" + schema)
|
||||
apiGet(channel.lucentUrl + "/content/" + schema)
|
||||
.then((response) => {
|
||||
data = response.data;
|
||||
data = response;
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
@@ -52,42 +49,41 @@
|
||||
}
|
||||
|
||||
export function open(schema) {
|
||||
dialogEl.showModal()
|
||||
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}
|
||||
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}
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={replace}
|
||||
disabled={selectedRecords.length === 0}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
{#if selectedRecords.length > 0}
|
||||
<span class="">
|
||||
{selectedRecords.length} records selected
|
||||
</span>
|
||||
<span class="">
|
||||
{selectedRecords.length} records selected
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|preventDefault={close}
|
||||
type="button"
|
||||
class="button close"
|
||||
aria-label="Close"
|
||||
on:click|preventDefault={close}
|
||||
type="button"
|
||||
class="button close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon icon="close"></Icon>
|
||||
</button>
|
||||
@@ -96,6 +92,5 @@
|
||||
<div class="dialog-body">
|
||||
<Index {...data} bind:selected={selectedRecords}></Index>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</dialog>
|
||||
</dialog>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import FileIndex from "./FileIndex.svelte";
|
||||
|
||||
let dialogEl;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
$: files = [];
|
||||
$: selectedRecords = [];
|
||||
// onMount(() => {
|
||||
// load();
|
||||
// });
|
||||
|
||||
export function close(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
dialogEl.close();
|
||||
selectedRecords = [];
|
||||
}
|
||||
|
||||
function load(recordId) {
|
||||
fetch(channel.lucentUrl + "/records/files/?recordId=" + recordId)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
files = json;
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
dispatch("insert_files", selectedRecords);
|
||||
}
|
||||
|
||||
function replace(e) {
|
||||
e.preventDefault();
|
||||
dispatch("replace_files", selectedRecords);
|
||||
}
|
||||
|
||||
export function open(recordId) {
|
||||
dialogEl.showModal();
|
||||
load(recordId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialogEl}>
|
||||
<div class="dialog-header">
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={insert}
|
||||
disabled={selectedRecords.length === 0}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={replace}
|
||||
disabled={selectedRecords.length === 0}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
{#if selectedRecords.length > 0}
|
||||
<span class="">
|
||||
{selectedRecords.length} records selected
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|preventDefault={close}
|
||||
type="button"
|
||||
class="button close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon icon="close"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<FileIndex {files} bind:selected={selectedRecords}></FileIndex>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import Checkbox from "../common/Checkbox.svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import { fileurl } from "../files/imageserver";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
export let files = [];
|
||||
export let selected = [];
|
||||
export let isWritable = true;
|
||||
|
||||
function eventToggleAll(e) {
|
||||
selected = toggleAll(e, files, selected);
|
||||
}
|
||||
|
||||
function select(file) {
|
||||
selected = selectFile(file, selected);
|
||||
}
|
||||
|
||||
export const toggleAll = (e, files, selected) => {
|
||||
if (selected.length === files.length) {
|
||||
return [];
|
||||
}
|
||||
e.currentTarget.checked = selected.length > 0;
|
||||
return files;
|
||||
};
|
||||
|
||||
export const selectFile = (file, selected) => {
|
||||
let fileExists = selected.find((r) => r.id === file.id);
|
||||
if (fileExists) {
|
||||
return selected.filter((r) => r.id !== file.id);
|
||||
}
|
||||
return [...selected, file];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="table mt-5">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#if isWritable}
|
||||
<th>
|
||||
<Checkbox
|
||||
value=""
|
||||
on:change={eventToggleAll}
|
||||
indeterminate={selected.length > 0 &&
|
||||
selected.length < files.length}
|
||||
checked={selected.length === files.length}
|
||||
></Checkbox>
|
||||
</th>
|
||||
{/if}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each files as file (file.id)}
|
||||
<tr>
|
||||
<td class="title-td">
|
||||
<div class="title-td-contents">
|
||||
{#if isWritable}
|
||||
<Checkbox
|
||||
on:change={() => select(file)}
|
||||
checked={selected.find(
|
||||
(s) => s.id === file.id,
|
||||
)}
|
||||
value={file}
|
||||
></Checkbox>
|
||||
{/if}
|
||||
<div class="file-table-row">
|
||||
<Preview
|
||||
{file}
|
||||
size={file.width > 0 ? "medium" : "small"}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{file.filename}
|
||||
<span
|
||||
>{(file.size / 1024).toFixed(1)}kB</span
|
||||
>
|
||||
|
||||
{#if file.width > 0}
|
||||
<span
|
||||
>{file.width +
|
||||
"x" +
|
||||
file.height}</span
|
||||
>
|
||||
{/if}
|
||||
<a
|
||||
href={fileurl(channel, file)}
|
||||
target="_blank"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,25 +1,29 @@
|
||||
|
||||
|
||||
|
||||
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)) ?? [];
|
||||
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,17 @@
|
||||
<script>
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import {imgurl} from "./imageserver.js";
|
||||
import {getContext} from "svelte";
|
||||
import { 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;
|
||||
|
||||
console.log({ channel });
|
||||
if (size == "large") {
|
||||
imageSide = 256;
|
||||
fileSide = 32;
|
||||
@@ -28,50 +30,49 @@
|
||||
fontSize = "13";
|
||||
}
|
||||
</script>
|
||||
<div style="display: flex;align-items: center;gap: 5px;">
|
||||
{#if record}
|
||||
|
||||
{#if record._file.mime.startsWith("image")}
|
||||
<div style="display: flex;align-items: center;gap: 5px;">
|
||||
{#if file}
|
||||
{#if file.mime.startsWith("image")}
|
||||
<!-- href={imgurl(record)} -->
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.originalName}
|
||||
style="width:{imageSide}px;height:{imageSide}px"
|
||||
href="{channel.lucentUrl}/files/{file.id}"
|
||||
title={file.filename}
|
||||
style="width:{imageSide}px;height:{imageSide}px"
|
||||
>
|
||||
<img
|
||||
class="rounded w-100"
|
||||
src={imgurl(channel,record)}
|
||||
alt={record._file.path}
|
||||
class="rounded w-100"
|
||||
src={imgurl(channel, file)}
|
||||
alt={file.path}
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
class="file-preview-small"
|
||||
style="width:{imageSide}px;height:{imageSide}px"
|
||||
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}/>
|
||||
<Icon icon="file" width={fileSide} height={fileSide} />
|
||||
<span class="ms-2"
|
||||
>.{record._file.path.split(".").pop().toLowerCase()}</span
|
||||
>.{file.path.split(".").pop().toLowerCase()}</span
|
||||
>
|
||||
</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
|
||||
>
|
||||
href="{channel.lucentUrl}/files/{file.id}"
|
||||
title={file.path}
|
||||
class="preview-file-filename lx-small-text text-decoration-none"
|
||||
>{file.path}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
img{
|
||||
img {
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
export let recordId;
|
||||
let mimeTypes = "";
|
||||
let files = [];
|
||||
let isLoading = false;
|
||||
@@ -17,39 +17,39 @@
|
||||
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 class="upload-button" disabled={isLoading}>
|
||||
<label class="button primary btn-spinner ">
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<fieldset class="upload-button" disabled={isLoading}>
|
||||
<label class="button primary btn-spinner">
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Upload file
|
||||
|
||||
<input
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
export function imgurl(channel, file) {
|
||||
if (file.mime === "image/svg+xml") {
|
||||
return fileurl(channel, file);
|
||||
}
|
||||
return channel.filesUrl + `/thumbs/${file.path}`;
|
||||
}
|
||||
|
||||
export function imgurl(channel,record) {
|
||||
export function fileurl(channel, file) {
|
||||
return channel.filesUrl + `/${file.path}`;
|
||||
}
|
||||
|
||||
if (record._file.mime === "image/svg+xml") {
|
||||
return fileurl(channel, record);
|
||||
export function htmlurl(channel, file, preset) {
|
||||
let html = "";
|
||||
let url = fileurl(channel, file);
|
||||
|
||||
if (file.width > 0) {
|
||||
let presetUrl = url;
|
||||
if (preset) {
|
||||
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
|
||||
}
|
||||
return channel.filesUrl + `/thumbs/${record._file.path}`;
|
||||
}
|
||||
|
||||
export function fileurl(channel, record) {
|
||||
return channel.filesUrl + `/${record._file.path}`;
|
||||
}
|
||||
|
||||
export function htmlurl(channel,record, preset) {
|
||||
|
||||
let html = "";
|
||||
let url = fileurl(channel,record)
|
||||
|
||||
if (record._file.width > 0) {
|
||||
let presetUrl = url;
|
||||
if (preset) {
|
||||
presetUrl = channel.filesUrl + `/templates/${preset}/${record._file.path}`;
|
||||
}
|
||||
|
||||
html = `<img src="${presetUrl}" alt="${record._file.path}" />`
|
||||
} else if (record._file.mime === "image/svg+xml") {
|
||||
html = `<img src="${url}" alt="${record._file.path}"/>`
|
||||
} else {
|
||||
html = `<a href="${url}">${record._file.originalName}</a>`
|
||||
}
|
||||
|
||||
return html;
|
||||
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
||||
} else if (file.mime === "image/svg+xml") {
|
||||
html = `<img src="${url}" alt="${file.path}"/>`;
|
||||
} else {
|
||||
html = `<a href="${url}">${file.originalName}</a>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -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,21 +19,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<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 {graph} {record} {users}/>
|
||||
</tr>
|
||||
{/each}
|
||||
{#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 {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>
|
||||
<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}"
|
||||
|
||||
>
|
||||
{previewTitle(channel.schemas, record, graph)}
|
||||
</a>
|
||||
{/if}
|
||||
{#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><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
|
||||
|
||||
<td>
|
||||
<div style="display: flex;gap: 14px">
|
||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||
<div class="ms-2">
|
||||
{frieldlyUpdatedAt}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<script>
|
||||
import Avatar from "../account/Avatar.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import Dropdown from "../common/Dropdown.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const user = getContext("user");
|
||||
</script>
|
||||
|
||||
|
||||
<div class="top-nav ">
|
||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||
|
||||
{#if channel.generateCommand}
|
||||
<a href="{channel.lucentUrl}/build-report" class="top-nav-item">Build website</a>
|
||||
<div class="top-nav">
|
||||
{#if channel.auth == "lucent"}
|
||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||
{/if}
|
||||
{#if channel.commands.length > 0}
|
||||
<Dropdown>
|
||||
<div slot="button">Actions</div>
|
||||
{#each channel.commands as command}
|
||||
<a
|
||||
href="{channel.lucentUrl}/command-report/{command.signature}"
|
||||
class="top-nav-item">{command.name}</a
|
||||
>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{/if}
|
||||
<!-- <div>-->
|
||||
<!-- <form method="GET">-->
|
||||
@@ -19,8 +28,11 @@
|
||||
<!-- class="form-control" required/>-->
|
||||
<!-- </form>-->
|
||||
<!-- </div>-->
|
||||
<a href="{channel.lucentUrl}/profile">
|
||||
<Avatar side="28" name={user.name}/>
|
||||
{#if channel.auth == "lucent"}
|
||||
<a href="{channel.lucentUrl}/profile">
|
||||
<Avatar side="28" name={user.name} />
|
||||
</a>
|
||||
{:else}
|
||||
<Avatar side="28" name={user.name} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
<script>
|
||||
import NavbarMenu from "./NavbarMenu.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let schema;
|
||||
const channel = getContext("channel");
|
||||
const readableSchemas = getContext("readableSchemas");
|
||||
|
||||
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
|
||||
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
|
||||
|
||||
</script>
|
||||
|
||||
<div class="sidebar-top">
|
||||
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
|
||||
<a class="nav-item" href="{channel.lucentUrl}/profile">
|
||||
</a>
|
||||
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
|
||||
|
||||
<NavbarMenu
|
||||
title="Content"
|
||||
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
|
||||
schema={schema}
|
||||
expanded={true}
|
||||
/>
|
||||
|
||||
<NavbarMenu
|
||||
title="Files"
|
||||
schemas={ fileSchemas}
|
||||
schema={schema}
|
||||
/>
|
||||
|
||||
<NavbarMenu
|
||||
title="Other"
|
||||
schemas={ otherSchemas}
|
||||
schema={schema}
|
||||
/>
|
||||
{#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,34 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schemas;
|
||||
export let title;
|
||||
export let schema;
|
||||
export let expanded = false;
|
||||
|
||||
if(schemas.find(s => s.name === schema?.name)){
|
||||
expanded = true;
|
||||
}
|
||||
|
||||
function toggleExpand(){
|
||||
expanded = !expanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="sidebar-header" tabindex="0" on:click={toggleExpand}>
|
||||
{title}
|
||||
{#if expanded}
|
||||
<Icon icon="circle-chevron-up"></Icon>
|
||||
{:else}
|
||||
<Icon icon="circle-chevron-down"></Icon>
|
||||
{/if}
|
||||
</button>
|
||||
{#if expanded}
|
||||
{#each schemas as aschema}
|
||||
<a class="sidebar-item" class:active={aschema.name === schema?.name}
|
||||
aria-current="page"
|
||||
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -1,20 +1,42 @@
|
||||
<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";
|
||||
|
||||
let parentElement;
|
||||
let codeMirrorView;
|
||||
export let value;
|
||||
export let editable = true;
|
||||
|
||||
export function insertMedia(info) {
|
||||
let insertText = "";
|
||||
if (info.file.width > 0) {
|
||||
insertText = ``;
|
||||
} else {
|
||||
insertText = `[${info.file.filename}](${info.originalUrl})`;
|
||||
}
|
||||
const cursor = codeMirrorView.state.selection.main.head;
|
||||
const transaction = codeMirrorView.state.update({
|
||||
changes: {
|
||||
from: cursor,
|
||||
insert: insertText,
|
||||
},
|
||||
// the next 2 lines will set the appropriate cursor position after inserting the new text.
|
||||
selection: { anchor: cursor + 1 },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
|
||||
if (transaction) {
|
||||
codeMirrorView.dispatch(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let language = new Compartment();
|
||||
let tabSize = new Compartment();
|
||||
@@ -23,11 +45,7 @@
|
||||
doc: value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of([
|
||||
indentWithTab,
|
||||
...lintKeymap,
|
||||
...completionKeymap
|
||||
]),
|
||||
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
||||
language.of(markdown()),
|
||||
markdown(),
|
||||
autocompletion(),
|
||||
@@ -40,18 +58,14 @@
|
||||
}
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({spellcheck: "true"})
|
||||
EditorView.contentAttributes.of({ spellcheck: "true" }),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
codeMirrorView = new EditorView({
|
||||
state,
|
||||
parent: parentElement,
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -61,4 +75,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="is-editable-{editable}" bind:this={parentElement}/>
|
||||
<div class="is-editable-{editable}" bind:this={parentElement} />
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<script>
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
|
||||
import tinymce from "tinymce/tinymce";
|
||||
import "tinymce/models/dom";
|
||||
import "tinymce/icons/default";
|
||||
import "tinymce/themes/silver";
|
||||
import "tinymce/skins/ui/oxide/skin.css";
|
||||
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css?inline";
|
||||
import customcss from "./tinymce.css?inline";
|
||||
|
||||
import "tinymce/plugins/link";
|
||||
import "tinymce/plugins/code";
|
||||
import "tinymce/plugins/image";
|
||||
import "tinymce/plugins/table";
|
||||
import "tinymce/plugins/codesample";
|
||||
import "tinymce/plugins/media";
|
||||
|
||||
import "tinymce/plugins/lists";
|
||||
import "tinymce/plugins/autoresize";
|
||||
import "tinymce/plugins/wordcount";
|
||||
|
||||
export let value = "";
|
||||
export let additionalConfig = {};
|
||||
let lastVal = "";
|
||||
let textareaEl;
|
||||
let activeEditor;
|
||||
let editorWrapper;
|
||||
const plugins = [
|
||||
"autoresize",
|
||||
"code",
|
||||
"image",
|
||||
"table",
|
||||
"codesample",
|
||||
"link",
|
||||
"lists",
|
||||
"media",
|
||||
"wordcount",
|
||||
];
|
||||
const toolbar =
|
||||
"bold italic underline strikethrough removeformat | link | subscript superscript bullist numlist media image codesample table code wordcount blockquote indent outdent blocks";
|
||||
|
||||
onDestroy(() => {
|
||||
if (activeEditor) {
|
||||
activeEditor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const config = {
|
||||
target: textareaEl,
|
||||
toolbar_mode: "sliding",
|
||||
toolbar_sticky: true,
|
||||
skin: false,
|
||||
content_css: false,
|
||||
content_style: contentUiSkinCss.toString() + customcss.toString(),
|
||||
branding: false,
|
||||
inline: false,
|
||||
plugins: plugins,
|
||||
contextmenu: false,
|
||||
menubar: false,
|
||||
statusbar: false,
|
||||
entity_encoding: "raw",
|
||||
convert_urls: false,
|
||||
toolbar: toolbar,
|
||||
image_caption: true,
|
||||
relative_urls: false,
|
||||
browser_spellcheck: true,
|
||||
max_height: 600,
|
||||
// media_poster: false,
|
||||
|
||||
setup: function (editor) {
|
||||
activeEditor = editor;
|
||||
|
||||
editor.on("init", function (e) {
|
||||
editor.setContent(value ?? "");
|
||||
});
|
||||
|
||||
// editor.on("blur", function (e) {
|
||||
// let content = setImageDimensions(editor.getContent());
|
||||
// dispatch("editorBlur", content);
|
||||
// editorWrapper.classList.remove("editorFocus");
|
||||
// // return false;
|
||||
// });
|
||||
|
||||
// editor.on("focus", function (e) {
|
||||
// editorWrapper.classList.add("editorFocus");
|
||||
// // return false;
|
||||
// });
|
||||
|
||||
editor.on("change input undo redo", function (e) {
|
||||
lastVal = editor.getContent();
|
||||
if (lastVal !== value) {
|
||||
value = lastVal;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
tinymce.init({...config, ...additionalConfig});
|
||||
});
|
||||
|
||||
export function insertMedia(html){
|
||||
activeEditor.execCommand('InsertHTML', false, html);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div bind:this={editorWrapper} class="tox-wrapper">
|
||||
<div class="form-control" bind:this={textareaEl}>
|
||||
{@html value}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.tox:not(.tox-tinymce-inline) .tox-editor-header) {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ced4da;
|
||||
box-shadow: none;
|
||||
padding: 4px 0;
|
||||
transition: box-shadow 0.5s;
|
||||
}
|
||||
|
||||
:global(.tox-tinymce) {
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -3,9 +3,12 @@
|
||||
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';
|
||||
@@ -19,6 +22,8 @@
|
||||
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;
|
||||
@@ -32,19 +37,22 @@
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
ListItem,
|
||||
BulletList,
|
||||
Code,
|
||||
CodeBlock,
|
||||
History,
|
||||
Italic,
|
||||
ListItem,
|
||||
HardBreak,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
Strike,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
Underline,
|
||||
Dropcursor,
|
||||
Image,
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
@@ -56,7 +64,11 @@
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor;
|
||||
},
|
||||
onUpdate: ({editor}) => {
|
||||
value = editor.getHTML()
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -64,33 +76,99 @@
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
export function insertMedia(info){
|
||||
editor.chain().focus().setImage({ src: info.url }).run()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if editor}
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 1 })}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 2 })}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().setParagraph().run()}
|
||||
class:active={editor.isActive('paragraph')}
|
||||
>
|
||||
P
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||
class:active={editor.isActive('bold')}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<div class="editor-toolbar">
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 1 })}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 2 })}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||
class:active={editor.isActive('bold')}
|
||||
>
|
||||
B
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleItalic().run()}
|
||||
class:active={editor.isActive('italic')}
|
||||
>
|
||||
<em>IT</em>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleUnderline().run()}
|
||||
class:active={editor.isActive('underline')}
|
||||
>
|
||||
<u>U</u>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleStrike().run()}
|
||||
class:active={editor.isActive('strike')}
|
||||
>
|
||||
<s>S</s>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.commands.unsetAllMarks()}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleCode().run()}
|
||||
class:active={editor.isActive('code')}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleBulletList().run()}
|
||||
class:active={editor.isActive('bulletList')}
|
||||
>
|
||||
<Icon icon="list"></Icon>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
class:active={editor.isActive('orderedList')}
|
||||
>
|
||||
<Icon icon="ordered-list"></Icon>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
class:active={editor.isActive('blockquote')}
|
||||
>
|
||||
""
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
class:active={editor.isActive('codeBlock')}
|
||||
>
|
||||
cb
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div bind:this={element} class="content"/>
|
||||
@@ -1,21 +1,66 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import Trix from "trix"
|
||||
import customcss from "./tinymce.css?inline";
|
||||
import "trix/dist/trix.css"
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import Trix from "trix";
|
||||
import "trix/dist/trix.css";
|
||||
|
||||
export let value = "";
|
||||
let textareaEl;
|
||||
let lastVal;
|
||||
let editorWrapper;
|
||||
let activeEditor;
|
||||
export let field;
|
||||
let editor;
|
||||
|
||||
Trix.config.blockAttributes.default.breakOnReturn = false
|
||||
console.log(Trix.config)
|
||||
function updateValue(e) {
|
||||
value = e.target.value;
|
||||
}
|
||||
|
||||
export function insertMedia(info) {
|
||||
if (info.file.width > 0) {
|
||||
var attachment = new Trix.Attachment({ content: info.html });
|
||||
editor.editor.insertAttachment(attachment);
|
||||
} else {
|
||||
editor.editor.insertHTML(
|
||||
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
editor.addEventListener("trix-file-accept", (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
editor.addEventListener("trix-before-initialize", (e) => {
|
||||
Trix.config.blockAttributes.heading1.tagName = "h2";
|
||||
const { toolbarElement } = e.target;
|
||||
const h1Button = toolbarElement.querySelector(
|
||||
"[data-trix-attribute=heading1]",
|
||||
);
|
||||
h1Button.insertAdjacentHTML(
|
||||
"afterend",
|
||||
`<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`,
|
||||
);
|
||||
});
|
||||
});
|
||||
// onDestroy(() => {
|
||||
// editor.removeEventListener("trix-before-initialize")
|
||||
// })
|
||||
|
||||
Trix.config.blockAttributes.default.breakOnReturn = false;
|
||||
Trix.config.blockAttributes.heading3 = {
|
||||
tagName: "h3",
|
||||
terminal: true,
|
||||
breakOnReturn: true,
|
||||
group: false,
|
||||
};
|
||||
// console.log(Trix.config)
|
||||
</script>
|
||||
|
||||
<div bind:this={editorWrapper} class="tox-wrapper">
|
||||
<input bind:this={textareaEl} id="x" bind:value type="hidden">
|
||||
<trix-editor class="trix-content content" input="x"></trix-editor>
|
||||
<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>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.mce-content-body .img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mce-content-body{
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.mce-content-body p{
|
||||
|
||||
margin-bottom: 14px;
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mce-content-body ul {
|
||||
padding: 0 0 0 16px;
|
||||
list-style: none outside none;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.mce-content-body li::before {
|
||||
content: "—";
|
||||
opacity: .5;
|
||||
font-size: 12px;
|
||||
padding-right: 6px;
|
||||
vertical-align: 10%;
|
||||
}
|
||||
|
||||
.mce-content-body li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -3,9 +3,8 @@
|
||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import axios from "axios";
|
||||
|
||||
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 ?? "";
|
||||
@@ -63,50 +60,45 @@
|
||||
<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}
|
||||
class="form-control"
|
||||
id="inviteeName"
|
||||
placeholder="Member name"
|
||||
required
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="form-control"
|
||||
id="inviteeName"
|
||||
placeholder="Member name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inviteeEmail" class="form-label"
|
||||
>Invitee Email Address</label
|
||||
>Invitee Email Address</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
id="inviteeEmail"
|
||||
placeholder="Member email"
|
||||
required
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="form-control"
|
||||
id="inviteeEmail"
|
||||
placeholder="Member email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="me-3">
|
||||
<select bind:value={role}>
|
||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||
<option
|
||||
value={arole}
|
||||
|
||||
>{arole}</option>
|
||||
<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>
|
||||
@@ -115,10 +107,10 @@
|
||||
<h3 class="header-small mb-5 mt-5">Members</h3>
|
||||
{#each users as user}
|
||||
<MemberSettingsCard
|
||||
member={user}
|
||||
roles={channel.roles}
|
||||
on:update={update}
|
||||
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
||||
member={user}
|
||||
roles={channel.roles}
|
||||
on:update={update}
|
||||
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script>
|
||||
import {afterUpdate, getContext, onMount} from "svelte";
|
||||
import {isEqual} from "lodash";
|
||||
import axios from "axios";
|
||||
import EditHeader from "./header/EditHeader.svelte"
|
||||
import FilePreview from "./FilePreview.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 { 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,7 +15,7 @@
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
edges: []
|
||||
edges: [],
|
||||
};
|
||||
// export let recordHistory;
|
||||
export let isCreateMode;
|
||||
@@ -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,70 +141,60 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={beforeUnload}/>
|
||||
<svelte:window on:beforeunload={beforeUnload} />
|
||||
|
||||
<div class="record-edit">
|
||||
<div class="tools-header">
|
||||
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
|
||||
<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"
|
||||
/>
|
||||
<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}
|
||||
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"
|
||||
/>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<Title {schema} {record} {isCreateMode}/>
|
||||
<Title {schema} {record} {isCreateMode} />
|
||||
|
||||
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<ErrorAlert message={errorMessage} />
|
||||
|
||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
/>
|
||||
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||
<FilePreview {record} {schema}/>
|
||||
{#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}
|
||||
{: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,7 +1,7 @@
|
||||
<script>
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import {fileurl} from "../files/imageserver"
|
||||
import {getContext} from "svelte"
|
||||
import { fileurl } from "../files/imageserver";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
@@ -11,10 +11,9 @@
|
||||
{#if schema.type === "files"}
|
||||
<div class="record-edit-file-preview">
|
||||
<div>
|
||||
<Preview {record} size="large"/>
|
||||
<Preview {record} size="large" />
|
||||
</div>
|
||||
<div class="file-details">
|
||||
|
||||
<div class="file-details-item">
|
||||
<span class="text-muted">Filename</span>
|
||||
<span>{record._file.path}</span>
|
||||
@@ -42,9 +41,13 @@
|
||||
<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>
|
||||
<a
|
||||
class="button primary"
|
||||
target="_blank"
|
||||
style="display: inline-flex"
|
||||
href={fileurl(channel, record)}>Download</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -45,25 +45,20 @@
|
||||
</script>
|
||||
|
||||
<div class="editor-field">
|
||||
<FieldHeader {field} {id}/>
|
||||
<FieldHeader {field} {id} />
|
||||
{#if field.info.name === "reference" && field.layout === "tags"}
|
||||
<ReferenceTags
|
||||
bind:graph
|
||||
{id}
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
||||
{:else if field.info.name === "reference"}
|
||||
<Reference
|
||||
bind:graph
|
||||
{id}
|
||||
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
||||
{:else if field.info.name === "file"}
|
||||
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
|
||||
<File
|
||||
bind:value={data[field.name]}
|
||||
{record}
|
||||
{id}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
{:else if field.info.name === "file"}
|
||||
<File bind:graph {record} {field} {validationErrors}/>
|
||||
{:else if field.info.name === "text"}
|
||||
<Text
|
||||
bind:value={data[field.name]}
|
||||
@@ -74,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
|
||||
@@ -90,13 +85,23 @@
|
||||
/>
|
||||
{:else if field.info.name === "rich"}
|
||||
<RichEditor
|
||||
bind:value={data[field.name]}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:graph
|
||||
{record}
|
||||
bind:value={data[field.name]}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:graph
|
||||
{record}
|
||||
/>
|
||||
{:else if field.info.name === "markdown"}
|
||||
<Markdown
|
||||
bind:value={data[field.name]}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:graph
|
||||
{record}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
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}
|
||||
|
||||
@@ -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} 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
|
||||
@@ -125,33 +123,31 @@
|
||||
</div>
|
||||
<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="revision"
|
||||
class:active={revision._sys.version ===
|
||||
selectedRevision?._sys.version}
|
||||
class="revision"
|
||||
class:active={revision.version ===
|
||||
selectedRevision?.version}
|
||||
>
|
||||
|
||||
<div class="version">
|
||||
<span>version {revision._sys.version}</span>
|
||||
<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="button"
|
||||
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}
|
||||
@@ -169,15 +165,13 @@
|
||||
<p class="text-center fw-bold mb-3 mt-5">
|
||||
If you choose to rollback to this revision
|
||||
</p>
|
||||
<button
|
||||
on:click={rollback}
|
||||
class="button"
|
||||
>
|
||||
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,31 +182,28 @@
|
||||
<!-- <div class="d-block" style="width:200px;">
|
||||
{field.label}
|
||||
</div> -->
|
||||
<div
|
||||
class="revision-field"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<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="compare-center">
|
||||
<span class="me-1">{field.label}</span>
|
||||
<Icon
|
||||
icon="angle-right"
|
||||
width="12"
|
||||
height="12"
|
||||
icon="angle-right"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
@@ -226,22 +217,16 @@
|
||||
{/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="revision-references"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<div class="revision-references" style="overflow:hidden">
|
||||
<div class="reference-field">
|
||||
{field}:
|
||||
</div>
|
||||
<div class="reference-compare">
|
||||
|
||||
<p class="">Record</p>
|
||||
{#each edges.record as edge}
|
||||
<RevisionEdgeRow {edge}/>
|
||||
<RevisionEdgeRow {edge} />
|
||||
{:else}
|
||||
<p>No references</p>
|
||||
{/each}
|
||||
@@ -249,7 +234,7 @@
|
||||
<div class="reference-compare">
|
||||
<p class="text-success">Revision</p>
|
||||
{#each edges.revision as edge}
|
||||
<RevisionEdgeRow {edge}/>
|
||||
<RevisionEdgeRow {edge} />
|
||||
{:else}
|
||||
<p>No references</p>
|
||||
{/each}
|
||||
@@ -258,7 +243,5 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<script>
|
||||
import {afterUpdate, createEventDispatcher, getContext, onMount} 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 "./header/ContentTabs.svelte";
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
import EditHeader from "./header/EditHeader.svelte";
|
||||
import axios from "axios";
|
||||
import Title from "./header/Title.svelte";
|
||||
import { apiPost, isEqual } from "../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -16,7 +20,7 @@
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
edges: []
|
||||
edges: [],
|
||||
};
|
||||
export let isCreateMode;
|
||||
let originalContent;
|
||||
@@ -25,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) => {
|
||||
@@ -53,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)),
|
||||
};
|
||||
}
|
||||
@@ -87,8 +87,6 @@
|
||||
data: record.data,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: record._sys,
|
||||
_file: record._file,
|
||||
edges: graph.edges,
|
||||
});
|
||||
}
|
||||
@@ -114,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();
|
||||
}
|
||||
@@ -150,64 +149,55 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={beforeUnload}/>
|
||||
<svelte:window on:beforeunload={beforeUnload} />
|
||||
|
||||
<div class="inline-edit record-edit">
|
||||
<div class="tools-header">
|
||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
|
||||
<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"
|
||||
/>
|
||||
<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}
|
||||
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"
|
||||
/>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<Title {schema} {record} {isCreateMode}/>
|
||||
<ErrorAlert message={errorMessage}/>
|
||||
<Title {schema} {record} {isCreateMode} />
|
||||
<ErrorAlert message={errorMessage} />
|
||||
|
||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
/>
|
||||
<FilePreview {record} {schema}/>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import Mustache from "mustache";
|
||||
import {stripHtml} from "../../helpers";
|
||||
|
||||
export function previewTitle(schemas, record, graph) {
|
||||
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
|
||||
if (!schema?.cardTitle) {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
let recordData = record.data;
|
||||
let render = Mustache.render(schema.cardTitle, recordData);
|
||||
if (!render || render === "") {
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
return stripHtml(render.slice(0, 300));
|
||||
}
|
||||
|
||||
function noTemplate(schema, record) {
|
||||
if (schema?.type === "files") {
|
||||
return record._file.path;
|
||||
}
|
||||
|
||||
let title = stripHtml(
|
||||
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
||||
).slice(0, 300);
|
||||
|
||||
if(title.trim() === ""){
|
||||
return "~Untitled~";
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
@@ -1,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}
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
export let validationErrors;
|
||||
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
export let id;
|
||||
let wrapperDiv;
|
||||
let pickerInput;
|
||||
let pickerInstance;
|
||||
let flatpickrOptions = {
|
||||
appendTo: wrapperDiv,
|
||||
static: true,
|
||||
allowInput: true,
|
||||
altInput: true,
|
||||
altFormat: "Y-m-d H:i:S",
|
||||
@@ -40,7 +44,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="mb-0" bind:this={wrapperDiv}>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -1,84 +1,69 @@
|
||||
<script>
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import { array_move } from "../../edges/sortEdges";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import Dialog from "../../dialog/Dialog.svelte";
|
||||
import {insertEdges} from "./reference.js";
|
||||
import {getContext} from "svelte";
|
||||
import FileDialog from "../../dialog/FileDialog.svelte";
|
||||
import Uploader from "../../files/Uploader.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
export let record;
|
||||
export let graph
|
||||
export let value = [];
|
||||
let browseModal;
|
||||
$: references = graph?.edges
|
||||
.filter((edge) => edge.field === field.name)
|
||||
.map((edge) => {
|
||||
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
|
||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||
|
||||
let collections = channel.schemas.filter((aschema) =>
|
||||
field.collections.includes(aschema.name)
|
||||
);
|
||||
|
||||
function removeReference(e) {
|
||||
function removeFile(e) {
|
||||
e.preventDefault();
|
||||
graph.edges = graph.edges.filter(
|
||||
(edge) => !(edge.target === e.detail && edge.field === field.name)
|
||||
);
|
||||
}
|
||||
|
||||
function openBrowseModal(e, schema) {
|
||||
e.preventDefault();
|
||||
browseModal.open(schema);
|
||||
value = value.filter((f) => !(f.id === e.detail));
|
||||
}
|
||||
|
||||
async function reorder(e) {
|
||||
|
||||
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
||||
|
||||
value = await array_move(value, e.detail.source, e.detail.target);
|
||||
}
|
||||
|
||||
function insert(e) {
|
||||
function insertFiles(e) {
|
||||
e.preventDefault();
|
||||
browseModal.close();
|
||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
||||
value = [...(value ?? []), ...(e.detail ?? [])];
|
||||
}
|
||||
|
||||
function replaceFiles(e) {
|
||||
e.preventDefault();
|
||||
browseModal.close();
|
||||
value = e.detail ?? [];
|
||||
}
|
||||
|
||||
function uploadComplete(e) {
|
||||
value = [...value, ...e.detail];
|
||||
}
|
||||
|
||||
function openBrowseModal(e) {
|
||||
e.preventDefault();
|
||||
browseModal.open(record.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
{#if field.collections.length === 1}
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, collections[0].name)}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
{:else}
|
||||
<Dropdown>
|
||||
<div slot="button">
|
||||
Browse
|
||||
</div>
|
||||
{#each collections as collection}
|
||||
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
|
||||
<a
|
||||
class="dropdown-item"
|
||||
on:click={(e) => openBrowseModal(e, collection.name)}
|
||||
href="/">{collection.label}</a
|
||||
>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{/if}
|
||||
<button class="button" on:click={openBrowseModal}> Browse </button>
|
||||
|
||||
<div>
|
||||
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
|
||||
</div>
|
||||
</div>
|
||||
{#if references.length > 0}
|
||||
{#if value.length > 0}
|
||||
<Sortable sortableClass="mt-3" on:update={reorder}>
|
||||
{#each references as reference (reference.id)}
|
||||
{#each value ?? [] as aFile (aFile.id)}
|
||||
<!--This div helps the sorting thing-->
|
||||
<div>
|
||||
<PreviewFile record={reference} hasDelete={true} on:remove={removeReference}></PreviewFile>
|
||||
<PreviewFile
|
||||
file={aFile}
|
||||
hasDelete={true}
|
||||
on:remove_file={removeFile}
|
||||
></PreviewFile>
|
||||
</div>
|
||||
{/each}
|
||||
</Sortable>
|
||||
{/if}
|
||||
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
|
||||
<FileDialog
|
||||
bind:this={browseModal}
|
||||
on:insert_files={insertFiles}
|
||||
on:replace_files={replaceFiles}
|
||||
></FileDialog>
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
<script>
|
||||
import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||
|
||||
|
||||
export let value;
|
||||
export let field;
|
||||
export let graph;
|
||||
export let record;
|
||||
export let isCreateMode;
|
||||
// export let id;
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
let editor;
|
||||
|
||||
function insertMedia(e){
|
||||
editor.insertMedia(e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-3">
|
||||
|
||||
<Codemirror bind:value editable={!field.readonly || isCreateMode} />
|
||||
|
||||
<Codemirror bind:this={editor} bind:value editable={!field.readonly || isCreateMode} />
|
||||
{#if field.collections.length > 0}
|
||||
<RichEditorFiles
|
||||
bind:graph
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
on:editor-insert={insertMedia}
|
||||
>
|
||||
</RichEditorFiles>
|
||||
{/if}
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block">
|
||||
{errorMessage}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {insertEdges} from "./reference";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import { getContext } from "svelte";
|
||||
import { insertEdges } from "./reference";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { sortByField } from "../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import PreviewReference from "../previews/PreviewReference.svelte";
|
||||
import axios from "axios";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
@@ -15,27 +14,36 @@
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
|
||||
$: references = graph.edges
|
||||
.filter((edge) => edge.field === field.name)
|
||||
.map((edge) => {
|
||||
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
|
||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||
$: references =
|
||||
graph.edges
|
||||
.filter((edge) => edge.field === field.name)
|
||||
.map((edge) => {
|
||||
return graph.records.find(
|
||||
(increc) =>
|
||||
increc.id === edge.target && record.id === edge.source,
|
||||
);
|
||||
})
|
||||
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||
|
||||
let collections = channel.schemas.filter((aschema) =>
|
||||
field.collections.includes(aschema.name)
|
||||
field.collections.includes(aschema.name),
|
||||
);
|
||||
|
||||
function removeReference(e) {
|
||||
e.preventDefault();
|
||||
graph.edges = graph.edges.filter(
|
||||
(edge) => !(edge.target === e.detail && edge.field === field.name)
|
||||
(edge) => !(edge.target === e.detail && edge.field === field.name),
|
||||
);
|
||||
}
|
||||
|
||||
function reorder(e) {
|
||||
|
||||
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
||||
graph.edges = sortByField(
|
||||
e.detail.source,
|
||||
e.detail.target,
|
||||
graph.edges,
|
||||
field.name,
|
||||
references,
|
||||
);
|
||||
}
|
||||
|
||||
function insert(e) {
|
||||
@@ -49,9 +57,14 @@
|
||||
// }).then(function (response) {
|
||||
// graph = response.data.graph;
|
||||
// })
|
||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
||||
graph = insertEdges(
|
||||
graph,
|
||||
record,
|
||||
e.detail.records,
|
||||
field.name,
|
||||
e.detail.action,
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if errorMessage}
|
||||
@@ -61,10 +74,10 @@
|
||||
{/if}
|
||||
<div class="inline-card-wrapper">
|
||||
<ReferenceInlineButtons
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
on:insert={insert}
|
||||
on:save={insert}
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
on:insert={insert}
|
||||
on:save={insert}
|
||||
/>
|
||||
</div>
|
||||
{#if references.length > 0}
|
||||
@@ -72,10 +85,10 @@
|
||||
{#each references as reference (reference.id)}
|
||||
<div>
|
||||
<PreviewReference
|
||||
{graph}
|
||||
record={reference}
|
||||
hasDelete={true}
|
||||
on:remove={removeReference}
|
||||
{graph}
|
||||
record={reference}
|
||||
hasDelete={true}
|
||||
on:remove={removeReference}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import InlineEdit from "../InlineEdit.svelte";
|
||||
import Dialog from "../../dialog/Dialog.svelte";
|
||||
import DialogRecord from "../../dialog/DialogRecord.svelte";
|
||||
import axios from "axios";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import { apiGet } from "../../../helpers";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
@@ -24,7 +24,7 @@
|
||||
e.preventDefault();
|
||||
console.log("Save inline");
|
||||
inLineCreateRecord = null;
|
||||
dialogRecord.close()
|
||||
dialogRecord.close();
|
||||
dispatch("save", {
|
||||
records: e.detail.records,
|
||||
after: recordId,
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
function createInlineReference(e, schemaUId) {
|
||||
e.preventDefault();
|
||||
axios
|
||||
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
||||
inLineCreateRecord = null;
|
||||
apiGet(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
||||
.then((response) => {
|
||||
inLineCreateRecord = response.data;
|
||||
dialogRecord.open()
|
||||
inLineCreateRecord = response;
|
||||
dialogRecord.open();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
@@ -56,58 +56,53 @@
|
||||
</script>
|
||||
|
||||
{#if schemas.length > 1}
|
||||
<div
|
||||
style="display: flex;align-items: center;gap:4px"
|
||||
>
|
||||
{#each schemas as schema}
|
||||
<Dropdown>
|
||||
<div slot="button" class:is-first={!recordId}>
|
||||
{schema.label}
|
||||
</div>
|
||||
<div style="display: flex;align-items: center;gap:4px">
|
||||
<Dropdown>
|
||||
<div slot="button">New</div>
|
||||
{#each schemas as schema}
|
||||
<button
|
||||
class=" button"
|
||||
on:click={(e) =>
|
||||
createInlineReference(e, schema.name)}
|
||||
>Create New Record
|
||||
class=" button"
|
||||
on:click={(e) => createInlineReference(e, schema.name)}
|
||||
>{schema.label}
|
||||
</button>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<div slot="button"><Icon icon="magnifying-glass" /></div>
|
||||
{#each schemas as schema}
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, schema.name)}
|
||||
>
|
||||
<Icon icon="magnifying-glass"/>
|
||||
Search
|
||||
</button
|
||||
>
|
||||
</Dropdown>
|
||||
{/each}
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, schema.name)}
|
||||
>{schema.label}
|
||||
</button>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
</div>
|
||||
{:else}
|
||||
<div style="display:flex;align-items: center;gap: 4px">
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => createInlineReference(e, schemas[0].name)}
|
||||
>New
|
||||
class="button"
|
||||
on:click={(e) => createInlineReference(e, schemas[0].name)}
|
||||
>New
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
||||
>
|
||||
<Icon icon="magnifying-glass"/>
|
||||
</button
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
||||
>
|
||||
<Icon icon="magnifying-glass" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DialogRecord bind:this={dialogRecord}>
|
||||
{#if inLineCreateRecord}
|
||||
<InlineEdit
|
||||
{...inLineCreateRecord}
|
||||
isCreateMode={true}
|
||||
on:cancel={(e) => (inLineCreateRecord = null)}
|
||||
on:inlinesaved={save}
|
||||
{...inLineCreateRecord}
|
||||
isCreateMode={true}
|
||||
on:cancel={(e) => (inLineCreateRecord = null)}
|
||||
on:inlinesaved={save}
|
||||
/>
|
||||
|
||||
{/if}
|
||||
</DialogRecord>
|
||||
|
||||
<Dialog bind:this={browseModal} on:insert={insert}/>
|
||||
<Dialog bind:this={browseModal} on:insert={insert} />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {debounce} from "lodash";
|
||||
import {previewTitle} from "../Preview";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {insertEdges} from "./reference.js";
|
||||
import { getContext } from "svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { insertEdges } from "./reference.js";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
@@ -15,20 +13,24 @@
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
$: references = graph.edges
|
||||
.filter((edge) => edge.field === field.name)
|
||||
.map((edge) => {
|
||||
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
|
||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||
|
||||
let search = ""
|
||||
$: searchOptions = []
|
||||
$: references =
|
||||
graph.edges
|
||||
.filter((edge) => edge.field === field.name)
|
||||
.map((edge) => {
|
||||
return graph.records.find(
|
||||
(increc) =>
|
||||
increc.id == edge.target && record.id == edge.source,
|
||||
);
|
||||
})
|
||||
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||
|
||||
let search = "";
|
||||
$: searchOptions = [];
|
||||
|
||||
function removeReference(e, recordId) {
|
||||
e.preventDefault();
|
||||
graph.edges = graph.edges.filter(
|
||||
(edge) => !(edge.target === recordId && edge.field === field.name)
|
||||
(edge) => !(edge.target === recordId && edge.field === field.name),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,14 +43,14 @@
|
||||
schema: field.collections[0],
|
||||
status: "published",
|
||||
data: {
|
||||
[field.searchField]: newValue
|
||||
}
|
||||
[field.searchField]: newValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
searchOptions = [];
|
||||
insert(e, response.data.records[0]);
|
||||
console.log(response)
|
||||
console.log(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
searchOptions = [];
|
||||
@@ -58,10 +60,16 @@
|
||||
|
||||
function insert(e, insertRecord) {
|
||||
e.preventDefault();
|
||||
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
|
||||
search = ""
|
||||
searchEl.focus()
|
||||
searchEl.blur()
|
||||
graph = insertEdges(
|
||||
graph,
|
||||
record,
|
||||
[insertRecord],
|
||||
field.name,
|
||||
e.detail.action,
|
||||
);
|
||||
search = "";
|
||||
searchEl.focus();
|
||||
searchEl.blur();
|
||||
}
|
||||
|
||||
const updateResults = debounce((e) => {
|
||||
@@ -82,9 +90,8 @@
|
||||
console.log(error);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="reference-tags">
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block mb-3">
|
||||
@@ -93,44 +100,39 @@
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="search"
|
||||
bind:this={searchEl}
|
||||
{id}
|
||||
on:keyup={updateResults}
|
||||
class:is-invalid={errorMessage}
|
||||
bind:value={search}
|
||||
placeholder={"Search for "+field.label}
|
||||
autocomplete="off"
|
||||
type="search"
|
||||
bind:this={searchEl}
|
||||
{id}
|
||||
on:keyup={updateResults}
|
||||
class:is-invalid={errorMessage}
|
||||
bind:value={search}
|
||||
placeholder={"Search for " + field.label}
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<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) => insert(e, option)}
|
||||
on:keypress={(e) => insert(e, option)}
|
||||
>
|
||||
{previewTitle(channel.schemas, option ,graph)}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div
|
||||
class="start-typing">
|
||||
Start typing...
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if search }
|
||||
<div
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => saveNew(e,search)}
|
||||
on:keypress={(e) => saveNew(e,search)}
|
||||
on:click={(e) => insert(e, option)}
|
||||
on:keypress={(e) => insert(e, option)}
|
||||
>
|
||||
{option.data.name}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="start-typing">Start typing...</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if search}
|
||||
<div
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => saveNew(e, search)}
|
||||
on:keypress={(e) => saveNew(e, search)}
|
||||
>
|
||||
Add "{search}"
|
||||
</div>
|
||||
@@ -142,26 +144,23 @@
|
||||
<div style="display: flex;align-items: center;gap: 4px">
|
||||
{#each references as record (record.id)}
|
||||
<span class="reference-tags-selected-value">
|
||||
<a
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
>
|
||||
{previewTitle(channel.schemas, record)}
|
||||
</a>
|
||||
<a
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
>
|
||||
{record.data.name}
|
||||
</a>
|
||||
|
||||
<button
|
||||
on:click|preventDefault={(e) => removeReference(e, record.id)}
|
||||
type="button"
|
||||
class="button-text"
|
||||
aria-label="Close"
|
||||
on:click|preventDefault={(e) =>
|
||||
removeReference(e, record.id)}
|
||||
type="button"
|
||||
class="button-text"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon width={12} height={12} icon="close"></Icon>
|
||||
</button>
|
||||
|
||||
<Icon width={12} height={12} icon="close"></Icon>
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Tinymce from "../../libs/Tinymce.svelte";
|
||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import Trix from "../../libs/Trix.svelte";
|
||||
|
||||
export let value;
|
||||
export let field;
|
||||
@@ -17,28 +17,23 @@
|
||||
readonly: field.readonly && !isCreateMode,
|
||||
};
|
||||
|
||||
function insertMedia(e){
|
||||
editor.insertMedia(e.detail)
|
||||
function insertMedia(e) {
|
||||
editor.insertMedia(e.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
|
||||
|
||||
<Tinymce bind:this={editor} bind:value {additionalConfig}/>
|
||||
<Trix {field} bind:this={editor} bind:value></Trix>
|
||||
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
|
||||
{#if field.collections.length > 0}
|
||||
<RichEditorFiles
|
||||
bind:graph
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
on:editor-insert={insertMedia}
|
||||
>
|
||||
|
||||
|
||||
</RichEditorFiles>
|
||||
bind:graph
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
on:editor-insert={insertMedia}
|
||||
></RichEditorFiles>
|
||||
{/if}
|
||||
<!-- <TipTap bind:value />-->
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script>
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import Dialog from "../../dialog/Dialog.svelte";
|
||||
@@ -70,7 +68,8 @@
|
||||
{#each references as reference (reference.id)}
|
||||
<!--This div helps the sorting thing-->
|
||||
<div>
|
||||
<PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference} on:editor-insert></PreviewFile>
|
||||
<PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference}
|
||||
on:editor-insert></PreviewFile>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getContext } from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
@@ -14,12 +13,11 @@
|
||||
|
||||
function generateId(e) {
|
||||
e.preventDefault();
|
||||
value = uuidv4();
|
||||
value = self.crypto.randomUUID();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<input
|
||||
type="text"
|
||||
@@ -31,13 +29,13 @@
|
||||
{readonly}
|
||||
/>
|
||||
{#if !readonly}
|
||||
<button
|
||||
class="btn btn-primary ms-2"
|
||||
title="Generate a new UUIDv4"
|
||||
on:click={generateId}
|
||||
>
|
||||
<Icon icon="dice" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary ms-2"
|
||||
title="Generate a new UUIDv4"
|
||||
on:click={generateId}
|
||||
>
|
||||
<Icon icon="dice" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import { uniqueId } from "lodash";
|
||||
import { getContext } from "svelte";
|
||||
const channelurl = getContext("channelurl");
|
||||
export let field;
|
||||
export let value;
|
||||
export let schema;
|
||||
let id = uniqueId();
|
||||
let id = self.crypto.randomUUID();
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
@@ -25,6 +24,6 @@
|
||||
placeholder="https://www.example.com"
|
||||
/>
|
||||
{#if field.help}
|
||||
<small class=" text-primary opacity-50">{field.help}</small>
|
||||
<small class=" text-primary opacity-50">{field.help}</small>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import {uniqBy} from "lodash";
|
||||
import { arrayUniqueCb } from "../../../helpers";
|
||||
|
||||
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") {
|
||||
let newEdges = targetRecords.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: sourceRecord.id,
|
||||
sourceSchema: sourceRecord.schema,
|
||||
targetSchema: r.schema,
|
||||
field: fieldName,
|
||||
depth: 1,
|
||||
rank: ""
|
||||
};
|
||||
});
|
||||
export function insertEdges(
|
||||
graph,
|
||||
sourceRecord,
|
||||
targetRecords,
|
||||
fieldName,
|
||||
action = "",
|
||||
) {
|
||||
let newEdges = targetRecords.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: sourceRecord.id,
|
||||
sourceSchema: sourceRecord.schema,
|
||||
targetSchema: r.schema,
|
||||
field: fieldName,
|
||||
depth: 1,
|
||||
rank: "",
|
||||
};
|
||||
});
|
||||
|
||||
let replacedEdges = graph.edges;
|
||||
if (action === "replace") {
|
||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||
}
|
||||
let replacedEdges = graph.edges;
|
||||
if (action === "replace") {
|
||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||
}
|
||||
|
||||
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id);
|
||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth);
|
||||
return graph;
|
||||
graph.records = arrayUniqueCb(
|
||||
[...graph.records, ...targetRecords],
|
||||
(r) => r.id,
|
||||
);
|
||||
graph.edges = arrayUniqueCb(
|
||||
[...replacedEdges, ...newEdges],
|
||||
(edge) => edge.source + edge.target + edge.field + edge.depth,
|
||||
);
|
||||
return graph;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import StatusSelect from "./StatusSelect.svelte";
|
||||
import { apiPost } from "../../../helpers";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
@@ -12,45 +13,42 @@
|
||||
|
||||
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 => {
|
||||
|
||||
});
|
||||
apiPost(channel.lucentUrl + "/records/clone/" + record.id)
|
||||
.then((response) => {
|
||||
window.location = channel.lucentUrl + "/records/" + response.id;
|
||||
})
|
||||
.catch((error) => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div style="display: flex;align-items: center; gap:10px;">
|
||||
{#if !isCreateMode}
|
||||
<Dropdown >
|
||||
<Dropdown>
|
||||
<div slot="button">
|
||||
<Icon icon="ellipsis"/>
|
||||
<Icon icon="ellipsis" />
|
||||
</div>
|
||||
<h6 class="dropdown-header">Record Actions</h6>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
>Create new</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}
|
||||
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
|
||||
on:click|preventDefault={(e) => (activeContentTab = "_info")}
|
||||
class="dropdown-item"
|
||||
href={channel.lucentUrl}>Revisions</a
|
||||
>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
|
||||
<StatusSelect bind:status={record.status} {record}></StatusSelect>
|
||||
</div>
|
||||
<StatusSelect bind:status={record.status} {record}></StatusSelect>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {previewTitle} from "./../Preview";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
export let record;
|
||||
export let isCreateMode;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="record-header">
|
||||
|
||||
<a
|
||||
class="schema-name"
|
||||
href="{channel.lucentUrl}/content/{schema.name}"
|
||||
>{schema.label.toUpperCase()}</a
|
||||
<a class="schema-name" href="{channel.lucentUrl}/content/{schema.name}"
|
||||
>{schema.label.toUpperCase()}</a
|
||||
>
|
||||
<span class="record-title">
|
||||
{#if !isCreateMode}
|
||||
{previewTitle(channel.schemas, record)}
|
||||
{record.data.name}
|
||||
{:else}
|
||||
New Record
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,85 +1,77 @@
|
||||
<script>
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
import {previewTitle} from "./../Preview";
|
||||
import {htmlurl} from "../../files/imageserver.js"
|
||||
import Status from "./../Status.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
export let file;
|
||||
export let hasDelete = false;
|
||||
export let hasInsert = false;
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(channel.schemas, record);
|
||||
let imagePresets = Object.keys(channel.imageFilters);
|
||||
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
dispatch("remove", record.id);
|
||||
dispatch("remove_file", file.id);
|
||||
}
|
||||
|
||||
function insert(e, preset) {
|
||||
e.preventDefault();
|
||||
let html = htmlurl(channel,record, preset)
|
||||
dispatch("editor-insert", html);
|
||||
// let html = htmlurl(channel, record, preset);
|
||||
// let url = !preset
|
||||
// ? `/${record._file.path}`
|
||||
// : `/templates/${preset}/${record._file.path}`;
|
||||
// dispatch("editor-insert", {
|
||||
// html: html,
|
||||
// url: channel.filesUrl + url,
|
||||
// originalUrl: channel.filesUrl + "/" + record._file.path,
|
||||
// record: record,
|
||||
// });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="preview-file">
|
||||
<div style="display: flex;align-items: center;gap: 10px;">
|
||||
<div class="image">
|
||||
<Preview {record} size="small"/>
|
||||
<Preview {file} size="small" />
|
||||
</div>
|
||||
<div class="title">
|
||||
<div>
|
||||
<a
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
>
|
||||
{cardTitle}
|
||||
</a>
|
||||
<small class="d-block">
|
||||
from {schema.label}
|
||||
{#if record.status === "draft"}
|
||||
<Status status={record.status}/>
|
||||
{/if}
|
||||
</small>
|
||||
|
||||
{file.filename}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;gap:4px; align-items: center; margin-right: 10px;">
|
||||
<div
|
||||
style="display: flex;gap:4px; align-items: center; margin-right: 10px;"
|
||||
>
|
||||
{#if hasInsert}
|
||||
<div class="reference-action">
|
||||
<Dropdown>
|
||||
<div slot="button">
|
||||
<Icon icon="photo-film"/>
|
||||
<Icon icon="photo-film" />
|
||||
</div>
|
||||
<button class="dropdown-item button" on:click={e => insert(e,null)}>original</button>
|
||||
<button
|
||||
class="dropdown-item button"
|
||||
on:click={(e) => insert(e, null)}>original</button
|
||||
>
|
||||
{#each imagePresets as preset}
|
||||
<button class="dropdown-item button" on:click={e => insert(e,preset)}>{preset}</button>
|
||||
<button
|
||||
class="dropdown-item button"
|
||||
on:click={(e) => insert(e, preset)}>{preset}</button
|
||||
>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
{#if hasDelete}
|
||||
<div class="reference-action">
|
||||
<button
|
||||
class="button"
|
||||
on:click={remove}
|
||||
>
|
||||
<Icon icon="trash-can"/>
|
||||
<button class="button" on:click={remove}>
|
||||
<Icon icon="trash-can" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script>
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import {previewTitle} from "./../Preview";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import Status from "./../Status.svelte";
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
|
||||
@@ -12,10 +11,15 @@
|
||||
export let record;
|
||||
export let hasDelete = false;
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(channel.schemas, record, graph);
|
||||
const cardImageEdge = graph.edges.find(e => e.source === record.id && e.field === schema.cardImage);
|
||||
let cardImageRecord = graph.records.find(r => r.id === cardImageEdge?.target);
|
||||
let schema = channel.schemas.find(
|
||||
(aschema) => aschema.name === record.schema,
|
||||
);
|
||||
const cardImageEdge = graph.edges.find(
|
||||
(e) => e.source === record.id && e.field === schema.cardImage,
|
||||
);
|
||||
let cardImageRecord = graph.records.find(
|
||||
(r) => r.id === cardImageEdge?.target,
|
||||
);
|
||||
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
@@ -23,43 +27,35 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="preview-reference">
|
||||
<div style="display: flex;align-items: center;gap: 10px;">
|
||||
|
||||
{#if cardImageRecord}
|
||||
<div class="image">
|
||||
<Preview record={cardImageRecord} size="small"/>
|
||||
<Preview record={cardImageRecord} size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="title">
|
||||
<div>
|
||||
<a
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
>
|
||||
{cardTitle}
|
||||
{record.data.name}
|
||||
</a>
|
||||
<small class="d-block">
|
||||
from {schema.label}
|
||||
{#if record.status === "draft"}
|
||||
<Status status={record.status}/>
|
||||
<Status status={record.status} />
|
||||
{/if}
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{#if hasDelete}
|
||||
<div class="reference-action">
|
||||
<button
|
||||
class="button"
|
||||
on:click={remove}
|
||||
>
|
||||
<Icon icon="trash-can"/>
|
||||
<button class="button" on:click={remove}>
|
||||
<Icon icon="trash-can" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
import PreviewCardSmall from "../PreviewCardSmall.svelte";
|
||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||
|
||||
export let field;
|
||||
export let side;
|
||||
@@ -8,26 +8,23 @@
|
||||
export let colorClass;
|
||||
</script>
|
||||
|
||||
{#if ["reference", "file"].includes(field.info.name)}
|
||||
{#if ["reference"].includes(field.info.name)}
|
||||
<div class="{colorClass} field-content">
|
||||
<div class="d-flex align-items-center text-center flex-wrap">
|
||||
{#each edges[field.name] as edgeRecord}
|
||||
{#if edgeRecord._file?.path}
|
||||
<div
|
||||
class="ms-2 "
|
||||
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
|
||||
>
|
||||
<Preview
|
||||
record={edgeRecord}
|
||||
size="small"
|
||||
showFilename={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ms-2 ">
|
||||
<PreviewCardSmall record={edgeRecord}/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ms-2">
|
||||
<PreviewCardSmall record={edgeRecord} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if ["file"].includes(field.info.name)}
|
||||
<div class="{colorClass} field-content">
|
||||
<div class="d-flex align-items-center text-center flex-wrap">
|
||||
{#each side as aFile}
|
||||
<div class="ms-2">
|
||||
<PreviewFile file={aFile} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +40,6 @@
|
||||
|
||||
<!-- {/if} -->
|
||||
<style>
|
||||
|
||||
.field-content {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
|
||||
Generated
+1825
-2401
File diff suppressed because it is too large
Load Diff
+23
-49
@@ -1,51 +1,25 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.2.5",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tiptap/core": "^2.6.4",
|
||||
"@tiptap/extension-blockquote": "^2.6.4",
|
||||
"@tiptap/extension-bold": "^2.6.4",
|
||||
"@tiptap/extension-bullet-list": "^2.6.4",
|
||||
"@tiptap/extension-code": "^2.6.4",
|
||||
"@tiptap/extension-document": "^2.6.4",
|
||||
"@tiptap/extension-heading": "^2.6.4",
|
||||
"@tiptap/extension-history": "^2.6.4",
|
||||
"@tiptap/extension-italic": "^2.6.4",
|
||||
"@tiptap/extension-list-item": "^2.6.4",
|
||||
"@tiptap/extension-ordered-list": "^2.6.4",
|
||||
"@tiptap/extension-paragraph": "^2.6.4",
|
||||
"@tiptap/extension-strike": "^2.6.4",
|
||||
"@tiptap/extension-table": "^2.6.4",
|
||||
"@tiptap/extension-table-cell": "^2.6.4",
|
||||
"@tiptap/extension-table-header": "^2.6.4",
|
||||
"@tiptap/extension-table-row": "^2.6.4",
|
||||
"@tiptap/extension-text": "^2.6.4",
|
||||
"@tiptap/extension-underline": "^2.6.4",
|
||||
"@tiptap/pm": "^2.6.4",
|
||||
"axios": "^1.7.4",
|
||||
"codemirror": "^6.0.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"fuse.js": "^7.0.0",
|
||||
"htmx.org": "^2.0.1",
|
||||
"laravel-vite-plugin": "^1.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "8.4.31",
|
||||
"sass": "^1.77.8",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte": "^4.2.18",
|
||||
"tinymce": "^6.8.4",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "5.2.6"
|
||||
}
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.2.5",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"fuse.js": "^7.0.0",
|
||||
"laravel-vite-plugin": "^1.0.5",
|
||||
"sass": "^1.77.8",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte": "^4.2.18",
|
||||
"trix": "^2.1.5",
|
||||
"vite": "5.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,48 @@
|
||||
.autocomplete {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
overflow: visible;
|
||||
.autocomplete-option {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 3px 10px;
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
border-radius: 12px;
|
||||
z-index: 1000;
|
||||
overflow: visible;
|
||||
.autocomplete-option {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 3px 10px;
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:focus-within {
|
||||
.autocomplete-results{
|
||||
display: flex;
|
||||
&:focus-within {
|
||||
.autocomplete-results {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.autocomplete-selected-value {
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--p30);
|
||||
padding: 3px 10px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
line-height: 22px;
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
}
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--p30);
|
||||
padding: 3px 10px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
line-height: 22px;
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-results {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
z-index: 20;
|
||||
background: var(--p30);
|
||||
//border: 1px solid var(--p40);
|
||||
transition: 600ms;
|
||||
flex-grow: 1;
|
||||
top: 45px;
|
||||
width: 100%;
|
||||
}
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
z-index: 20;
|
||||
background: var(--p30);
|
||||
//border: 1px solid var(--p40);
|
||||
transition: 600ms;
|
||||
flex-grow: 1;
|
||||
top: 45px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
|
||||
.cm-content{
|
||||
background-color: var(--p10);
|
||||
color: var(--p100);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-content{
|
||||
background-color: var(--p20);
|
||||
|
||||
}
|
||||
.ͼ4 .cm-line ::selection, .ͼ4 .cm-line::selection{
|
||||
background: var(--p40) !important;
|
||||
}
|
||||
|
||||
.cm-activeLine{
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
.flatpickr-wrapper {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
.flatpickr-calendar {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-month {
|
||||
background: var(--p30);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||
background: var(--p30);
|
||||
}
|
||||
|
||||
.flatpickr-weekdays{
|
||||
background: var(--p30);
|
||||
color: var(--text);
|
||||
|
||||
}
|
||||
|
||||
.flatpickr-weekdaycontainer .flatpickr-weekday{
|
||||
background: var(--p30);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.flatpickr-days{
|
||||
background: var(--p10);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.flatpickr-time{
|
||||
background: var(--p10);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ body:has(dialog[open]) {
|
||||
|
||||
dialog {
|
||||
margin: 2vh auto;
|
||||
background-color: #fff;
|
||||
background-color: var(--p10);
|
||||
padding: 34px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
@@ -49,6 +49,6 @@ dialog::backdrop {
|
||||
position: sticky;
|
||||
top: -34px;
|
||||
z-index: 999;
|
||||
background: #fff;
|
||||
background-color: var(--p10);
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
z-index: 20;
|
||||
z-index: 22;
|
||||
background: var(--p20);
|
||||
transition: 600ms;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.record-edit {
|
||||
position: relative;
|
||||
|
||||
max-width: 900px;
|
||||
.invalid-feedback {
|
||||
color: var(--text-error);
|
||||
font-size: 15px;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
.tiptap {
|
||||
width: 100%;
|
||||
background: var(--p20);
|
||||
border: 1px solid var(--p50);
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 15px 15px;
|
||||
font-size: 16px;
|
||||
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
&:focus {
|
||||
background: var(--p10);
|
||||
|
||||
}
|
||||
|
||||
img {
|
||||
&.ProseMirror-selectednode {
|
||||
box-shadow: 0 0 1px 2px var(--p70);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--p30);
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 5px 7px;
|
||||
|
||||
.button:not(.primary) {
|
||||
font-weight: 700;
|
||||
|
||||
&.active {
|
||||
background: var(--p40);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.tiptap {
|
||||
li > p {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trix-editor {
|
||||
background: var(--p20)!important;
|
||||
border: 1px solid var(--p50)!important;
|
||||
border-radius: 0 0 5px 5px!important;
|
||||
padding: 15px 15px!important;
|
||||
& > div {
|
||||
margin-bottom: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--p10)!important;
|
||||
|
||||
}
|
||||
figure.attachment{
|
||||
display: flex!important;
|
||||
flex-direction: column!important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.attachment {
|
||||
background: var(--p20);
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[data-trix-mutable].attachment img {
|
||||
box-shadow: 0 0 1px 2px var(--p70) !important;
|
||||
|
||||
}
|
||||
|
||||
.trix-button--remove {
|
||||
box-shadow: none !important;
|
||||
border: 2px solid var(--p40) !important;
|
||||
}
|
||||
|
||||
.trix-button--remove:hover {
|
||||
border: 2px solid var(--p40);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--p80);
|
||||
}
|
||||
}
|
||||
|
||||
trix-toolbar {
|
||||
.trix-button-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.trix-button-group {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
display: flex !important;
|
||||
gap: 4px;
|
||||
}
|
||||
.trix-button-group--history-tools,.trix-button-group--file-tools
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.trix-button {
|
||||
border-radius: 6px !important;
|
||||
background: var(--p30) !important;
|
||||
padding: 14px 22px !important;
|
||||
margin: 0 !important;
|
||||
cursor: pointer;
|
||||
border: 0px solid var(--p30) !important;
|
||||
font-size: 14px !important;
|
||||
min-height: 27px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 4px;
|
||||
color: var(--text) !important;
|
||||
|
||||
&:before{
|
||||
background-size: 22px!important;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: var(--p40) !important;
|
||||
}
|
||||
&.trix-active{
|
||||
background: var(--p50) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
background: var(--p20);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
@@ -29,10 +29,14 @@
|
||||
align-items: center;
|
||||
background: var(--p30);
|
||||
font-size: 16px;
|
||||
padding: 3px 12px 6px;
|
||||
padding: 3px 12px 3px;
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
&:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
|
||||
@@ -113,6 +113,18 @@
|
||||
.field-ui-number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.references{
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.reference{
|
||||
font-size: 13px;
|
||||
border-radius: 12px;
|
||||
background: var(--p30);
|
||||
padding: 1px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-table-row {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
margin: 20px 0 20px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
.tab{
|
||||
list-style: none;
|
||||
|
||||
|
||||
@@ -12,6 +12,20 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
h1{
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3{
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 16px;
|
||||
@@ -31,6 +45,54 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
code{
|
||||
background: var(--p30);
|
||||
padding: 0 6px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
img{
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
blockquote{
|
||||
border:1px solid var(--p30);
|
||||
border-radius: 12px;
|
||||
padding: 12px 40px;
|
||||
position: relative;
|
||||
|
||||
&::before{
|
||||
content: "\201C";
|
||||
color: var(--p60);
|
||||
font-size:4em;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&::after{
|
||||
content: '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--grey-light);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lx-small-text {
|
||||
|
||||
+2
-1
@@ -53,6 +53,7 @@
|
||||
@import "./table";
|
||||
@import "./avatar";
|
||||
@import "./codemirror";
|
||||
@import "./rich";
|
||||
@import "./layout";
|
||||
@import "./wrappers";
|
||||
@import "./toolbar";
|
||||
@@ -69,6 +70,7 @@
|
||||
@import "./reference-tags";
|
||||
@import "./members";
|
||||
@import "./revisions";
|
||||
@import "./datepicker";
|
||||
|
||||
body {
|
||||
background-color: var(--p10);
|
||||
@@ -103,4 +105,3 @@ a {
|
||||
.lucent-component {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
|
||||
<x-lucent::notice type="success" title="Success">
|
||||
<p>
|
||||
If you have provided a valid email you should receive in the following seconds a login email
|
||||
</p>
|
||||
|
||||
<p>You can safely close this tab</p>
|
||||
</x-lucent::notice>
|
||||
@@ -1,36 +0,0 @@
|
||||
@extends("lucent::layouts.account")
|
||||
|
||||
@section("content")
|
||||
<div class="scope-login">
|
||||
<div class="bg-image">
|
||||
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<div class="form">
|
||||
<h2 class="mb-5">Enter Lucent</h2>
|
||||
|
||||
<form hx-post="/lucent/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"
|
||||
name="email"
|
||||
class="form-control"
|
||||
id="emailaddress"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<x-lucent::button-indicator>
|
||||
Send email
|
||||
</x-lucent::button-indicator>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user