Compare commits
38 Commits
v1.2.13
...
085c307137
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -6,3 +6,4 @@ front/node_modules
|
|||||||
front/npm-debug.log
|
front/npm-debug.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/.claude
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "/phpactor.schema.json",
|
||||||
|
"language_server_phpstan.enabled": false
|
||||||
|
}
|
||||||
+2
-7
@@ -6,17 +6,13 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"ext-sqlite3": "*",
|
|
||||||
"ext-imagick": "*",
|
"ext-imagick": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"php": "^8.3",
|
"php": "^8.4",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
|
||||||
"intervention/image": "^2.7",
|
"intervention/image": "^2.7",
|
||||||
"phpoption/phpoption": "^1.9",
|
"phpoption/phpoption": "^1.9",
|
||||||
"spatie/image-optimizer": "^1.6",
|
"spatie/image-optimizer": "^1.6",
|
||||||
"staudenmeir/laravel-cte": "^1.0",
|
"staudenmeir/laravel-cte": "^1.0"
|
||||||
"mustache/mustache": "^2.14"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^1.8",
|
"phpstan/phpstan": "^1.8",
|
||||||
@@ -40,5 +36,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+635
-984
File diff suppressed because it is too large
Load Diff
+145
-298
@@ -5,374 +5,221 @@ include_toc: true
|
|||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
|
|
||||||
Fields are similar to a table's columns in a relational databases.
|
Fields define the columns of a schema. Each field has a `ui` type that controls both storage and the admin UI component rendered.
|
||||||
|
|
||||||
## Available fields for Collections and Files
|
## Common Optional Properties
|
||||||
|
|
||||||
|
Most fields share these optional properties:
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `required` | Whether the field must have a value to save as `published` |
|
||||||
|
| `nullable` | Allow saving as `null` |
|
||||||
|
| `help` | Help text shown below the input |
|
||||||
|
| `default` | Default value when creating a new record |
|
||||||
|
| `readonly` | Prevent editing from the UI |
|
||||||
|
| `group` | Tab group this field belongs to |
|
||||||
|
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
### text
|
### text
|
||||||
One-line text input
|
|
||||||
|
|
||||||
required
|
One-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
| `selectOptions` | Array of options. Strings or `[{value, label}]` objects |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### textarea
|
### textarea
|
||||||
textarea input
|
|
||||||
|
|
||||||
required
|
Multi-line text input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### slug
|
### slug
|
||||||
Slug input. Generates automatically if left empty
|
|
||||||
|
|
||||||
required
|
Slug input. Auto-generates from a source field if left empty.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `source`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **source**: The source field from which it generates
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `source` | Field name to generate the slug from |
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### rich
|
### rich
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
WYSIWYG rich text editor.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
### markdown
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
Markdown editor.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum character count |
|
||||||
|
| `max` | Maximum character count |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### number
|
### number
|
||||||
Any numeric value
|
|
||||||
|
|
||||||
required
|
Numeric input.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `decimals` | Number of decimal places. Default: `0` |
|
||||||
|
| `min` | Minimum value |
|
||||||
|
| `max` | Maximum value |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **decimals**: default is 0
|
---
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### checkbox
|
### checkbox
|
||||||
True or false
|
|
||||||
|
|
||||||
required
|
Boolean true/false toggle.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### color
|
### color
|
||||||
Color picker
|
|
||||||
|
|
||||||
required
|
Color picker.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `selectOptions` | Restrict to a predefined palette |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
| `optionsSuggest` | Allow typing new values not in the options list |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### date
|
### date
|
||||||
Date select
|
|
||||||
|
|
||||||
required
|
Date selector. Stores as a date string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum date |
|
||||||
|
| `max` | Maximum date |
|
||||||
|
| `selectOptions` | Predefined date options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### datetime
|
### datetime
|
||||||
Date and time selector
|
|
||||||
|
|
||||||
required
|
Date and time selector. Stores as an ISO 8601 string.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `min` | Minimum datetime |
|
||||||
|
| `max` | Maximum datetime |
|
||||||
|
| `selectOptions` | Predefined datetime options |
|
||||||
|
| `optionsFrom` | Schema name to load options from |
|
||||||
|
| `optionsField` | Field from `optionsFrom` to use as the value |
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
---
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum date
|
### uuid
|
||||||
- **max**: Maximum date
|
|
||||||
- **help**: Help text
|
UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID.
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
**Required:** `name`, `label`
|
||||||
- **optionsFrom**: Schema to choose options from
|
|
||||||
- **optionsField**: Field's value to insert
|
---
|
||||||
- **optionsSuggest**: Allow to insert new values
|
|
||||||
- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]`
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### json
|
### json
|
||||||
Json data
|
|
||||||
|
|
||||||
required
|
Raw JSON data field. Accepts either a JSON string or a plain array — both are stored as JSON.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
---
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **min**: Minimum characters
|
|
||||||
- **max**: Maximum characters
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
### file
|
### file
|
||||||
Upload or select files
|
|
||||||
|
|
||||||
required
|
Upload or select files from a files schema. Files can be uploaded directly or browsed from previously uploaded files. Image files (jpeg, png, webp, gif, tiff) automatically get a 300×300 thumbnail generated on upload.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
- **mime**: The mime types allowed to select
|
| `collections` | Array of file schema names to choose from |
|
||||||
- **nullable**: Can the field be saved as null
|
| `mime` | Allowed MIME types e.g. `["image/*"]` |
|
||||||
- **min**: Minimum files
|
| `min` | Minimum number of files |
|
||||||
- **max**: Maximum files
|
| `max` | Maximum number of files |
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### reference
|
### reference
|
||||||
Reference other records
|
|
||||||
|
|
||||||
required
|
Reference records from another collection.
|
||||||
|
|
||||||
- **name**: The id of the field
|
**Required:** `name`, `label`, `collections`
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
| Property | Description |
|
||||||
|
|---|---|
|
||||||
|
| `collections` | Array of collection schema names to reference |
|
||||||
|
| `min` | Minimum number of references |
|
||||||
|
| `max` | Maximum number of references |
|
||||||
|
|
||||||
- **nullable**: Can the field be saved as null
|
---
|
||||||
- **min**: Minimum files
|
|
||||||
- **max**: Maximum files
|
|
||||||
- **help**: Help text
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
|
## Example Field
|
||||||
|
|
||||||
### block
|
```json
|
||||||
The block editor
|
{
|
||||||
|
"ui": "text",
|
||||||
required
|
"name": "title",
|
||||||
|
"label": "Title",
|
||||||
- **name**: The id of the field
|
"required": true,
|
||||||
- **label**: The friendly name of the field
|
"min": 3,
|
||||||
- **schema**: The block schema name
|
"max": 200,
|
||||||
|
"help": "The main title of the post"
|
||||||
optional
|
}
|
||||||
|
```
|
||||||
- **required**: Is the field required to save the record
|
|
||||||
- **nullable**: Can the field be saved as null
|
|
||||||
- **help**: Help text
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
- **readonly**: Cannot edit this value from the UI
|
|
||||||
- **group**: The group that this field belongs to
|
|
||||||
|
|
||||||
)
|
|
||||||
## Available fields for the Block Editor
|
|
||||||
|
|
||||||
### heading
|
|
||||||
Single-line text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### textarea
|
|
||||||
Multiline text
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### rich
|
|
||||||
WYSIWYG editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
|
|
||||||
### markdown
|
|
||||||
Markdown editor
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
|
|
||||||
optional
|
|
||||||
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### file
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: File collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **mime**: The mime types allowed to select
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|
||||||
### reference
|
|
||||||
Choose files
|
|
||||||
|
|
||||||
required
|
|
||||||
|
|
||||||
- **name**: The id of the field
|
|
||||||
- **label**: The friendly name of the field
|
|
||||||
- **collections**: Collections to choose from
|
|
||||||
|
|
||||||
optional
|
|
||||||
- **min**: Minimum date
|
|
||||||
- **max**: Maximum date
|
|
||||||
- **default**: Default value when creating new record
|
|
||||||
|
|||||||
+133
-52
@@ -5,88 +5,169 @@ include_toc: true
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
|
|
||||||
## Graph or Tree
|
The `Query` class is the main way to fetch records. Inject it via the Laravel container.
|
||||||
|
|
||||||
Queries can return results in 2 formats. A graph or a tree.
|
|
||||||
|
|
||||||
Graphs results are a collection of records (nodes) and a collection of edges. This format is more useful for network visualization.
|
|
||||||
|
|
||||||
The tree format is more straightforward as it returns a collection of records. Each record has a **_children** and a **_parents** field and the tree can continue upwards or downwards according to the depth you have requested.
|
|
||||||
|
|
||||||
For example to request records with their children and their children's children:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->childrenDepth(2);
|
public function __construct(private Query $query) {}
|
||||||
```
|
```
|
||||||
Maybe you only want to get a specific type of relationship:
|
|
||||||
|
|
||||||
```php
|
## Return Formats
|
||||||
$query->childrenDepth(2)->childrenFields(["categories"]);
|
|
||||||
```
|
Queries return results in two formats:
|
||||||
|
|
||||||
|
**Graph** — via `->run()`. Returns a `Graph` object with `records` and `edges` collections. Useful for network-style data.
|
||||||
|
|
||||||
|
**Tree** — via `->tree()`. Returns a flat `Collection` of records where each record has `_children` and `_parents` arrays populated based on the requested depth. This is the most common format.
|
||||||
|
|
||||||
|
## Chaining Methods
|
||||||
|
|
||||||
|
All methods return `$this` and can be chained:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `->filter(array)` | Add an AND filter |
|
||||||
|
| `->orFilter(array)` | Add an OR filter (grouped) |
|
||||||
|
| `->limit(int)` | Max number of root records to return |
|
||||||
|
| `->skip(int)` | Offset for pagination |
|
||||||
|
| `->sort(string)` | Sort by field. Prefix with `-` for descending e.g. `"-_sys.updatedAt"` |
|
||||||
|
| `->status(array)` | Filter by status array e.g. `["published", "draft"]` |
|
||||||
|
| `->onlyPublished()` | Shorthand for `->status(["published"])` |
|
||||||
|
| `->childrenDepth(int)` | How many levels of children to load |
|
||||||
|
| `->childrenLimit(int)` | Max children per record |
|
||||||
|
| `->childrenFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->parentsDepth(int)` | How many levels of parents to load |
|
||||||
|
| `->parentsLimit(int)` | Max parents per record |
|
||||||
|
| `->parentFields(array)` | Only follow specific relationship fields |
|
||||||
|
| `->notLinked(string)` | Return only records with no parents |
|
||||||
|
| `->run()` | Execute and return a `Graph` |
|
||||||
|
| `->tree()` | Execute and return a `Collection` (tree format) |
|
||||||
|
| `->runWithCount()` | Execute and return a `Graph` with a `total` count |
|
||||||
|
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
You can filter your query with the following format:
|
Filter keys use the format `field_operator`. When no operator suffix is given, `eq` is assumed.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query->filter(["field_operator" => "value"]);
|
$query->filter(["field_operator" => "value"]);
|
||||||
|
|
||||||
// example:
|
// No operator = eq
|
||||||
|
$query->filter(["schema" => "blogPosts"]);
|
||||||
|
|
||||||
$query->filter(["date_lt" => "2020-09-15"]);
|
// With operator
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Or filters are also available:
|
### Operator Reference
|
||||||
|
|
||||||
|
| Operator | Description |
|
||||||
|
|---|---|
|
||||||
|
| _(none)_ or `eq` | Equals (string) |
|
||||||
|
| `ne` | Not equals (string) |
|
||||||
|
| `eqnum` | Equals (numeric) |
|
||||||
|
| `neqnum` | Not equals (numeric) |
|
||||||
|
| `eqtrue` | Equals `true` |
|
||||||
|
| `eqfalse` | Equals `false` |
|
||||||
|
| `netrue` | Not equals `true` |
|
||||||
|
| `nefalse` | Not equals `false` |
|
||||||
|
| `regex` | Regular expression match |
|
||||||
|
| `in` | Value is in array (strings) |
|
||||||
|
| `nin` | Value is not in array (strings) |
|
||||||
|
| `innum` | Value is in array (numeric) |
|
||||||
|
| `ninnum` | Value is not in array (numeric) |
|
||||||
|
| `lt` | Less than |
|
||||||
|
| `lte` | Less than or equal |
|
||||||
|
| `gt` | Greater than |
|
||||||
|
| `gte` | Greater than or equal |
|
||||||
|
| `null` | Field is null |
|
||||||
|
| `nnull` | Field is not null |
|
||||||
|
| `exists` | Field key exists |
|
||||||
|
| `nexists` | Field key does not exist |
|
||||||
|
| `filter` | Raw filter passthrough |
|
||||||
|
|
||||||
|
### OR Filters
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$query
|
$query
|
||||||
->filter(["price_eqn" => 10])
|
->filter(["schema" => "blogPosts"])
|
||||||
->orFilter(["title_regex" => "search", "slug_regex" => "search"])
|
->orFilter(["title_regex" => "search", "slug_regex" => "search"]);
|
||||||
;
|
|
||||||
```
|
```
|
||||||
## Operator List
|
|
||||||
|
|
||||||
- regex
|
Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together.
|
||||||
- eq
|
|
||||||
- ne
|
|
||||||
- eqnum
|
|
||||||
- neqnum
|
|
||||||
- filter
|
|
||||||
- eqtrue
|
|
||||||
- eqfalse
|
|
||||||
- netrue
|
|
||||||
- nefalse
|
|
||||||
- in:
|
|
||||||
- innum
|
|
||||||
- nin
|
|
||||||
- ninnum
|
|
||||||
- lt
|
|
||||||
- lte
|
|
||||||
- gt
|
|
||||||
- gte
|
|
||||||
- null
|
|
||||||
- nnull
|
|
||||||
- exists
|
|
||||||
- nexists
|
|
||||||
|
|
||||||
## Example
|
### Cross-Schema (Children) Filters
|
||||||
|
|
||||||
Get 10 posts from the sports category as a tree
|
You can filter records by properties of their related records using the `children.` prefix:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query->filter([
|
||||||
|
"schema" => "blogPosts",
|
||||||
|
"children.categories.data.slug" => "sports",
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns `blogPosts` that have a child in `categories` where `slug` equals `sports`.
|
||||||
|
|
||||||
|
|
||||||
|
## Nested Data Fields
|
||||||
|
|
||||||
|
Use dot notation to filter on JSON data fields and system fields:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Filter on a data field
|
||||||
|
$query->filter(["data.status_eq" => "draft"]);
|
||||||
|
|
||||||
|
// Filter on a system field
|
||||||
|
$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Get published blog posts, newest first:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$posts = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->onlyPublished()
|
||||||
|
->sort("-_sys.updatedAt")
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get posts from a specific category with children loaded:**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$posts = $query
|
$posts = $query
|
||||||
->filter([
|
->filter([
|
||||||
"schema" => "posts",
|
"schema" => "blogPosts",
|
||||||
"children.categories.data.slug" => "sports",
|
"children.categories.data.slug" => "sports",
|
||||||
]
|
])
|
||||||
)
|
|
||||||
->childrenDepth(1)
|
->childrenDepth(1)
|
||||||
|
->childrenFields(["categories"])
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->tree();
|
->tree();
|
||||||
|
|
||||||
$posts->map(...)->toArray();
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Paginate records:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$graph = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->limit(20)
|
||||||
|
->skip(40)
|
||||||
|
->runWithCount();
|
||||||
|
|
||||||
|
$posts = $graph->tree();
|
||||||
|
$total = $graph->total;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Search across fields:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$results = $query
|
||||||
|
->filter(["schema" => "blogPosts"])
|
||||||
|
->orFilter(["title_regex" => $term, "slug_regex" => $term])
|
||||||
|
->limit(10)
|
||||||
|
->tree();
|
||||||
|
```
|
||||||
|
|||||||
+77
-38
@@ -5,70 +5,106 @@ include_toc: true
|
|||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
||||||
Schemas define both the shape of your data and how the UI on the admin will behave.
|
Schemas define both the shape of your data and how the admin UI behaves.
|
||||||
|
|
||||||
There are 3 types of schemas
|
There are 2 types of schemas:
|
||||||
|
|
||||||
- Collections: Normal data
|
- **collection** — Regular data records
|
||||||
- Files: Images and files
|
- **files** — Images and file uploads
|
||||||
- Block: Used in the block editor
|
|
||||||
|
|
||||||
|
|
||||||
## Collection Reference
|
## Collection Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "collection"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` |
|
||||||
- **visible**: An array of field id to show on the content browser _optional_
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"collection"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
|
| `visible` | no | Field IDs to show in the content browser list |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **cardImage**: Field name of image you want to use as a preview image _optional_
|
| `sortBy` | no | Default sort in browser. Prefix with `-` for descending e.g. `-_sys.updatedAt` |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `cardTitle` | no | Mustache template for the record preview title e.g. `{{name}} - {{slug}}` |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `revisions` | no | Number of revisions to keep per record. Default: `0` (disabled) |
|
||||||
|
| `read` | no | Roles with read access. Empty means all roles can read |
|
||||||
|
| `write` | no | Roles with write access. Empty means all roles can write |
|
||||||
|
|
||||||
|
|
||||||
## Files Reference
|
## Files Reference
|
||||||
|
|
||||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
| Field | Required | Description |
|
||||||
- **label**: The friendly name of the schema
|
|---|---|---|
|
||||||
- **type**: The type of the collection. Should be "files"
|
| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` |
|
||||||
- **path**: The relative directory that these files will be stored.
|
| `label` | yes | Friendly display name |
|
||||||
- **groups**: A list if group ids to separate your field in different tabs _optional_
|
| `type` | yes | Must be `"files"` |
|
||||||
- **fields**: The list of your fields. Look the field reference for more
|
| `fields` | yes | Array of field definitions. See [Fields](Fields.md) |
|
||||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
| `groups` | no | Group IDs to split fields into tabs |
|
||||||
- **sortBy**: The default sorting in the content browser _optional_
|
| `isEntry` | no | Show in sidebar. Default: `false` |
|
||||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
| `sortBy` | no | Default sort in browser |
|
||||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
| `cardTitle` | no | Mustache template for the record preview title |
|
||||||
- **read**: Array of user groups that have read permissions _optional_
|
| `cardImage` | no | Field name to use as the preview image |
|
||||||
- **write**: Array of user groups that have write permissions _optional_
|
| `revisions` | no | Number of revisions to keep per record |
|
||||||
|
| `read` | no | Roles with read access |
|
||||||
|
| `write` | no | Roles with write access |
|
||||||
|
|
||||||
|
### File Metadata
|
||||||
|
|
||||||
|
Every uploaded file is automatically stored with the following metadata (stored in `lucent_files`):
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `id` | Unique file ID |
|
||||||
|
| `recordId` | ID of the record this file belongs to |
|
||||||
|
| `originalName` | Original filename as uploaded |
|
||||||
|
| `mime` | MIME type e.g. `image/webp` |
|
||||||
|
| `path` | Storage path e.g. `files/{recordId}/{filename}` |
|
||||||
|
| `size` | File size in bytes |
|
||||||
|
| `width` | Image width in pixels (0 for non-images) |
|
||||||
|
| `height` | Image height in pixels (0 for non-images) |
|
||||||
|
| `checksum` | SHA-1 hash of the file contents |
|
||||||
|
|
||||||
|
Image files (jpeg, png, webp, gif, tiff) also get a 300×300 thumbnail generated automatically at `thumbs/{path}`, plus any image filter presets configured in `lucent.imageFilters`.
|
||||||
|
|
||||||
|
|
||||||
A full Collection example without the fields:
|
## System Fields
|
||||||
|
|
||||||
|
Every record automatically has these read-only system fields available in queries:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `_sys.createdAt` | ISO 8601 creation timestamp |
|
||||||
|
| `_sys.updatedAt` | ISO 8601 last update timestamp |
|
||||||
|
| `_sys.createdBy` | User ID who created the record |
|
||||||
|
| `_sys.updatedBy` | User ID who last updated the record |
|
||||||
|
| `_sys.version` | Revision version number |
|
||||||
|
| `status` | `draft` or `published` |
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"label": "Departments",
|
"schemas": [
|
||||||
"name": "departments",
|
{
|
||||||
|
"label": "Blog Posts",
|
||||||
|
"name": "blogPosts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"visible": [
|
"visible": [
|
||||||
|
"title",
|
||||||
"slug",
|
"slug",
|
||||||
"cover",
|
|
||||||
"_sys.updatedAt",
|
"_sys.updatedAt",
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
"Extra Info",
|
"Content",
|
||||||
"Metadata",
|
|
||||||
"SEO"
|
"SEO"
|
||||||
],
|
],
|
||||||
"sortBy": "-_sys.createdAt",
|
"sortBy": "-_sys.createdAt",
|
||||||
"schemaTitle": "{{name}} {{slug}}",
|
"cardTitle": "{{title}}",
|
||||||
"schemaImage": "cover",
|
"cardImage": "cover",
|
||||||
"revisions": 15,
|
"revisions": 15,
|
||||||
"read": [
|
"read": [
|
||||||
"admin",
|
"admin",
|
||||||
@@ -81,4 +117,7 @@ A full Collection example without the fields:
|
|||||||
],
|
],
|
||||||
"fields": []
|
"fields": []
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"roles": ["admin", "editors", "reviewers"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
+87
-28
@@ -5,51 +5,94 @@ To generate the static content of the website, lucent provides a helper class.
|
|||||||
|
|
||||||
## Laravel Command
|
## Laravel Command
|
||||||
|
|
||||||
Just create a command and name it as you like. Make sure to inject the StaticGenerator class.
|
Create an Artisan command and inject `StaticGenerator`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
class GenerateStatic extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'generate:static';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public StaticGenerator $staticGenerator,
|
public StaticGenerator $staticGenerator,
|
||||||
|
public Context $ctx,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
## Redirect command
|
public function handle(): void
|
||||||
|
{
|
||||||
There are cases which is useful to create a redirect like when you have a multilingual website:
|
$this->staticGenerator->run('generate:static', function ($writer) {
|
||||||
|
|
||||||
```php
|
|
||||||
$this->staticGenerator->run(function ($writer) {
|
|
||||||
$writer->createRedirect("/", "/el");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Writer save command
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```php
|
|
||||||
$this->staticGenerator->run(function ($writer) {
|
|
||||||
$writer->save("/", $this->ctx->render("homepage"));
|
$writer->save("/", $this->ctx->render("homepage"));
|
||||||
$writer->save("/about", $this->ctx->render("about"));
|
$writer->save("/about", $this->ctx->render("about"));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`run(string $signature, callable $callback)` — the first argument must match the command's artisan signature. This is used to stream live build logs in the Lucent UI. The callback receives a `Writer` instance.
|
||||||
|
|
||||||
|
|
||||||
|
## Writer: save
|
||||||
|
|
||||||
|
Writes an HTML file at the given path:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$writer->save("/blog/my-post", $html);
|
||||||
|
// writes to: storage/lucent/build/blog/my-post/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
An optional third argument changes the file extension (default `"html"`).
|
||||||
|
|
||||||
|
|
||||||
|
## Writer: createRedirect
|
||||||
|
|
||||||
|
Generates an HTML meta-refresh redirect. Useful for root-level locale redirects:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$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
|
## Storage and Permissions
|
||||||
|
|
||||||
All the generated html is stored on `storage/lucent/live`
|
All generated HTML is written to `storage/lucent/build` during the run, then atomically swapped to `storage/lucent/live` on completion. Create the public symlink with:
|
||||||
In order to make it accessible you have to create symlink:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan lucent:livelink
|
php artisan lucent:livelink
|
||||||
```
|
```
|
||||||
|
|
||||||
Now your static website is accessible in http://localhost:8000/live
|
The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost:
|
||||||
|
|
||||||
But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost
|
|
||||||
|
|
||||||
```nginxconf
|
```nginxconf
|
||||||
location / {
|
location / {
|
||||||
@@ -57,14 +100,30 @@ location / {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Artisan Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent:setup-db` | Create all Lucent database tables |
|
||||||
|
| `lucent:schemas` | Compile schema JSON files |
|
||||||
|
| `lucent:livelink` | Create the `public/live` symlink |
|
||||||
|
| `lucent:rebuild:thumbnails` | Regenerate thumbnails for all uploaded images |
|
||||||
|
| `lucent:removeOrphanEdges` | Remove edges pointing to deleted records |
|
||||||
|
| `lucent:generate:collection {name}` | Scaffold a new collection schema JSON file |
|
||||||
|
|
||||||
|
|
||||||
## Build from the Lucent UI
|
## Build from the Lucent UI
|
||||||
|
|
||||||
In your lucent.php config file you can define your command that is used to generate the static files:
|
Register your generate command in `config/lucent.php` so admin users can trigger it from the UI:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
"generateCommand" => "generate:static"
|
"commands" => [
|
||||||
|
"generate:static" => "Generate Static Site",
|
||||||
|
],
|
||||||
```
|
```
|
||||||
That way, the users will be able to initiate the build command through the user interface.
|
|
||||||
|
Only roles listed in `canBuild` can trigger commands from the UI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,3 +99,113 @@ class Context
|
|||||||
```
|
```
|
||||||
|
|
||||||
Add the middleware inside the HTTP Kernel file.
|
Add the middleware inside the HTTP Kernel file.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
After publishing the config (`php artisan vendor:publish --tag=lucent-config`), configure `config/lucent.php` via `.env`:
|
||||||
|
|
||||||
|
| Key | Env var | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `env` | `LUCENT_ENV` | `production` | `production` or `development` |
|
||||||
|
| `auth` | `LUCENT_AUTH` | `lucent` | Auth driver: `lucent` or `lunar` |
|
||||||
|
| `disk` | `LUCENT_DISK` | `public` | Laravel filesystem disk for uploads |
|
||||||
|
| `schemas_path` | `LUCENT_SCHEMAS_PATH` | `resources/lucent/schemas` | Where schema JSON files live |
|
||||||
|
| `database` | `LUCENT_DB_CONNECTION` | `DB_CONNECTION` | Database connection name |
|
||||||
|
| `name` | `LUCENT_NAME` | `Lucent` | CMS display name |
|
||||||
|
| `url` | `LUCENT_URL` | `APP_URL` | Base URL of the CMS |
|
||||||
|
| `previewTarget` | `LUCENT_PREVIEW_TARGET` | `previewTarget` | Preview route parameter |
|
||||||
|
| `commands` | — | `[]` | Artisan commands exposed to admin users (see [Static Generator](Static%20Generator.md)) |
|
||||||
|
| `imageFilters` | — | `[]` | Image filter presets applied to uploads (see below) |
|
||||||
|
| `canInvite` | — | `["admin"]` | Roles that can invite new users |
|
||||||
|
| `canBuild` | — | `["admin"]` | Roles that can trigger static builds |
|
||||||
|
| `systemUserId` | — | `""` | User ID used for console-initiated record writes |
|
||||||
|
|
||||||
|
|
||||||
|
## Image Filters
|
||||||
|
|
||||||
|
Image filters are Intervention Image filter classes applied to every uploaded image, producing additional versions (e.g. a cropped or watermarked variant). Each preset is stored at `templates/{name}/{original-path}` on the configured disk.
|
||||||
|
|
||||||
|
Define filters in `config/lucent.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
"imageFilters" => [
|
||||||
|
"hero" => \App\ImageFilters\HeroFilter::class,
|
||||||
|
"thumb" => \App\ImageFilters\ThumbFilter::class,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
A filter class must implement `Intervention\Image\Filters\FilterInterface`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\ImageFilters;
|
||||||
|
|
||||||
|
use Intervention\Image\Filters\FilterInterface;
|
||||||
|
use Intervention\Image\Image;
|
||||||
|
|
||||||
|
class HeroFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function applyFilter(Image $image): Image
|
||||||
|
{
|
||||||
|
return $image->fit(1200, 600)->encode('webp', 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every uploaded image automatically gets a 300×300 WebP thumbnail at `thumbs/{path}` in addition to any configured presets.
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Lucent uses **email-link (magic link) login** — no passwords. Users receive a time-limited link by email.
|
||||||
|
|
||||||
|
### Auth modes
|
||||||
|
|
||||||
|
Set `LUCENT_AUTH` to choose how users are stored:
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent` | Users stored in the `lucent_users` table. Supports roles. |
|
||||||
|
| `lunar` | Delegates to Lunar's `lunar_staff` table. Roles not supported. |
|
||||||
|
|
||||||
|
### Login flow
|
||||||
|
|
||||||
|
1. User submits their email at `/lucent/login`
|
||||||
|
2. A 32-character token is stored against the user with a timestamp
|
||||||
|
3. Lucent emails a login link containing the token
|
||||||
|
4. User clicks the link → token is validated (must be used within **1 hour**)
|
||||||
|
5. Session is established; token is cleared
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
|
||||||
|
The `/lucent/register` route is only available when **no users exist** in the system. Once the first admin registers, the route returns a redirect to `/lucent/login`. Registration automatically assigns the `admin` role and sends a login link.
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
Two middleware aliases are available for your routes:
|
||||||
|
|
||||||
|
| Alias | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent.auth` | Requires an active Lucent session |
|
||||||
|
| `lucent.guest` | Redirects authenticated users away |
|
||||||
|
|
||||||
|
### Roles
|
||||||
|
|
||||||
|
Roles are defined in your channel config. Only roles listed there are valid — anything else is silently stripped on assignment. The `canInvite` and `canBuild` config keys control which roles can invite users and trigger builds respectively.
|
||||||
|
|
||||||
|
On console commands and non-`/lucent` routes, `currentUserId()` returns `config("lucent.systemUserId")` instead of the session user.
|
||||||
|
|
||||||
|
|
||||||
|
## Database Tables
|
||||||
|
|
||||||
|
All Lucent-managed tables use the `lucent_` prefix:
|
||||||
|
|
||||||
|
| Table | Description |
|
||||||
|
|---|---|
|
||||||
|
| `lucent_records` | All collection and file schema records |
|
||||||
|
| `lucent_files` | Uploaded file metadata |
|
||||||
|
| `lucent_edges` | Relationships between records |
|
||||||
|
| `lucent_revisions` | Record revision history |
|
||||||
|
| `lucent_users` | Users (when using `lucent` auth mode) |
|
||||||
|
| `lucent_command_logs` | Background command execution logs |
|
||||||
|
|
||||||
|
These are created automatically by running `php artisan lucent:setup`.
|
||||||
Vendored
-342
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
+212
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
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"main.js": {
|
"main.js": {
|
||||||
"file": "assets/main-BJyanQ7P.js",
|
"file": "assets/main-DH0OAeUr.js",
|
||||||
"name": "main",
|
"name": "main",
|
||||||
"src": "main.js",
|
"src": "main.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main-Dk7njt4m.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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
+117
-13
@@ -19,34 +19,138 @@ export function readableDatetime(date) {
|
|||||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stripHtml(html = "") {
|
export function stripHtml(html = "") {
|
||||||
let tmp = document.createElement("div");
|
let tmp = document.createElement("div");
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
return tmp.textContent || tmp.innerText || "";
|
return tmp.textContent || tmp.innerText || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function randomId(length = 10) {
|
export function randomId(length = 10) {
|
||||||
return Math.random().toString(36).substring(2, length + 2);
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, length + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clickOutside(node) {
|
export function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
const handleClick = event => {
|
|
||||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||||
node.dispatchEvent(
|
node.dispatchEvent(new CustomEvent("click_outside", node));
|
||||||
new CustomEvent('click_outside', node)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('click', handleClick, true);
|
document.addEventListener("click", handleClick, true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
document.removeEventListener('click', handleClick, true);
|
document.removeEventListener("click", handleClick, true);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function apiFetch(url, options = {}) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiPost(url, body, options = {}) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
}).then((r) => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiGet(url, options = {}) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
}).then((r) => r.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEqual(db, ed) {
|
||||||
|
let isObject = (x) =>
|
||||||
|
typeof x === "object" && !Array.isArray(x) && x !== null;
|
||||||
|
let isArray = (x) => x?.constructor === Array;
|
||||||
|
let isEmpty = (x) => x === null || x === undefined || x == [];
|
||||||
|
const db_value = db ?? null;
|
||||||
|
const ed_value = ed ?? null;
|
||||||
|
|
||||||
|
if (isObject(db_value)) {
|
||||||
|
let keys = Object.keys(db_value);
|
||||||
|
return keys.reduce((acc, k) => {
|
||||||
|
if (acc === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(db_value?.[k], ed_value?.[k]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
if (isArray(db_value)) {
|
||||||
|
return db_value.reduce((c, v, i) => {
|
||||||
|
if (c === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(v, ed_value?.[i]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(db_value) && isEmpty(ed_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_value == ed_value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// const ok = Object.keys,
|
||||||
|
// tx = typeof x,
|
||||||
|
// ty = typeof y;
|
||||||
|
// return x && y && tx === "object" && tx === ty
|
||||||
|
// ? ok(x).length === ok(y).length &&
|
||||||
|
// ok(x).every((key) => isEqual(x[key], y[key]))
|
||||||
|
// : x === y;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(fn, delay) {
|
||||||
|
let timer;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUnique(array) {
|
||||||
|
return array.filter((value, index) => array.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUniqueBy(items, uniqueBy) {
|
||||||
|
const ids = new Set(items.map((item) => item[uniqueBy]));
|
||||||
|
return [...ids].map((id) => items.find((i) => i[uniqueBy] === id));
|
||||||
|
}
|
||||||
|
export function arrayUniqueCb(items, aFilter) {
|
||||||
|
const cache = new Set();
|
||||||
|
return items.filter((item) => {
|
||||||
|
const cacheValue = aFilter(item);
|
||||||
|
if (cache.has(cacheValue)) return false;
|
||||||
|
cache.add(cacheValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
+6
-12
@@ -1,14 +1,11 @@
|
|||||||
import {axiosInstance} from "./bootstrap";
|
|
||||||
import "../sass/app.scss";
|
import "../sass/app.scss";
|
||||||
import Account from "./svelte/Account.svelte";
|
import Account from "./svelte/Account.svelte";
|
||||||
import Channel from "./svelte/Channel.svelte";
|
import Channel from "./svelte/Channel.svelte";
|
||||||
import Mustache from "mustache";
|
// import Mustache from "mustache";
|
||||||
import 'htmx.org';
|
|
||||||
|
|
||||||
Mustache.escape = function (value) {
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Mustache.escape = function (value) {
|
||||||
|
// return value;
|
||||||
|
// };
|
||||||
|
|
||||||
// Define all components
|
// Define all components
|
||||||
const entryComponents = {
|
const entryComponents = {
|
||||||
@@ -29,17 +26,14 @@ let loadSvelte = function () {
|
|||||||
const loadElement = function (element) {
|
const loadElement = function (element) {
|
||||||
const componentId = element.attributes["data-layout"].value;
|
const componentId = element.attributes["data-layout"].value;
|
||||||
const [_, component] = Object.entries(entryComponents).find(
|
const [_, component] = Object.entries(entryComponents).find(
|
||||||
([key, _]) => componentId === key
|
([key, _]) => componentId === key,
|
||||||
);
|
);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonData = document.getElementById(
|
const jsonData = document.getElementById("json-" + componentId).innerHTML;
|
||||||
"json-" + componentId
|
|
||||||
).innerHTML;
|
|
||||||
const props = JSON.parse(jsonData);
|
const props = JSON.parse(jsonData);
|
||||||
props.axios = axiosInstance;
|
|
||||||
const compOptions = {
|
const compOptions = {
|
||||||
target: element,
|
target: element,
|
||||||
props: props,
|
props: props,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import Login from "./account/Login.svelte";
|
import Login from "./account/Login.svelte";
|
||||||
import Verify from "./account/Verify.svelte";
|
import Verify from "./account/Verify.svelte";
|
||||||
import Profile from "./account/Profile.svelte";
|
import Profile from "./account/Profile.svelte";
|
||||||
import SetupIndex from "./setup/Index.svelte";
|
|
||||||
import { setContext } from "svelte";
|
import { setContext } from "svelte";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
login: Login,
|
login: Login,
|
||||||
verify: Verify,
|
verify: Verify,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
setup: SetupIndex,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
@@ -23,10 +21,7 @@
|
|||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
</script>
|
</script>
|
||||||
<div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)">
|
|
||||||
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name ?? "Lucent Setup"}</a></h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<svelte:component this={components[view]} {title} {...data}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<svelte:component this={components[view]} {channel} {title} {...data} />
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -25,18 +25,16 @@
|
|||||||
// export let layout;
|
// export let layout;
|
||||||
export let channel;
|
export let channel;
|
||||||
|
|
||||||
export let axios;
|
|
||||||
export let readableSchemas;
|
export let readableSchemas;
|
||||||
|
|
||||||
|
|
||||||
setContext("axios", axios);
|
|
||||||
setContext("channel", channel);
|
setContext("channel", channel);
|
||||||
setContext("readableSchemas", channel.schemas.filter((s) => readableSchemas.includes(s.name)));
|
setContext(
|
||||||
|
"readableSchemas",
|
||||||
|
channel.schemas.filter((s) => readableSchemas.includes(s.name)),
|
||||||
|
);
|
||||||
setContext("user", user);
|
setContext("user", user);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<Navbar schema={data.schema} />
|
<Navbar schema={data.schema} />
|
||||||
@@ -46,6 +44,3 @@
|
|||||||
<svelte:component this={components[view]} {title} {...data} />
|
<svelte:component this={components[view]} {title} {...data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let email = "";
|
let email = "";
|
||||||
let message = "";
|
let submitted = false;
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/login", { email: email })
|
||||||
.post(channel.lucentUrl + "/login", {
|
.then(() => {
|
||||||
email: email,
|
submitted = true;
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.catch(() => {});
|
||||||
console.log(response)
|
|
||||||
message = "You will receive an email with a login link"
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="scope-login">
|
||||||
{#if message}
|
<div class="bg-image"></div>
|
||||||
|
<div class="login-form">
|
||||||
|
{#if submitted}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
{message}
|
<p>
|
||||||
|
You will receive an email with a login link at <b>{email}</b
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p>Check your spam folder</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="form">
|
||||||
|
<h2 class="mb-5">Enter Lucent</h2>
|
||||||
|
|
||||||
<form on:submit={login}>
|
<form on:submit={login}>
|
||||||
<div class="mb-3">
|
<p>
|
||||||
<label for="emailaddress" class="form-label">Email address</label>
|
Submit your email address and you will receive a <b
|
||||||
|
>login link</b
|
||||||
|
> to your email
|
||||||
|
</p>
|
||||||
|
<p>Don't forget to check your spam folder</p>
|
||||||
|
<div class="mt-5 mb-3">
|
||||||
|
<label for="emailaddress" class="form-label"
|
||||||
|
>Email address</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
@@ -43,12 +52,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="bt bt-primary">
|
||||||
<div class="text-center mt-5 d-block">
|
Send email
|
||||||
<SpinnerButton label="Login"/>
|
<img
|
||||||
</div>
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
|
src="/img/spinner.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import Avatar from "./Avatar.svelte";
|
import Avatar from "./Avatar.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -16,8 +17,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-name", {
|
||||||
.post(channel.lucentUrl + "/account/update-name", {
|
|
||||||
name: name,
|
name: name,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -33,8 +33,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/account/update-email", {
|
||||||
.post(channel.lucentUrl + "/account/update-email", {
|
|
||||||
email: email,
|
email: email,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="wrapper-tiny">
|
<div class="wrapper-tiny">
|
||||||
<ErrorAlert message={errorMessage} />
|
<ErrorAlert message={errorMessage} />
|
||||||
<SuccessAlert bind:this={successAlert} />
|
<SuccessAlert bind:this={successAlert} />
|
||||||
@@ -81,7 +79,9 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this
|
<a
|
||||||
device</a>
|
class="list-group-item list-group-item-action"
|
||||||
|
href="{channel.lucentUrl}/logout">Logout from this device</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
@@ -12,8 +13,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/register", {
|
||||||
.post(channel.lucentUrl + "/register", {
|
|
||||||
name: name,
|
name: name,
|
||||||
email: email,
|
email: email,
|
||||||
})
|
})
|
||||||
@@ -50,10 +50,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="text-center mt-5 d-block">
|
<div class="text-center mt-5 d-block">
|
||||||
<SpinnerButton label="Register" />
|
<SpinnerButton label="Register" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import { apiPost } from "../../helpers";
|
||||||
import SuccessAlert from "../common/SuccessAlert.svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let email;
|
export let email;
|
||||||
export let token;
|
export let token;
|
||||||
let successAlert;
|
|
||||||
|
|
||||||
function login(e) {
|
function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/verify", {
|
||||||
.post(channel.lucentUrl + "/verify", {
|
|
||||||
email: email,
|
email: email,
|
||||||
token: token,
|
token: token,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessAlert bind:this={successAlert}/>
|
<div class="scope-login">
|
||||||
<div class="wrapper-tiny">
|
<div class="bg-image"></div>
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="form">
|
||||||
|
<h2 class="mb-5">Welcome to Lucent</h2>
|
||||||
<form on:submit={login}>
|
<form on:submit={login}>
|
||||||
<div class="mb-3 text-center">
|
<button class="bt bt-primary">
|
||||||
<h3>Login as {email}</h3>
|
Enter as {email}
|
||||||
|
<img
|
||||||
</div>
|
alt="indicator"
|
||||||
|
id="indicator"
|
||||||
|
class="htmx-indicator"
|
||||||
<div class="text-center mt-5 d-block">
|
src="/img/spinner.svg"
|
||||||
<SpinnerButton label="Enter"/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="form-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle execution of a function. Especially useful for rate limiting
|
||||||
|
* execution of handlers on events like resize and scroll.
|
||||||
|
*
|
||||||
|
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
|
||||||
|
* are most useful.
|
||||||
|
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
|
||||||
|
* as-is, to `callback` when the throttled-function is executed.
|
||||||
|
* @param {object} [options] - An object to configure options.
|
||||||
|
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
|
||||||
|
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
|
||||||
|
* one final time after the last throttled-function call. (After the throttled-function has not been called for
|
||||||
|
* `delay` milliseconds, the internal counter is reset).
|
||||||
|
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
|
||||||
|
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
|
||||||
|
* callback will never executed if both noLeading = true and noTrailing = true.
|
||||||
|
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
|
||||||
|
* false (at end), schedule `callback` to execute after `delay` ms.
|
||||||
|
*
|
||||||
|
* @returns {Function} A new, throttled, function.
|
||||||
|
*/
|
||||||
|
export function throttle(delay, callback, options) {
|
||||||
|
const {
|
||||||
|
noTrailing = false,
|
||||||
|
noLeading = false,
|
||||||
|
debounceMode = undefined,
|
||||||
|
} = options || {};
|
||||||
|
/*
|
||||||
|
* After wrapper has stopped being called, this timeout ensures that
|
||||||
|
* `callback` is executed at the proper times in `throttle` and `end`
|
||||||
|
* debounce modes.
|
||||||
|
*/
|
||||||
|
let timeoutID;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Keep track of the last time `callback` was executed.
|
||||||
|
let lastExec = 0;
|
||||||
|
|
||||||
|
// Function to clear existing timeout
|
||||||
|
function clearExistingTimeout() {
|
||||||
|
if (timeoutID) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel next exec
|
||||||
|
function cancel(options) {
|
||||||
|
const { upcomingOnly = false } = options || {};
|
||||||
|
clearExistingTimeout();
|
||||||
|
cancelled = !upcomingOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The `wrapper` function encapsulates all of the throttling / debouncing
|
||||||
|
* functionality and when executed will limit the rate at which `callback`
|
||||||
|
* is executed.
|
||||||
|
*/
|
||||||
|
function wrapper(...arguments_) {
|
||||||
|
let self = this;
|
||||||
|
let elapsed = Date.now() - lastExec;
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute `callback` and update the `lastExec` timestamp.
|
||||||
|
function exec() {
|
||||||
|
lastExec = Date.now();
|
||||||
|
callback.apply(self, arguments_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `debounceMode` is true (at begin) this is used to clear the flag
|
||||||
|
* to allow future `callback` executions.
|
||||||
|
*/
|
||||||
|
function clear() {
|
||||||
|
timeoutID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noLeading && debounceMode && !timeoutID) {
|
||||||
|
/*
|
||||||
|
* Since `wrapper` is being called for the first time and
|
||||||
|
* `debounceMode` is true (at begin), execute `callback`
|
||||||
|
* and noLeading != true.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExistingTimeout();
|
||||||
|
|
||||||
|
if (debounceMode === undefined && elapsed > delay) {
|
||||||
|
if (noLeading) {
|
||||||
|
/*
|
||||||
|
* In throttle mode with noLeading, if `delay` time has
|
||||||
|
* been exceeded, update `lastExec` and schedule `callback`
|
||||||
|
* to execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
lastExec = Date.now();
|
||||||
|
if (!noTrailing) {
|
||||||
|
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
|
||||||
|
* `callback`.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
} else if (noTrailing !== true) {
|
||||||
|
/*
|
||||||
|
* In trailing throttle mode, since `delay` time has not been
|
||||||
|
* exceeded, schedule `callback` to execute `delay` ms after most
|
||||||
|
* recent execution.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is true (at begin), schedule `clear` to execute
|
||||||
|
* after `delay` ms.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is false (at end), schedule `callback` to
|
||||||
|
* execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
timeoutID = setTimeout(
|
||||||
|
debounceMode ? clear : exec,
|
||||||
|
debounceMode === undefined ? delay - elapsed : delay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.cancel = cancel;
|
||||||
|
|
||||||
|
// Return the wrapper function.
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(delay, callback, options) {
|
||||||
|
const { atBegin = false } = options || {};
|
||||||
|
return throttle(delay, callback, { debounceMode: atBegin !== false });
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let title;
|
export let title;
|
||||||
export let command;
|
export let command;
|
||||||
@@ -12,59 +11,57 @@
|
|||||||
let inProgress = false;
|
let inProgress = false;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature );
|
const eventSource = new EventSource(
|
||||||
|
channel.lucentUrl + "/command-report-source/" + command.signature,
|
||||||
|
);
|
||||||
|
|
||||||
eventSource.onmessage = function (event) {
|
eventSource.onmessage = function (event) {
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
date = data.date;
|
date = data.date;
|
||||||
logs = data.logs;
|
logs = data.logs;
|
||||||
anchorEl.scrollIntoView()
|
anchorEl.scrollIntoView();
|
||||||
}
|
};
|
||||||
eventSource.onerror = (e) => {
|
eventSource.onerror = (e) => {
|
||||||
console.log(e)
|
console.log(e);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWebsite(e) {
|
function buildWebsite(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
|
apiPost(channel.lucentUrl + "/command/" + command.signature).then(
|
||||||
connect()
|
(response) => {
|
||||||
})
|
connect();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
connect()
|
connect();
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="common-wrapper">
|
<div class="common-wrapper">
|
||||||
<div class="lx-card mt-5">
|
<div class="lx-card mt-5">
|
||||||
|
|
||||||
<h3 class="header-small mb-5">{title}</h3>
|
<h3 class="header-small mb-5">{title}</h3>
|
||||||
|
|
||||||
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start
|
<button
|
||||||
|
on:click={buildWebsite}
|
||||||
|
class="button primary mb-3"
|
||||||
|
disabled={inProgress}
|
||||||
|
>Start
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#if inProgress}
|
{#if inProgress}
|
||||||
<span class="badge text-bg-warning">
|
<span class="badge text-bg-warning"> Action in progress </span>
|
||||||
Action in progress
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !inProgress && logs}
|
{#if !inProgress && logs}
|
||||||
<span class="badge text-bg-info">
|
<span class="badge text-bg-info"> Action completed </span>
|
||||||
Action completed
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre class="logs">{logs}
|
<pre class="logs">{logs}
|
||||||
@@ -72,6 +69,7 @@
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.logs {
|
.logs {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let selected;
|
export let selected;
|
||||||
@@ -8,8 +9,7 @@
|
|||||||
|
|
||||||
function deleteRecords(e) {
|
function deleteRecords(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/delete", {
|
||||||
.post(channel.lucentUrl + "/records/delete", {
|
|
||||||
ids: selected.map((s) => s.id),
|
ids: selected.map((s) => s.id),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -21,10 +21,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeStatus(e, status) {
|
function changeStatus(e, status) {
|
||||||
axios
|
apiPost(channel.lucentUrl + "/records/status/" + status, {
|
||||||
.post(channel.lucentUrl + "/records/status/" + status, {
|
|
||||||
schemaName: schema.name,
|
schemaName: schema.name,
|
||||||
records: selected
|
records: selected,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -40,42 +39,42 @@
|
|||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Publish
|
class="button"
|
||||||
</button
|
>Publish
|
||||||
>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Make Draft
|
class="button"
|
||||||
</button
|
>Make Draft
|
||||||
>
|
</button>
|
||||||
{#if filter["status_in"] === "trashed"}
|
{#if filter["status_in"] === "trashed"}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Publish
|
class="button"
|
||||||
</button
|
>Publish
|
||||||
>
|
</button>
|
||||||
{#if schema.hasDrafts}
|
{#if schema.hasDrafts}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Make Draft
|
class="button"
|
||||||
</button
|
>Make Draft
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={deleteRecords}
|
on:click|preventDefault={deleteRecords}
|
||||||
type="button"
|
type="button"
|
||||||
class="button">Delete forever
|
class="button"
|
||||||
</button
|
>Delete forever
|
||||||
>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
|
||||||
class="button">Move to trash
|
class="button"
|
||||||
</button
|
>Move to trash
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {getContext} from "svelte";
|
|
||||||
import Preview from "../files/Preview.svelte";
|
|
||||||
import {selectRecord} from "./functions/recordSelect.js";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
|
|
||||||
export let schema;
|
|
||||||
export let records;
|
|
||||||
export let isWritable;
|
|
||||||
export let selected = [];
|
|
||||||
|
|
||||||
function select(record) {
|
|
||||||
selected = selectRecord(record, selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<div class="row" style="max-width:1000px">
|
|
||||||
{#each records as record (record.id)}
|
|
||||||
<div class="col-6 col-md-4">
|
|
||||||
<div
|
|
||||||
class="file-wrapper rounded p-2 mb-4 bg-light"
|
|
||||||
class:selected={selected.includes(record)}
|
|
||||||
>
|
|
||||||
{#if isWritable}
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
on:change={() => select(record)}
|
|
||||||
class="form-check-input "
|
|
||||||
type="checkbox"
|
|
||||||
checked={selected.find(
|
|
||||||
(r) => r.id === record.id
|
|
||||||
)}
|
|
||||||
value={record}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<Preview {record} size="medium"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
title={record._file.path}
|
|
||||||
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
|
||||||
style="
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #333;
|
|
||||||
">{record._file.path}</a
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="lx-small-text text-muted d-block text-center"
|
|
||||||
>{record._file.mime}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-check {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
||||||
import Table from "./Table.svelte";
|
import Table from "./Table.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
const axios = getContext("axios");
|
|
||||||
export let schema;
|
export let schema;
|
||||||
export let users;
|
export let users;
|
||||||
export let records;
|
export let records;
|
||||||
@@ -26,18 +26,17 @@
|
|||||||
|
|
||||||
function refresh(e) {
|
function refresh(e) {
|
||||||
const newUrl = e.detail;
|
const newUrl = e.detail;
|
||||||
axios
|
apiGet(newUrl)
|
||||||
.get(newUrl)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
records = response.data.records;
|
records = response.records;
|
||||||
sortParam = response.data.sortParam;
|
sortParam = response.sortParam;
|
||||||
sortField = response.data.sortField;
|
sortField = response.sortField;
|
||||||
operators = response.data.operators;
|
operators = response.operators;
|
||||||
filter = response.data.filter;
|
filter = response.filter;
|
||||||
skip = response.data.skip;
|
skip = response.skip;
|
||||||
limit = response.data.limit;
|
limit = response.limit;
|
||||||
total = response.data.total;
|
total = response.total;
|
||||||
modalUrl = response.data.modalUrl;
|
modalUrl = response.modalUrl;
|
||||||
document.querySelector("dialog h3").scrollIntoView();
|
document.querySelector("dialog h3").scrollIntoView();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -47,7 +46,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="{inModal ? 'mt-0' : 'mt-5'}">
|
<div class={inModal ? "mt-0" : "mt-5"}>
|
||||||
<h3 class="header-normal mb-5">
|
<h3 class="header-normal mb-5">
|
||||||
{schema.label}
|
{schema.label}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -82,7 +81,6 @@
|
|||||||
{isWritable}
|
{isWritable}
|
||||||
bind:selected
|
bind:selected
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -94,4 +92,3 @@
|
|||||||
{modalUrl}
|
{modalUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
export let sortParam;
|
export let sortParam;
|
||||||
export let sortField;
|
export let sortField;
|
||||||
export let visibleColumns;
|
export let visibleColumns;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each visibleColumns as field, index}
|
{#each visibleColumns as field, index}
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
<RenderField {record} {schema} {graph} {field} />
|
<RenderField {record} {schema} {graph} {field} />
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
{#if schema.visible?.includes("status")}
|
{#if schema.visible?.includes("_sys.status")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
class:is-sort={"-status" == sortParam || "status" == sortParam}
|
||||||
@@ -34,26 +33,34 @@
|
|||||||
{#if schema.visible?.includes("_sys.createdBy")}
|
{#if schema.visible?.includes("_sys.createdBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
|
class:is-sort={"-_sys.createdBy" == sortParam ||
|
||||||
|
"_sys.createdBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/>
|
<Avatar name={usernameById(users, record.createdBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.updatedBy")}
|
{#if schema.visible?.includes("_sys.updatedBy")}
|
||||||
<td
|
<td
|
||||||
class="text-center"
|
class="text-center"
|
||||||
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
|
class:is-sort={"-_sys.updatedBy" == sortParam ||
|
||||||
|
"_sys.updatedBy" == sortParam}
|
||||||
>
|
>
|
||||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.createdAt")}
|
{#if schema.visible?.includes("_sys.createdAt")}
|
||||||
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(record._sys.createdAt)}
|
class:is-sort={"-_sys.createdAt" == sortParam ||
|
||||||
|
"_sys.createdAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema.visible?.includes("_sys.updatedAt")}
|
{#if schema.visible?.includes("_sys.updatedAt")}
|
||||||
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
|
<td
|
||||||
{friendlyDate(record._sys.updatedAt)}
|
class:is-sort={"-_sys.updatedAt" == sortParam ||
|
||||||
|
"_sys.updatedAt" == sortParam}
|
||||||
|
>
|
||||||
|
{friendlyDate(record.updatedAt)}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import RecordRow from "./RecordRow.svelte";
|
import RecordRow from "./RecordRow.svelte";
|
||||||
import {previewTitle} from "../records/Preview";
|
|
||||||
import { usernameById } from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import { selectRecord, toggleAll } from "./functions/recordSelect.js";
|
import { selectRecord, toggleAll } from "./functions/recordSelect.js";
|
||||||
import Checkbox from "../common/Checkbox.svelte";
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
import Preview from "../files/Preview.svelte";
|
|
||||||
import {fileurl} from "../files/imageserver.js";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -23,14 +20,16 @@
|
|||||||
export let selected = [];
|
export let selected = [];
|
||||||
|
|
||||||
function eventToggleAll(e) {
|
function eventToggleAll(e) {
|
||||||
selected = toggleAll(e, records, selected)
|
selected = toggleAll(e, records, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(record) {
|
function select(record) {
|
||||||
selected = selectRecord(record, selected)
|
selected = selectRecord(record, selected);
|
||||||
}
|
}
|
||||||
|
console.log(schema);
|
||||||
$: visibleColumns = schema.fields.filter(c => schema.visible?.includes(c.name) ?? [])
|
$: visibleColumns = schema.fields.filter(
|
||||||
|
(c) => schema.visible?.includes(c.name) ?? [],
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table mt-5">
|
<div class="table mt-5">
|
||||||
@@ -42,10 +41,10 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value=""
|
value=""
|
||||||
on:change={eventToggleAll}
|
on:change={eventToggleAll}
|
||||||
indeterminate={selected.length > 0 && selected.length < records.length}
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < records.length}
|
||||||
checked={selected.length === records.length}
|
checked={selected.length === records.length}
|
||||||
>
|
></Checkbox>
|
||||||
</Checkbox>
|
|
||||||
</th>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -54,12 +53,13 @@
|
|||||||
class="field-ui-{field.info.name ?? field.ui}"
|
class="field-ui-{field.info.name ?? field.ui}"
|
||||||
class:is-sort={field.name === sortField.name}
|
class:is-sort={field.name === sortField.name}
|
||||||
scope="col"
|
scope="col"
|
||||||
title={field.help}
|
title={field.help}>{field.label}</th
|
||||||
>{field.label}</th
|
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField}
|
{#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
|
||||||
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th>
|
<th class:is-sort={sysField.name === sortField.name}
|
||||||
|
>{sysField.label}</th
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -68,59 +68,29 @@
|
|||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title-td">
|
<td class="title-td">
|
||||||
<div
|
<div class="title-td-contents">
|
||||||
class="title-td-contents"
|
|
||||||
>
|
|
||||||
{#if isWritable}
|
{#if isWritable}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
on:change={() => select(record)}
|
on:change={() => select(record)}
|
||||||
checked={selected.find((r) => r.id === record.id)}
|
checked={selected.find(
|
||||||
|
(r) => r.id === record.id,
|
||||||
|
)}
|
||||||
value={record}
|
value={record}
|
||||||
>
|
></Checkbox>
|
||||||
</Checkbox>
|
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if record._file?.path}
|
|
||||||
<div class="file-table-row">
|
|
||||||
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if record.status === "draft"}
|
|
||||||
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
target={inModal ? "_blank" : "_self"}
|
|
||||||
>
|
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
|
||||||
</a>
|
|
||||||
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
|
|
||||||
|
|
||||||
{#if record._file.width > 0}
|
|
||||||
<span>{record._file.width + "x" + record._file.height}</span>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
href="{fileurl(channel,record)}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
target={inModal ? "_blank" : "_self"}
|
target={inModal ? "_blank" : "_self"}
|
||||||
>
|
>
|
||||||
{#if record.status === "draft"}
|
{#if record.status === "draft"}
|
||||||
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
|
<span
|
||||||
|
style="text-transform: uppercase;font-size:10px"
|
||||||
|
>{record.status}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<RecordRow
|
<RecordRow
|
||||||
@@ -134,10 +104,7 @@
|
|||||||
/>
|
/>
|
||||||
<td>
|
<td>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(
|
name={usernameById(users, record.updatedBy)}
|
||||||
users,
|
|
||||||
record._sys.updatedBy
|
|
||||||
)}
|
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -146,4 +113,3 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
import { range } from "lodash";
|
|
||||||
import NavItem from "./NavItem.svelte";
|
import NavItem from "./NavItem.svelte";
|
||||||
export let inModal;
|
export let inModal;
|
||||||
export let modalUrl;
|
export let modalUrl;
|
||||||
@@ -11,7 +11,11 @@
|
|||||||
|
|
||||||
$: totalPages = Math.ceil(total / limit);
|
$: totalPages = Math.ceil(total / limit);
|
||||||
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
||||||
|
const range = (start, end, step = 1) =>
|
||||||
|
Array.from(
|
||||||
|
{ length: Math.ceil((end - start) / step) },
|
||||||
|
(_, i) => start + i * step,
|
||||||
|
);
|
||||||
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
||||||
return i > 0 && i <= totalPages;
|
return i > 0 && i <= totalPages;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {previewTitle} from "../../records/Preview";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
export let operators;
|
export let operators;
|
||||||
@@ -19,13 +17,10 @@
|
|||||||
isReference: key.startsWith("children"),
|
isReference: key.startsWith("children"),
|
||||||
};
|
};
|
||||||
|
|
||||||
filter = [
|
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
|
||||||
extractOperator(key),
|
(mem, fn) => fn(mem),
|
||||||
extractLabel(schema, key),
|
filter,
|
||||||
].reduce((mem, fn) => fn(mem), filter);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function extractOperator(key) {
|
function extractOperator(key) {
|
||||||
return (filter) => {
|
return (filter) => {
|
||||||
@@ -50,18 +45,16 @@
|
|||||||
const filterField = schema.fields.find((f) => f.name === fieldName);
|
const filterField = schema.fields.find((f) => f.name === fieldName);
|
||||||
filter.label = filterField?.label ?? fieldName;
|
filter.label = filterField?.label ?? fieldName;
|
||||||
return filter;
|
return filter;
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterRecord = extractFilterRecord(graph, value);
|
const filterRecord = extractFilterRecord(graph, value);
|
||||||
|
|
||||||
function extractFilterRecord(graph, value) {
|
function extractFilterRecord(graph, value) {
|
||||||
|
|
||||||
if (!filter.isReference) {
|
if (!filter.isReference) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return graph.records.find(r => r.id === value);
|
return graph.records.find((r) => r.id === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFilter(k) {
|
function removeFilter(k) {
|
||||||
@@ -78,11 +71,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="applied-filter">
|
<span class="applied-filter">
|
||||||
|
|
||||||
{#if filter.isReference && filterRecord}
|
{#if filter.isReference && filterRecord}
|
||||||
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
|
{filter.label} is {filterRecord.data.name}
|
||||||
{:else}
|
{:else}
|
||||||
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""}
|
{filter.label}
|
||||||
|
{operators.find((o) => o.name === filter.operator)?.symbol ?? ""}
|
||||||
|
{operators.find((o) => o.name === filter.operator)?.hasValue
|
||||||
|
? value
|
||||||
|
: ""}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -90,7 +86,6 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="button-text"
|
class="button-text"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
><Icon width={12} height={12} icon="close"></Icon></button>
|
><Icon width={12} height={12} icon="close"></Icon></button
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {debounce} from "lodash";
|
import { apiGet, debounce } from "../../../helpers";
|
||||||
import {previewTitle} from "../../records/Preview";
|
|
||||||
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -10,13 +8,11 @@
|
|||||||
export let value = "";
|
export let value = "";
|
||||||
export let field;
|
export let field;
|
||||||
|
|
||||||
let search = ""
|
let search = "";
|
||||||
$: searchOptions = []
|
$: searchOptions = [];
|
||||||
|
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/records/suggestions", {
|
||||||
.get(channel.lucentUrl + "/records/suggestions", {
|
|
||||||
params: {
|
params: {
|
||||||
schema: field.collections[0],
|
schema: field.collections[0],
|
||||||
field: "search",
|
field: "search",
|
||||||
@@ -25,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = response.data;
|
searchOptions = response;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
@@ -35,13 +31,12 @@
|
|||||||
|
|
||||||
function apply(e, newOption) {
|
function apply(e, newOption) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
value = newOption.id
|
value = newOption.id;
|
||||||
dispatch("addFilter");
|
dispatch("addFilter");
|
||||||
value = ""
|
value = "";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="reference-tags">
|
<div class="reference-tags">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -52,7 +47,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reference-tags-results">
|
<div class="reference-tags-results">
|
||||||
|
|
||||||
{#if searchOptions}
|
{#if searchOptions}
|
||||||
{#each searchOptions as option (option.id)}
|
{#each searchOptions as option (option.id)}
|
||||||
<div
|
<div
|
||||||
@@ -62,19 +56,11 @@
|
|||||||
on:click={(e) => apply(e, option)}
|
on:click={(e) => apply(e, option)}
|
||||||
on:keypress={(e) => apply(e, option)}
|
on:keypress={(e) => apply(e, option)}
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, option)}
|
{option.data.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="start-typing">Start typing...</div>
|
||||||
class="start-typing">
|
|
||||||
Start typing...
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import FilterFields from "./FilterFields.svelte";
|
import FilterFields from "./FilterFields.svelte";
|
||||||
import Uploader from "../../files/Uploader.svelte";
|
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import SortFields from "./SortFields.svelte";
|
import SortFields from "./SortFields.svelte";
|
||||||
import AppliedFilter from "./AppliedFilter.svelte";
|
import AppliedFilter from "./AppliedFilter.svelte";
|
||||||
@@ -41,17 +40,11 @@
|
|||||||
} else {
|
} else {
|
||||||
window.location = url;
|
window.location = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadComplete(e) {
|
|
||||||
records = e.detail;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
|
|
||||||
<SortFields
|
<SortFields
|
||||||
{schema}
|
{schema}
|
||||||
{sortParam}
|
{sortParam}
|
||||||
@@ -62,7 +55,6 @@
|
|||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FilterFields
|
<FilterFields
|
||||||
bind:schema
|
bind:schema
|
||||||
{systemFields}
|
{systemFields}
|
||||||
@@ -74,15 +66,17 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<form method="GET" on:submit={search}>
|
<form method="GET" on:submit={search}>
|
||||||
<input type="search" name="filter[search_regex]" placeholder="Search"
|
<input
|
||||||
class="search" required>
|
type="search"
|
||||||
|
name="filter[search_regex]"
|
||||||
|
placeholder="Search"
|
||||||
|
class="search"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items: center;gap:4px">
|
<div style="display:flex;align-items: center;gap:4px">
|
||||||
{#if schema.type === "collection"}
|
|
||||||
{#if !inModal && isWritable}
|
{#if !inModal && isWritable}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||||
@@ -91,11 +85,7 @@
|
|||||||
New Record
|
New Record
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else }
|
|
||||||
<div>
|
|
||||||
<Uploader {schema} on:uploadComplete={uploadComplete}/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if !inModal}
|
{#if !inModal}
|
||||||
<Dropdown orientation="right">
|
<Dropdown orientation="right">
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
@@ -111,12 +101,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href={csvUrl}
|
|
||||||
>Export to CSV</a
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
|
||||||
@@ -129,17 +114,12 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="applied-filters">
|
<div class="applied-filters">
|
||||||
<AppliedFilterNotLinked
|
<AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
|
||||||
{inModal}
|
|
||||||
{modalUrl}
|
|
||||||
on:refresh
|
|
||||||
></AppliedFilterNotLinked>
|
></AppliedFilterNotLinked>
|
||||||
{#if Object.entries(filter).length > 0}
|
{#if Object.entries(filter).length > 0}
|
||||||
{#each Object.entries(filter) as [k, v]}
|
{#each Object.entries(filter) as [k, v]}
|
||||||
@@ -156,5 +136,3 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import Index from "../content/Index.svelte";
|
import Index from "../content/Index.svelte";
|
||||||
import axios from "axios";
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
let dialogEl;
|
let dialogEl;
|
||||||
|
|
||||||
@@ -15,21 +14,19 @@
|
|||||||
// load();
|
// load();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
export function close(e) {
|
export function close(e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogEl.close()
|
dialogEl.close();
|
||||||
selectedRecords = [];
|
selectedRecords = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(schema) {
|
function load(schema) {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/content/" + schema)
|
||||||
.get(channel.lucentUrl + "/content/" + schema)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
data = response.data;
|
data = response;
|
||||||
})
|
})
|
||||||
.catch((error) => console.log(error));
|
.catch((error) => console.log(error));
|
||||||
}
|
}
|
||||||
@@ -52,12 +49,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function open(schema) {
|
export function open(schema) {
|
||||||
dialogEl.showModal()
|
dialogEl.showModal();
|
||||||
load(schema);
|
load(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog bind:this={dialogEl}>
|
<dialog bind:this={dialogEl}>
|
||||||
{#if data.schema}
|
{#if data.schema}
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
@@ -96,6 +92,5 @@
|
|||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<Index {...data} bind:selected={selectedRecords}></Index>
|
<Index {...data} bind:selected={selectedRecords}></Index>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import FileIndex from "./FileIndex.svelte";
|
||||||
|
|
||||||
|
let dialogEl;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const channel = getContext("channel");
|
||||||
|
$: files = [];
|
||||||
|
$: selectedRecords = [];
|
||||||
|
// onMount(() => {
|
||||||
|
// load();
|
||||||
|
// });
|
||||||
|
|
||||||
|
export function close(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogEl.close();
|
||||||
|
selectedRecords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(recordId) {
|
||||||
|
fetch(channel.lucentUrl + "/records/files/?recordId=" + recordId)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
files = json;
|
||||||
|
})
|
||||||
|
.catch((error) => console.log(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("insert_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("replace_files", selectedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function open(recordId) {
|
||||||
|
dialogEl.showModal();
|
||||||
|
load(recordId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog bind:this={dialogEl}>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={insert}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Insert
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
on:click={replace}
|
||||||
|
disabled={selectedRecords.length === 0}
|
||||||
|
>
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
|
{#if selectedRecords.length > 0}
|
||||||
|
<span class="">
|
||||||
|
{selectedRecords.length} records selected
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={close}
|
||||||
|
type="button"
|
||||||
|
class="button close"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<Icon icon="close"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<FileIndex {files} bind:selected={selectedRecords}></FileIndex>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import Checkbox from "../common/Checkbox.svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import { fileurl } from "../files/imageserver";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
export let files = [];
|
||||||
|
export let selected = [];
|
||||||
|
export let isWritable = true;
|
||||||
|
|
||||||
|
function eventToggleAll(e) {
|
||||||
|
selected = toggleAll(e, files, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(file) {
|
||||||
|
selected = selectFile(file, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleAll = (e, files, selected) => {
|
||||||
|
if (selected.length === files.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
e.currentTarget.checked = selected.length > 0;
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectFile = (file, selected) => {
|
||||||
|
let fileExists = selected.find((r) => r.id === file.id);
|
||||||
|
if (fileExists) {
|
||||||
|
return selected.filter((r) => r.id !== file.id);
|
||||||
|
}
|
||||||
|
return [...selected, file];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table mt-5">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#if isWritable}
|
||||||
|
<th>
|
||||||
|
<Checkbox
|
||||||
|
value=""
|
||||||
|
on:change={eventToggleAll}
|
||||||
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < files.length}
|
||||||
|
checked={selected.length === files.length}
|
||||||
|
></Checkbox>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each files as file (file.id)}
|
||||||
|
<tr>
|
||||||
|
<td class="title-td">
|
||||||
|
<div class="title-td-contents">
|
||||||
|
{#if isWritable}
|
||||||
|
<Checkbox
|
||||||
|
on:change={() => select(file)}
|
||||||
|
checked={selected.find(
|
||||||
|
(s) => s.id === file.id,
|
||||||
|
)}
|
||||||
|
value={file}
|
||||||
|
></Checkbox>
|
||||||
|
{/if}
|
||||||
|
<div class="file-table-row">
|
||||||
|
<Preview
|
||||||
|
{file}
|
||||||
|
size={file.width > 0 ? "medium" : "small"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{file.filename}
|
||||||
|
<span
|
||||||
|
>{(file.size / 1024).toFixed(1)}kB</span
|
||||||
|
>
|
||||||
|
|
||||||
|
{#if file.width > 0}
|
||||||
|
<span
|
||||||
|
>{file.width +
|
||||||
|
"x" +
|
||||||
|
file.height}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<a
|
||||||
|
href={fileurl(channel, file)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function sortByField(from, to, edges, fieldName, references) {
|
export function sortByField(from, to, edges, fieldName, references) {
|
||||||
if (from === to) {
|
if (from === to) {
|
||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
let referenceIds = references.map(r => r.id);
|
let referenceIds = references.map((r) => r.id);
|
||||||
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? [];
|
let edgesTosort =
|
||||||
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
edges?.filter(
|
||||||
|
(ed) =>
|
||||||
|
ed.field === fieldName &&
|
||||||
|
ed.depth === 1 &&
|
||||||
|
referenceIds.includes(ed.target),
|
||||||
|
) ?? [];
|
||||||
|
let remainingEdge =
|
||||||
|
edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
||||||
|
|
||||||
edgesTosort = array_move(edgesTosort, from, to);
|
edgesTosort = array_move(edgesTosort, from, to);
|
||||||
return [...remainingEdge, ...edgesTosort];
|
return [...remainingEdge, ...edgesTosort];
|
||||||
}
|
}
|
||||||
|
|
||||||
function array_move(arr, old_index, new_index) {
|
export function array_move(arr, old_index, new_index) {
|
||||||
if (new_index >= arr.length) {
|
if (new_index >= arr.length) {
|
||||||
var k = new_index - arr.length + 1;
|
var k = new_index - arr.length + 1;
|
||||||
while (k--) {
|
while (k--) {
|
||||||
@@ -22,4 +26,4 @@ function array_move(arr, old_index, new_index) {
|
|||||||
}
|
}
|
||||||
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
||||||
return arr; // for testing
|
return arr; // for testing
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
import { imgurl } from "./imageserver.js";
|
import { imgurl } from "./imageserver.js";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export let record;
|
export let file;
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let size = "small";
|
export let size = "small";
|
||||||
export let showFilename = false;
|
export let showFilename = false;
|
||||||
let imageSide;
|
let imageSide;
|
||||||
let fileSide;
|
let fileSide;
|
||||||
let fontSize;
|
let fontSize;
|
||||||
|
|
||||||
|
console.log({ channel });
|
||||||
if (size == "large") {
|
if (size == "large") {
|
||||||
imageSide = 256;
|
imageSide = 256;
|
||||||
fileSide = 32;
|
fileSide = 32;
|
||||||
@@ -28,43 +30,43 @@
|
|||||||
fontSize = "13";
|
fontSize = "13";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<div style="display: flex;align-items: center;gap: 5px;">
|
|
||||||
{#if record}
|
|
||||||
|
|
||||||
{#if record._file.mime.startsWith("image")}
|
<div style="display: flex;align-items: center;gap: 5px;">
|
||||||
|
{#if file}
|
||||||
|
{#if file.mime.startsWith("image")}
|
||||||
<!-- href={imgurl(record)} -->
|
<!-- href={imgurl(record)} -->
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.originalName}
|
title={file.filename}
|
||||||
style="width:{imageSide}px;height:{imageSide}px"
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="rounded w-100"
|
class="rounded w-100"
|
||||||
src={imgurl(channel,record)}
|
src={imgurl(channel, file)}
|
||||||
alt={record._file.path}
|
alt={file.path}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.path}
|
title={file.path}
|
||||||
class="file-preview-small"
|
class="file-preview-small"
|
||||||
style="width:{imageSide}px;height:{imageSide}px"
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
>
|
>
|
||||||
<Icon icon="file" width={fileSide} height={fileSide} />
|
<Icon icon="file" width={fileSide} height={fileSide} />
|
||||||
<span class="ms-2"
|
<span class="ms-2"
|
||||||
>.{record._file.path.split(".").pop().toLowerCase()}</span
|
>.{file.path.split(".").pop().toLowerCase()}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if showFilename}
|
{#if showFilename}
|
||||||
<a
|
<a
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/files/{file.id}"
|
||||||
title={record._file.path}
|
title={file.path}
|
||||||
class="preview-file-filename lx-small-text text-decoration-none"
|
class="preview-file-filename lx-small-text text-decoration-none"
|
||||||
>{record._file.path} </a
|
>{file.path}
|
||||||
>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,5 +75,4 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let recordId;
|
||||||
let mimeTypes = "";
|
let mimeTypes = "";
|
||||||
let files = [];
|
let files = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
@@ -17,28 +17,28 @@
|
|||||||
files = e.target.files ? [...e.target.files] : [];
|
files = e.target.files ? [...e.target.files] : [];
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
formData.append("schema", schema.name);
|
formData.append("recordId", recordId);
|
||||||
Array.from(files).forEach(function (file) {
|
Array.from(files).forEach(function (file) {
|
||||||
formData.append("files[]", file);
|
formData.append("files[]", file);
|
||||||
});
|
});
|
||||||
dispatch("beforeUpload", files);
|
dispatch("beforeUpload", files);
|
||||||
axios
|
fetch(channel.lucentUrl + "/files/upload", {
|
||||||
.post(channel.lucentUrl + "/files/upload", formData, {
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"X-CSRF-TOKEN": document.querySelector(
|
||||||
|
'meta[name="csrf-token"]',
|
||||||
|
).content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response.json())
|
||||||
if (response.data.error) {
|
.then((data) => {
|
||||||
dispatch("uploadError", response.data.error);
|
if (data.error) {
|
||||||
|
dispatch("uploadError", data.error);
|
||||||
} else {
|
} else {
|
||||||
dispatch("uploadComplete", response.data);
|
dispatch("uploadComplete", data);
|
||||||
}
|
}
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
isLoading = false;
|
|
||||||
console.log(error.response.data);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
export function imgurl(channel, record) {
|
export function imgurl(channel, file) {
|
||||||
if (record._file.mime === "image/svg+xml") {
|
if (file.mime === "image/svg+xml") {
|
||||||
return fileurl(channel, record);
|
return fileurl(channel, file);
|
||||||
}
|
}
|
||||||
return channel.disks[record._file.disk] + `/thumbs/${record._file.path}`;
|
return channel.filesUrl + `/thumbs/${file.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileurl(channel, record) {
|
export function fileurl(channel, file) {
|
||||||
return channel.disks[record._file.disk] + `/${record._file.path}`;
|
return channel.filesUrl + `/${file.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlurl(channel, record, preset) {
|
export function htmlurl(channel, file, preset) {
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
let url = fileurl(channel, record)
|
let url = fileurl(channel, file);
|
||||||
|
|
||||||
if (record._file.width > 0) {
|
if (file.width > 0) {
|
||||||
let presetUrl = url;
|
let presetUrl = url;
|
||||||
if (preset) {
|
if (preset) {
|
||||||
presetUrl = channel.disks[record._file.disk] + `/templates/${preset}/${record._file.path}`;
|
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
|
||||||
}
|
}
|
||||||
html = `<img src="${presetUrl}" alt="${record._file.path}" />`
|
html = `<img src="${presetUrl}" alt="${file.path}" />`;
|
||||||
} else if (record._file.mime === "image/svg+xml") {
|
} else if (file.mime === "image/svg+xml") {
|
||||||
html = `<img src="${url}" alt="${record._file.path}"/>`
|
html = `<img src="${url}" alt="${file.path}"/>`;
|
||||||
} else {
|
} else {
|
||||||
html = `<a href="${url}">${record._file.originalName}</a>`
|
html = `<a href="${url}">${file.originalName}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import RecordRow from "./RecordRow.svelte"
|
import RecordRow from "./RecordRow.svelte";
|
||||||
|
import { apiGet } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
let records = [];
|
let records = [];
|
||||||
let graph = null;
|
let graph = null;
|
||||||
let users = [];
|
let users = [];
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
axios
|
apiGet(channel.lucentUrl + "/home/records")
|
||||||
.get(channel.lucentUrl + "/home/records")
|
.then((data) => {
|
||||||
.then((response) => {
|
records = data.records;
|
||||||
records = response.data.records;
|
users = data.users;
|
||||||
graph = response.data.graph;
|
|
||||||
users = response.data.users;
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -21,21 +19,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
|
||||||
{#if records.length > 0}
|
{#if records.length > 0}
|
||||||
|
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<table class="">
|
<table class="">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<RecordRow {graph} {record} {users}/>
|
<RecordRow {record} {users} />
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { formatDistanceToNow, parseJSON } from "date-fns";
|
import { formatDistanceToNow, parseJSON } from "date-fns";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import {previewTitle} from "../records/Preview";
|
|
||||||
import Preview from "../files/Preview.svelte";
|
import Preview from "../files/Preview.svelte";
|
||||||
import { usernameById } from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let users;
|
export let users;
|
||||||
export let graph;
|
|
||||||
export let record;
|
export let record;
|
||||||
let schema = channel.schemas.find((s) => s.name === record.schema);
|
let schema = channel.schemas.find((s) => s.name === record.schema);
|
||||||
let frieldlyUpdatedAt = formatDistanceToNow(
|
let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.updatedAt), {
|
||||||
parseJSON(record._sys.updatedAt),
|
addSuffix: true,
|
||||||
{addSuffix: true}
|
});
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@@ -25,24 +22,17 @@
|
|||||||
{#if schema.type === "files"}
|
{#if schema.type === "files"}
|
||||||
<Preview {record} size="tiny" showFilename={true} />
|
<Preview {record} size="tiny" showFilename={true} />
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a href="{channel.lucentUrl}/records/{record.id}">
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
{record.data.name}
|
||||||
|
|
||||||
>
|
|
||||||
{previewTitle(channel.schemas, record, graph)}
|
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><a
|
<td><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
|
||||||
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex;gap: 14px">
|
<div style="display: flex;gap: 14px">
|
||||||
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
{frieldlyUpdatedAt}
|
{frieldlyUpdatedAt}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,21 +5,22 @@
|
|||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const user = getContext("user");
|
const user = getContext("user");
|
||||||
console.log( channel.commands)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
|
{#if channel.auth == "lucent"}
|
||||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||||
|
{/if}
|
||||||
{#if channel.commands.length > 0}
|
{#if channel.commands.length > 0}
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">Actions</div>
|
<div slot="button">Actions</div>
|
||||||
{#each channel.commands as command}
|
{#each channel.commands as command}
|
||||||
<a href="{channel.lucentUrl}/command-report/{command.signature}" class="top-nav-item">{command.name}</a>
|
<a
|
||||||
|
href="{channel.lucentUrl}/command-report/{command.signature}"
|
||||||
|
class="top-nav-item">{command.name}</a
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <div>-->
|
<!-- <div>-->
|
||||||
<!-- <form method="GET">-->
|
<!-- <form method="GET">-->
|
||||||
@@ -27,8 +28,11 @@
|
|||||||
<!-- class="form-control" required/>-->
|
<!-- class="form-control" required/>-->
|
||||||
<!-- </form>-->
|
<!-- </form>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
|
{#if channel.auth == "lucent"}
|
||||||
<a href="{channel.lucentUrl}/profile">
|
<a href="{channel.lucentUrl}/profile">
|
||||||
<Avatar side="28" name={user.name} />
|
<Avatar side="28" name={user.name} />
|
||||||
</a>
|
</a>
|
||||||
|
{:else}
|
||||||
|
<Avatar side="28" name={user.name} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import NavbarMenu from "./NavbarMenu.svelte";
|
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export let schema;
|
export let schema;
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const readableSchemas = getContext("readableSchemas");
|
const readableSchemas = getContext("readableSchemas");
|
||||||
|
|
||||||
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
|
|
||||||
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sidebar-top">
|
<div class="sidebar-top">
|
||||||
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
|
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
|
||||||
<a class="nav-item" href="{channel.lucentUrl}/profile">
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
{#each readableSchemas as aschema}
|
||||||
|
<a
|
||||||
<NavbarMenu
|
class="sidebar-item"
|
||||||
title="Content"
|
class:active={aschema.name === schema?.name}
|
||||||
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
|
aria-current="page"
|
||||||
schema={schema}
|
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a
|
||||||
expanded={true}
|
>
|
||||||
/>
|
{/each}
|
||||||
|
|
||||||
<NavbarMenu
|
|
||||||
title="Files"
|
|
||||||
schemas={ fileSchemas}
|
|
||||||
schema={schema}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NavbarMenu
|
|
||||||
title="Other"
|
|
||||||
schemas={ otherSchemas}
|
|
||||||
schema={schema}
|
|
||||||
/>
|
|
||||||
</div>
|
</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,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { basicSetup, EditorView } from "codemirror";
|
import { basicSetup, EditorView } from "codemirror";
|
||||||
@@ -17,10 +16,10 @@
|
|||||||
|
|
||||||
export function insertMedia(info) {
|
export function insertMedia(info) {
|
||||||
let insertText = "";
|
let insertText = "";
|
||||||
if (info.record._file.width > 0) {
|
if (info.file.width > 0) {
|
||||||
insertText = ``;
|
insertText = ``;
|
||||||
} else {
|
} else {
|
||||||
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
|
insertText = `[${info.file.filename}](${info.originalUrl})`;
|
||||||
}
|
}
|
||||||
const cursor = codeMirrorView.state.selection.main.head;
|
const cursor = codeMirrorView.state.selection.main.head;
|
||||||
const transaction = codeMirrorView.state.update({
|
const transaction = codeMirrorView.state.update({
|
||||||
@@ -46,11 +45,7 @@
|
|||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
keymap.of([
|
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
|
||||||
indentWithTab,
|
|
||||||
...lintKeymap,
|
|
||||||
...completionKeymap
|
|
||||||
]),
|
|
||||||
language.of(markdown()),
|
language.of(markdown()),
|
||||||
markdown(),
|
markdown(),
|
||||||
autocompletion(),
|
autocompletion(),
|
||||||
@@ -63,17 +58,14 @@
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.contentAttributes.of({spellcheck: "true"})
|
EditorView.contentAttributes.of({ spellcheck: "true" }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
codeMirrorView = new EditorView({
|
codeMirrorView = new EditorView({
|
||||||
state,
|
state,
|
||||||
parent: parentElement,
|
parent: parentElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {onDestroy, onMount} from "svelte";
|
|
||||||
|
|
||||||
import tinymce from "tinymce/tinymce";
|
|
||||||
import "tinymce/models/dom";
|
|
||||||
import "tinymce/icons/default";
|
|
||||||
import "tinymce/themes/silver";
|
|
||||||
import "tinymce/skins/ui/oxide/skin.css";
|
|
||||||
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css?inline";
|
|
||||||
import customcss from "./tinymce.css?inline";
|
|
||||||
|
|
||||||
import "tinymce/plugins/link";
|
|
||||||
import "tinymce/plugins/code";
|
|
||||||
import "tinymce/plugins/image";
|
|
||||||
import "tinymce/plugins/table";
|
|
||||||
import "tinymce/plugins/codesample";
|
|
||||||
import "tinymce/plugins/media";
|
|
||||||
|
|
||||||
import "tinymce/plugins/lists";
|
|
||||||
import "tinymce/plugins/autoresize";
|
|
||||||
import "tinymce/plugins/wordcount";
|
|
||||||
|
|
||||||
export let value = "";
|
|
||||||
export let additionalConfig = {};
|
|
||||||
let lastVal = "";
|
|
||||||
let textareaEl;
|
|
||||||
let activeEditor;
|
|
||||||
let editorWrapper;
|
|
||||||
const plugins = [
|
|
||||||
"autoresize",
|
|
||||||
"code",
|
|
||||||
"image",
|
|
||||||
"table",
|
|
||||||
"codesample",
|
|
||||||
"link",
|
|
||||||
"lists",
|
|
||||||
"media",
|
|
||||||
"wordcount",
|
|
||||||
];
|
|
||||||
const toolbar =
|
|
||||||
"bold italic underline strikethrough removeformat | link | subscript superscript bullist numlist media image codesample table code wordcount blockquote indent outdent blocks";
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (activeEditor) {
|
|
||||||
activeEditor.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const config = {
|
|
||||||
target: textareaEl,
|
|
||||||
toolbar_mode: "sliding",
|
|
||||||
toolbar_sticky: true,
|
|
||||||
skin: false,
|
|
||||||
content_css: false,
|
|
||||||
content_style: contentUiSkinCss.toString() + customcss.toString(),
|
|
||||||
branding: false,
|
|
||||||
inline: false,
|
|
||||||
plugins: plugins,
|
|
||||||
contextmenu: false,
|
|
||||||
menubar: false,
|
|
||||||
statusbar: false,
|
|
||||||
entity_encoding: "raw",
|
|
||||||
convert_urls: false,
|
|
||||||
toolbar: toolbar,
|
|
||||||
image_caption: true,
|
|
||||||
relative_urls: false,
|
|
||||||
browser_spellcheck: true,
|
|
||||||
max_height: 600,
|
|
||||||
// media_poster: false,
|
|
||||||
|
|
||||||
setup: function (editor) {
|
|
||||||
activeEditor = editor;
|
|
||||||
|
|
||||||
editor.on("init", function (e) {
|
|
||||||
editor.setContent(value ?? "");
|
|
||||||
});
|
|
||||||
|
|
||||||
// editor.on("blur", function (e) {
|
|
||||||
// let content = setImageDimensions(editor.getContent());
|
|
||||||
// dispatch("editorBlur", content);
|
|
||||||
// editorWrapper.classList.remove("editorFocus");
|
|
||||||
// // return false;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// editor.on("focus", function (e) {
|
|
||||||
// editorWrapper.classList.add("editorFocus");
|
|
||||||
// // return false;
|
|
||||||
// });
|
|
||||||
|
|
||||||
editor.on("change input undo redo", function (e) {
|
|
||||||
lastVal = editor.getContent();
|
|
||||||
if (lastVal !== value) {
|
|
||||||
value = lastVal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
tinymce.init({...config, ...additionalConfig});
|
|
||||||
});
|
|
||||||
|
|
||||||
export function insertMedia(info){
|
|
||||||
activeEditor.execCommand('InsertHTML', false, info.html);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={editorWrapper} class="tox-wrapper">
|
|
||||||
<div class="form-control" bind:this={textareaEl}>
|
|
||||||
{@html value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(.tox:not(.tox-tinymce-inline) .tox-editor-header) {
|
|
||||||
background-color: #fff;
|
|
||||||
border-bottom: 1px solid #ced4da;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 4px 0;
|
|
||||||
transition: box-shadow 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.tox-tinymce) {
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,61 +1,60 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import Trix from "trix"
|
import Trix from "trix";
|
||||||
import "trix/dist/trix.css"
|
import "trix/dist/trix.css";
|
||||||
|
|
||||||
export let value = "";
|
export let value = "";
|
||||||
export let field;
|
export let field;
|
||||||
let editor;
|
let editor;
|
||||||
|
|
||||||
|
|
||||||
function updateValue(e) {
|
function updateValue(e) {
|
||||||
value = e.target.value;
|
value = e.target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertMedia(info) {
|
export function insertMedia(info) {
|
||||||
if(info.record._file.width > 0){
|
if (info.file.width > 0) {
|
||||||
var attachment = new Trix.Attachment({ content: info.html })
|
var attachment = new Trix.Attachment({ content: info.html });
|
||||||
editor.editor.insertAttachment(attachment)
|
editor.editor.insertAttachment(attachment);
|
||||||
} else {
|
} else {
|
||||||
editor.editor.insertHTML(`<a href="${info.originalUrl}">${info.record._file.originalName}</a>`)
|
editor.editor.insertHTML(
|
||||||
|
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor.addEventListener("trix-file-accept", (e) => {
|
editor.addEventListener("trix-file-accept", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
})
|
});
|
||||||
|
|
||||||
editor.addEventListener("trix-before-initialize", (e) => {
|
editor.addEventListener("trix-before-initialize", (e) => {
|
||||||
Trix.config.blockAttributes.heading1.tagName = 'h2';
|
Trix.config.blockAttributes.heading1.tagName = "h2";
|
||||||
const { toolbarElement } = e.target
|
const { toolbarElement } = e.target;
|
||||||
const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]")
|
const h1Button = toolbarElement.querySelector(
|
||||||
h1Button.insertAdjacentHTML("afterend", `<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`)
|
"[data-trix-attribute=heading1]",
|
||||||
})
|
);
|
||||||
|
h1Button.insertAdjacentHTML(
|
||||||
|
"afterend",
|
||||||
})
|
`<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
// onDestroy(() => {
|
// onDestroy(() => {
|
||||||
// editor.removeEventListener("trix-before-initialize")
|
// editor.removeEventListener("trix-before-initialize")
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
Trix.config.blockAttributes.default.breakOnReturn = false;
|
||||||
Trix.config.blockAttributes.default.breakOnReturn = false
|
|
||||||
Trix.config.blockAttributes.heading3 = {
|
Trix.config.blockAttributes.heading3 = {
|
||||||
tagName: 'h3',
|
tagName: "h3",
|
||||||
terminal: true,
|
terminal: true,
|
||||||
breakOnReturn: true,
|
breakOnReturn: true,
|
||||||
group: false
|
group: false,
|
||||||
}
|
};
|
||||||
// console.log(Trix.config)
|
// console.log(Trix.config)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="tox-wrapper">
|
<div class="tox-wrapper">
|
||||||
<input id="x-{field.name}" {value} type="hidden">
|
<input id="x-{field.name}" {value} type="hidden" />
|
||||||
<trix-editor
|
<trix-editor
|
||||||
bind:this={editor}
|
bind:this={editor}
|
||||||
class=" content"
|
class=" content"
|
||||||
@@ -63,6 +62,5 @@
|
|||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:trix-change={updateValue}
|
on:trix-change={updateValue}
|
||||||
|
|
||||||
></trix-editor>
|
></trix-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
.mce-content-body .img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body{
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.mce-content-body p{
|
|
||||||
|
|
||||||
margin-bottom: 14px;
|
|
||||||
&:last-child{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body ul {
|
|
||||||
padding: 0 0 0 16px;
|
|
||||||
list-style: none outside none;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body li::before {
|
|
||||||
content: "—";
|
|
||||||
opacity: .5;
|
|
||||||
font-size: 12px;
|
|
||||||
padding-right: 6px;
|
|
||||||
vertical-align: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mce-content-body li {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
import SpinnerButton from "../common/SpinnerButton.svelte";
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
import MemberSettingsCard from "./MemberSettingsCard.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import axios from "axios";
|
import { apiPost } from "../../helpers";
|
||||||
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let users;
|
export let users;
|
||||||
@@ -23,15 +22,14 @@
|
|||||||
function invite(newName, newEmail, newRole) {
|
function invite(newName, newEmail, newRole) {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/invite", {
|
||||||
.post(channel.lucentUrl + "/members/invite", {
|
|
||||||
name: newName,
|
name: newName,
|
||||||
email: newEmail,
|
email: newEmail,
|
||||||
roles: [newRole],
|
roles: [newRole],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("User was invited");
|
successAlert.show("User was invited");
|
||||||
users = [...users, response.data.user];
|
users = [...users, response.user];
|
||||||
name = null;
|
name = null;
|
||||||
email = null;
|
email = null;
|
||||||
role = null;
|
role = null;
|
||||||
@@ -45,14 +43,13 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
|
||||||
axios
|
apiPost(channel.lucentUrl + "/members/update", {
|
||||||
.post(channel.lucentUrl + "/members/update", {
|
|
||||||
id: e.detail.user,
|
id: e.detail.user,
|
||||||
roles: e.detail.roles,
|
roles: e.detail.roles,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
successAlert.show("Users updated");
|
successAlert.show("Users updated");
|
||||||
users = response.data.users;
|
users = response.users;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorMessage = error.response?.data?.error ?? "";
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
@@ -68,9 +65,7 @@
|
|||||||
|
|
||||||
<form on:submit={submitInvite}>
|
<form on:submit={submitInvite}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="inviteeName" class="form-label"
|
<label for="inviteeName" class="form-label">Invitee Name</label>
|
||||||
>Invitee Name</label
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
@@ -97,10 +92,7 @@
|
|||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<select bind:value={role}>
|
<select bind:value={role}>
|
||||||
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
{#each channel.roles.filter((r) => r !== "removed") as arole}
|
||||||
<option
|
<option value={arole}>{arole}</option>
|
||||||
value={arole}
|
|
||||||
|
|
||||||
>{arole}</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { afterUpdate, getContext, onMount } from "svelte";
|
import { afterUpdate, getContext, onMount } from "svelte";
|
||||||
import {isEqual} from "lodash";
|
import EditHeader from "./header/EditHeader.svelte";
|
||||||
import axios from "axios";
|
import ContentTabs from "./header/ContentTabs.svelte";
|
||||||
import EditHeader from "./header/EditHeader.svelte"
|
import FormField from "./FormField.svelte";
|
||||||
import FilePreview from "./FilePreview.svelte"
|
import Graph from "./Graph.svelte";
|
||||||
import ContentTabs from "./header/ContentTabs.svelte"
|
import Info from "./Info.svelte";
|
||||||
import FormField from "./FormField.svelte"
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import Graph from "./Graph.svelte"
|
|
||||||
import Info from "./Info.svelte"
|
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte"
|
|
||||||
import Title from "./header/Title.svelte";
|
import Title from "./header/Title.svelte";
|
||||||
|
import { apiPost, isEqual } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
edges: []
|
edges: [],
|
||||||
};
|
};
|
||||||
// export let recordHistory;
|
// export let recordHistory;
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
@@ -33,10 +31,7 @@
|
|||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
(f) => f.name !== "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
@@ -45,10 +40,7 @@
|
|||||||
function setOriginalContent() {
|
function setOriginalContent() {
|
||||||
originalContent = {
|
originalContent = {
|
||||||
data: JSON.parse(JSON.stringify(record.data)),
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
schema: record.schema,
|
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
|
||||||
_file: JSON.parse(JSON.stringify(record._file)),
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -79,10 +71,7 @@
|
|||||||
}
|
}
|
||||||
return !isEqual(originalContent, {
|
return !isEqual(originalContent, {
|
||||||
data: record.data,
|
data: record.data,
|
||||||
schema: record.schema,
|
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: record._sys,
|
|
||||||
_file: record._file,
|
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,9 +93,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
graph.edges = graph.edges?.filter(
|
||||||
axios
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
.post(channel.lucentUrl + "/records", {
|
);
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
record: record,
|
record: record,
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
@@ -115,16 +105,17 @@
|
|||||||
console.log("SAVE: SAVED");
|
console.log("SAVE: SAVED");
|
||||||
|
|
||||||
if (isCreateMode) {
|
if (isCreateMode) {
|
||||||
window.location = channel.lucentUrl + "/records/" + record.id;
|
window.location =
|
||||||
|
channel.lucentUrl + "/records/" + record.id;
|
||||||
} else {
|
} else {
|
||||||
record = response.data.records[0] ?? null;
|
record = response.records[0] ?? null;
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// means trashed
|
// means trashed
|
||||||
hasUnsavedData = false;
|
hasUnsavedData = false;
|
||||||
window.location = channel.lucentUrl;
|
window.location = channel.lucentUrl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
graph = response.data;
|
graph = response;
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +128,7 @@
|
|||||||
errorMessage = error.response.data.error;
|
errorMessage = error.response.data.error;
|
||||||
} else {
|
} else {
|
||||||
validationErrors = error.response.data.error;
|
validationErrors = error.response.data.error;
|
||||||
console.log(validationErrors)
|
console.log(validationErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -156,10 +147,7 @@
|
|||||||
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
||||||
{#if isCreateMode}
|
{#if isCreateMode}
|
||||||
<button
|
<button class="button primary btn-spinner" on:click={save}>
|
||||||
class="button primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
@@ -181,26 +169,19 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Title {schema} {record} {isCreateMode} />
|
<Title {schema} {record} {isCreateMode} />
|
||||||
|
|
||||||
|
|
||||||
<ErrorAlert message={errorMessage} />
|
<ErrorAlert message={errorMessage} />
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<ContentTabs
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
{schema}
|
|
||||||
{isCreateMode}
|
|
||||||
bind:active={activeContentTab}
|
|
||||||
/>
|
|
||||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||||
<FilePreview {record} {schema}/>
|
|
||||||
{#each activeFields as field (field.name)}
|
{#each activeFields as field (field.name)}
|
||||||
{#if activeContentTab === field.group}
|
{#if activeContentTab === field.group}
|
||||||
<FormField
|
<FormField
|
||||||
bind:data={record.data}
|
bind:data={record.data}
|
||||||
bind:graph={graph}
|
bind:graph
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../files/Preview.svelte";
|
import Preview from "../files/Preview.svelte";
|
||||||
import {fileurl} from "../files/imageserver"
|
import { fileurl } from "../files/imageserver";
|
||||||
import {getContext} from "svelte"
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
<Preview {record} size="large" />
|
<Preview {record} size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div class="file-details">
|
<div class="file-details">
|
||||||
|
|
||||||
<div class="file-details-item">
|
<div class="file-details-item">
|
||||||
<span class="text-muted">Filename</span>
|
<span class="text-muted">Filename</span>
|
||||||
<span>{record._file.path}</span>
|
<span>{record._file.path}</span>
|
||||||
@@ -42,9 +41,13 @@
|
|||||||
<span>{record._file.checksum}</span>
|
<span>{record._file.checksum}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-details-item">
|
<div class="file-details-item">
|
||||||
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(channel,record)}">Download</a>
|
<a
|
||||||
|
class="button primary"
|
||||||
|
target="_blank"
|
||||||
|
style="display: inline-flex"
|
||||||
|
href={fileurl(channel, record)}>Download</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -47,23 +47,18 @@
|
|||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<FieldHeader {field} {id} />
|
<FieldHeader {field} {id} />
|
||||||
{#if field.info.name === "reference" && field.layout === "tags"}
|
{#if field.info.name === "reference" && field.layout === "tags"}
|
||||||
<ReferenceTags
|
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
|
||||||
bind:graph
|
|
||||||
{id}
|
|
||||||
{record}
|
|
||||||
{field}
|
|
||||||
{validationErrors}
|
|
||||||
/>
|
|
||||||
{:else if field.info.name === "reference"}
|
{:else if field.info.name === "reference"}
|
||||||
<Reference
|
<Reference bind:graph {id} {record} {field} {validationErrors} />
|
||||||
bind:graph
|
{:else if field.info.name === "file"}
|
||||||
{id}
|
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
|
||||||
|
<File
|
||||||
|
bind:value={data[field.name]}
|
||||||
{record}
|
{record}
|
||||||
|
{id}
|
||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
/>
|
/>
|
||||||
{:else if field.info.name === "file"}
|
|
||||||
<File bind:graph {record} {field} {validationErrors}/>
|
|
||||||
{:else if field.info.name === "text"}
|
{:else if field.info.name === "text"}
|
||||||
<Text
|
<Text
|
||||||
bind:value={data[field.name]}
|
bind:value={data[field.name]}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {friendlyDate} from "../../helpers";
|
import { friendlyDate, isEqual } from "../../helpers";
|
||||||
import Avatar from "../account/Avatar.svelte";
|
import Avatar from "../account/Avatar.svelte";
|
||||||
import { usernameById } from "../account/users";
|
import { usernameById } from "../account/users";
|
||||||
import {isEqual} from "lodash";
|
|
||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
import RevisionCell from "./revisions/RevisionCell.svelte";
|
import RevisionCell from "./revisions/RevisionCell.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
@@ -30,27 +30,27 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getEdgesByField(fieldsWithDiff, revision) {
|
function getEdgesByField(fieldsWithDiff, revision) {
|
||||||
|
edgeFieldsDiff = graph.edges
|
||||||
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
|
.filter((e) => e.depth === 1)
|
||||||
|
.reduce((c, e) => {
|
||||||
if (!c[e.field]) {
|
if (!c[e.field]) {
|
||||||
c[e.field] = {
|
c[e.field] = {
|
||||||
record: [],
|
record: [],
|
||||||
revision: [],
|
revision: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
c[e.field]["record"].push(e);
|
||||||
c[e.field]["record"].push(e)
|
|
||||||
return c;
|
return c;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
edgeFieldsDiff = revision._edges.reduce((c, e) => {
|
edgeFieldsDiff = revision._edges.reduce((c, e) => {
|
||||||
if (!c[e.field]) {
|
if (!c[e.field]) {
|
||||||
c[e.field] = {
|
c[e.field] = {
|
||||||
record: [],
|
record: [],
|
||||||
revision: [],
|
revision: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
c[e.field]["revision"].push(e);
|
||||||
c[e.field]["revision"].push(e)
|
|
||||||
return c;
|
return c;
|
||||||
}, edgeFieldsDiff);
|
}, edgeFieldsDiff);
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
fieldsWithDiff = schema.fields.filter((f) => {
|
fieldsWithDiff = schema.fields.filter((f) => {
|
||||||
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
|
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
|
||||||
});
|
});
|
||||||
getEdgesByField(fieldsWithDiff, revision)
|
getEdgesByField(fieldsWithDiff, revision);
|
||||||
revisionSection.scrollIntoView();
|
revisionSection.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
rollbackError = "";
|
rollbackError = "";
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
|
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -93,29 +93,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">current version </span>
|
<span class="label text-end text-muted">current version </span>
|
||||||
{record._sys.version}
|
{record.version}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted"> created </span>
|
<span class="label text-end text-muted"> created </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.createdBy)}
|
name={usernameById(users, record.createdBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.createdAt)}
|
{friendlyDate(record.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="label text-end text-muted">updated </span>
|
<span class="label text-end text-muted">updated </span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, record._sys.updatedBy)}
|
name={usernameById(users, record.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(record._sys.updatedAt)}
|
{friendlyDate(record.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="label d-block text-muted "
|
<span class="label d-block text-muted">Rules for this schema </span>
|
||||||
>Rules for this schema
|
|
||||||
</span>
|
|
||||||
<small>
|
<small>
|
||||||
Each record maintains the last {schema.revisions}
|
Each record maintains the last {schema.revisions}
|
||||||
versions
|
versions
|
||||||
@@ -127,31 +125,29 @@
|
|||||||
{#if schema.revisions > 0}
|
{#if schema.revisions > 0}
|
||||||
<div class="header-small mb-3">Revisions</div>
|
<div class="header-small mb-3">Revisions</div>
|
||||||
{#each revisions as revision}
|
{#each revisions as revision}
|
||||||
{#if revision._sys.version !== record._sys.version}
|
{#if revision.version !== record.version}
|
||||||
<div
|
<div
|
||||||
class="revision"
|
class="revision"
|
||||||
class:active={revision._sys.version ===
|
class:active={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="version">
|
<div class="version">
|
||||||
<span>version {revision._sys.version}</span>
|
<span>version {revision.version}</span>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={usernameById(users, revision._sys.updatedBy)}
|
name={usernameById(users, revision.updatedBy)}
|
||||||
side={24}
|
side={24}
|
||||||
/>
|
/>
|
||||||
{friendlyDate(revision._sys.updatedAt)}
|
{friendlyDate(revision.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
<button
|
<button
|
||||||
disabled={revision._sys.version ===
|
disabled={revision.version ===
|
||||||
selectedRevision?._sys.version}
|
selectedRevision?.version}
|
||||||
class="button"
|
class="button"
|
||||||
on:click={(e) => compare(e, revision)}
|
on:click={(e) => compare(e, revision)}
|
||||||
>Compare
|
>Compare
|
||||||
</button
|
</button>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -169,15 +165,13 @@
|
|||||||
<p class="text-center fw-bold mb-3 mt-5">
|
<p class="text-center fw-bold mb-3 mt-5">
|
||||||
If you choose to rollback to this revision
|
If you choose to rollback to this revision
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button on:click={rollback} class="button">
|
||||||
on:click={rollback}
|
Rollback to version {selectedRevision.version}
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
Rollback to version {selectedRevision._sys.version}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if rollbackError}
|
{#if rollbackError}
|
||||||
<span class="d-block text-danger mt-3">{rollbackError}</span>
|
<span class="d-block text-danger mt-3">{rollbackError}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
{#each fieldsWithDiff as field}
|
{#each fieldsWithDiff as field}
|
||||||
@@ -188,10 +182,7 @@
|
|||||||
<!-- <div class="d-block" style="width:200px;">
|
<!-- <div class="d-block" style="width:200px;">
|
||||||
{field.label}
|
{field.label}
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div class="revision-field" style="overflow:hidden">
|
||||||
class="revision-field"
|
|
||||||
style="overflow:hidden"
|
|
||||||
>
|
|
||||||
<div class="compare-left">
|
<div class="compare-left">
|
||||||
<RevisionCell
|
<RevisionCell
|
||||||
{field}
|
{field}
|
||||||
@@ -226,19 +217,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<p class="text-center fw-bold mb-3 mt-5">
|
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
|
||||||
Record References
|
|
||||||
</p>
|
|
||||||
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
|
||||||
<div
|
<div class="revision-references" style="overflow:hidden">
|
||||||
class="revision-references"
|
|
||||||
style="overflow:hidden"
|
|
||||||
>
|
|
||||||
<div class="reference-field">
|
<div class="reference-field">
|
||||||
{field}:
|
{field}:
|
||||||
</div>
|
</div>
|
||||||
<div class="reference-compare">
|
<div class="reference-compare">
|
||||||
|
|
||||||
<p class="">Record</p>
|
<p class="">Record</p>
|
||||||
{#each edges.record as edge}
|
{#each edges.record as edge}
|
||||||
<RevisionEdgeRow {edge} />
|
<RevisionEdgeRow {edge} />
|
||||||
@@ -258,7 +243,5 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
<script>
|
<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 FormField from "./FormField.svelte";
|
||||||
import FilePreview from "./FilePreview.svelte";
|
import FilePreview from "./FilePreview.svelte";
|
||||||
import ContentTabs from "./header/ContentTabs.svelte";
|
import ContentTabs from "./header/ContentTabs.svelte";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
import EditHeader from "./header/EditHeader.svelte";
|
import EditHeader from "./header/EditHeader.svelte";
|
||||||
import axios from "axios";
|
|
||||||
import Title from "./header/Title.svelte";
|
import Title from "./header/Title.svelte";
|
||||||
|
import { apiPost, isEqual } from "../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -16,7 +20,7 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
edges: []
|
edges: [],
|
||||||
};
|
};
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
let originalContent;
|
let originalContent;
|
||||||
@@ -29,9 +33,7 @@
|
|||||||
} error(s)`
|
} error(s)`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter((f) => f.name !== "id");
|
||||||
(f) => f.name !== "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
let tabname = "_default";
|
let tabname = "_default";
|
||||||
let fieldToTabs = schema.fields.reduce((c, f) => {
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
@@ -53,8 +55,6 @@
|
|||||||
data: JSON.parse(JSON.stringify(record.data)),
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
schema: record.schema,
|
schema: record.schema,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
|
||||||
_file: JSON.parse(JSON.stringify(record._file)),
|
|
||||||
edges: JSON.parse(JSON.stringify(graph.edges)),
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,8 +87,6 @@
|
|||||||
data: record.data,
|
data: record.data,
|
||||||
schema: record.schema,
|
schema: record.schema,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
_sys: record._sys,
|
|
||||||
_file: record._file,
|
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,11 +112,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// remove trashed edges
|
// remove trashed edges
|
||||||
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
|
graph.edges =
|
||||||
|
graph.edges?.filter(
|
||||||
|
(edge) => !edge._isTrashed && edge.source === record.id,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
apiPost(channel.lucentUrl + "/records", {
|
||||||
axios
|
|
||||||
.post(channel.lucentUrl + "/records", {
|
|
||||||
record: record,
|
record: record,
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
@@ -126,8 +125,8 @@
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log("SAVE: SAVED INLINE");
|
console.log("SAVE: SAVED INLINE");
|
||||||
|
|
||||||
record = response.data.records[0];
|
record = response.records[0];
|
||||||
graph = response.data;
|
graph = response;
|
||||||
if (!isCreateMode) {
|
if (!isCreateMode) {
|
||||||
setOriginalContent();
|
setOriginalContent();
|
||||||
}
|
}
|
||||||
@@ -156,10 +155,7 @@
|
|||||||
<div class="tools-header">
|
<div class="tools-header">
|
||||||
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
|
||||||
{#if isCreateMode}
|
{#if isCreateMode}
|
||||||
<button
|
<button class="button primary btn-spinner" on:click={save}>
|
||||||
class="button primary btn-spinner"
|
|
||||||
on:click={save}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
@@ -181,24 +177,19 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Title {schema} {record} {isCreateMode} />
|
<Title {schema} {record} {isCreateMode} />
|
||||||
<ErrorAlert message={errorMessage} />
|
<ErrorAlert message={errorMessage} />
|
||||||
|
|
||||||
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
|
||||||
<ContentTabs
|
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
|
||||||
{schema}
|
|
||||||
{isCreateMode}
|
|
||||||
bind:active={activeContentTab}
|
|
||||||
/>
|
|
||||||
<FilePreview {record} {schema} />
|
<FilePreview {record} {schema} />
|
||||||
<!-- <fieldset disabled="disabled"> -->
|
<!-- <fieldset disabled="disabled"> -->
|
||||||
{#each activeFields as field (field.name)}
|
{#each activeFields as field (field.name)}
|
||||||
{#if activeContentTab === field.group}
|
{#if activeContentTab === field.group}
|
||||||
<FormField
|
<FormField
|
||||||
bind:data={record.data}
|
bind:data={record.data}
|
||||||
bind:graph={graph}
|
bind:graph
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{record}
|
{record}
|
||||||
@@ -210,4 +201,3 @@
|
|||||||
<!-- </fieldset> -->
|
<!-- </fieldset> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import Mustache from "mustache";
|
|
||||||
import {stripHtml} from "../../helpers";
|
|
||||||
|
|
||||||
export function previewTitle(schemas, record, graph) {
|
|
||||||
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
|
|
||||||
if (!schema?.cardTitle) {
|
|
||||||
return noTemplate(schema, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
let recordData = record.data;
|
|
||||||
let render = Mustache.render(schema.cardTitle, recordData);
|
|
||||||
if (!render || render === "") {
|
|
||||||
return noTemplate(schema, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stripHtml(render.slice(0, 300));
|
|
||||||
}
|
|
||||||
|
|
||||||
function noTemplate(schema, record) {
|
|
||||||
if (schema?.type === "files") {
|
|
||||||
return record._file.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = stripHtml(
|
|
||||||
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
|
||||||
).slice(0, 300);
|
|
||||||
|
|
||||||
if(title.trim() === ""){
|
|
||||||
return "~Untitled~";
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {previewTitle} from "./Preview";
|
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
export let graph;
|
$: title = record.data.name;
|
||||||
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
|
||||||
|
|
||||||
$: title = previewTitle(channel.schemas, record, graph);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if record?.data}
|
{#if record?.data}
|
||||||
<a
|
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
|
||||||
{title}
|
|
||||||
class="reference"
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,84 +1,69 @@
|
|||||||
<script>
|
<script>
|
||||||
import {sortByField} from "../../edges/sortEdges";
|
import { array_move } from "../../edges/sortEdges";
|
||||||
import Sortable from "../../libs/Sortable.svelte";
|
import Sortable from "../../libs/Sortable.svelte";
|
||||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import FileDialog from "../../dialog/FileDialog.svelte";
|
||||||
import Dialog from "../../dialog/Dialog.svelte";
|
import Uploader from "../../files/Uploader.svelte";
|
||||||
import {insertEdges} from "./reference.js";
|
|
||||||
import {getContext} from "svelte";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
|
||||||
export let field;
|
export let field;
|
||||||
export let record;
|
export let record;
|
||||||
export let graph
|
export let value = [];
|
||||||
let browseModal;
|
let browseModal;
|
||||||
$: references = 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) =>
|
function removeFile(e) {
|
||||||
field.collections.includes(aschema.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
function removeReference(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = graph.edges.filter(
|
value = value.filter((f) => !(f.id === e.detail));
|
||||||
(edge) => !(edge.target === e.detail && edge.field === field.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBrowseModal(e, schema) {
|
|
||||||
e.preventDefault();
|
|
||||||
browseModal.open(schema);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reorder(e) {
|
async function reorder(e) {
|
||||||
|
value = await array_move(value, e.detail.source, e.detail.target);
|
||||||
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e) {
|
function insertFiles(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
browseModal.close();
|
browseModal.close();
|
||||||
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
|
value = [...(value ?? []), ...(e.detail ?? [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceFiles(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.close();
|
||||||
|
value = e.detail ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadComplete(e) {
|
||||||
|
// value = [...value, e.detail];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowseModal(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.open(record.id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
{#if field.collections.length === 1}
|
<button class="button" on:click={openBrowseModal}> Browse </button>
|
||||||
<button
|
|
||||||
class="button"
|
<div>
|
||||||
on:click={(e) => openBrowseModal(e, collections[0].name)}
|
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<Dropdown>
|
|
||||||
<div slot="button">
|
|
||||||
Browse
|
|
||||||
</div>
|
</div>
|
||||||
{#each collections as collection}
|
|
||||||
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
on:click={(e) => openBrowseModal(e, collection.name)}
|
|
||||||
href="/">{collection.label}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</Dropdown>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if references.length > 0}
|
{#if value.length > 0}
|
||||||
<Sortable sortableClass="mt-3" on:update={reorder}>
|
<Sortable sortableClass="mt-3" on:update={reorder}>
|
||||||
{#each references as reference (reference.id)}
|
{#each value ?? [] as aFile (aFile.id)}
|
||||||
<!--This div helps the sorting thing-->
|
<!--This div helps the sorting thing-->
|
||||||
<div>
|
<div>
|
||||||
<PreviewFile record={reference} hasDelete={true} on:remove={removeReference}></PreviewFile>
|
<PreviewFile
|
||||||
|
file={aFile}
|
||||||
|
hasDelete={true}
|
||||||
|
on:remove_file={removeFile}
|
||||||
|
></PreviewFile>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Sortable>
|
</Sortable>
|
||||||
{/if}
|
{/if}
|
||||||
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
|
<FileDialog
|
||||||
|
bind:this={browseModal}
|
||||||
|
on:insert_files={insertFiles}
|
||||||
|
on:replace_files={replaceFiles}
|
||||||
|
></FileDialog>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||||
import Sortable from "../../libs/Sortable.svelte";
|
import Sortable from "../../libs/Sortable.svelte";
|
||||||
import PreviewReference from "../previews/PreviewReference.svelte";
|
import PreviewReference from "../previews/PreviewReference.svelte";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let record;
|
||||||
@@ -15,27 +14,36 @@
|
|||||||
export let validationErrors;
|
export let validationErrors;
|
||||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
|
||||||
|
$: references =
|
||||||
$: references = graph.edges
|
graph.edges
|
||||||
.filter((edge) => edge.field === field.name)
|
.filter((edge) => edge.field === field.name)
|
||||||
.map((edge) => {
|
.map((edge) => {
|
||||||
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
|
return graph.records.find(
|
||||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
(increc) =>
|
||||||
|
increc.id === edge.target && record.id === edge.source,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
let collections = channel.schemas.filter((aschema) =>
|
let collections = channel.schemas.filter((aschema) =>
|
||||||
field.collections.includes(aschema.name)
|
field.collections.includes(aschema.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
function removeReference(e) {
|
function removeReference(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = graph.edges.filter(
|
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) {
|
function reorder(e) {
|
||||||
|
graph.edges = sortByField(
|
||||||
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
|
e.detail.source,
|
||||||
|
e.detail.target,
|
||||||
|
graph.edges,
|
||||||
|
field.name,
|
||||||
|
references,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e) {
|
function insert(e) {
|
||||||
@@ -49,9 +57,14 @@
|
|||||||
// }).then(function (response) {
|
// }).then(function (response) {
|
||||||
// graph = response.data.graph;
|
// 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>
|
</script>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import InlineEdit from "../InlineEdit.svelte";
|
import InlineEdit from "../InlineEdit.svelte";
|
||||||
import Dialog from "../../dialog/Dialog.svelte";
|
import Dialog from "../../dialog/Dialog.svelte";
|
||||||
import DialogRecord from "../../dialog/DialogRecord.svelte";
|
import DialogRecord from "../../dialog/DialogRecord.svelte";
|
||||||
import axios from "axios";
|
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
import { apiGet } from "../../../helpers";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("Save inline");
|
console.log("Save inline");
|
||||||
inLineCreateRecord = null;
|
inLineCreateRecord = null;
|
||||||
dialogRecord.close()
|
dialogRecord.close();
|
||||||
dispatch("save", {
|
dispatch("save", {
|
||||||
records: e.detail.records,
|
records: e.detail.records,
|
||||||
after: recordId,
|
after: recordId,
|
||||||
@@ -44,11 +44,10 @@
|
|||||||
function createInlineReference(e, schemaUId) {
|
function createInlineReference(e, schemaUId) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inLineCreateRecord = null;
|
inLineCreateRecord = null;
|
||||||
axios
|
apiGet(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
||||||
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
inLineCreateRecord = response.data;
|
inLineCreateRecord = response;
|
||||||
dialogRecord.open()
|
dialogRecord.open();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -57,20 +56,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if schemas.length > 1}
|
{#if schemas.length > 1}
|
||||||
<div
|
<div style="display: flex;align-items: center;gap:4px">
|
||||||
style="display: flex;align-items: center;gap:4px"
|
|
||||||
>
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">New</div>
|
<div slot="button">New</div>
|
||||||
{#each schemas as schema}
|
{#each schemas as schema}
|
||||||
<button
|
<button
|
||||||
class=" button"
|
class=" button"
|
||||||
on:click={(e) =>
|
on:click={(e) => createInlineReference(e, schema.name)}
|
||||||
createInlineReference(e, schema.name)}
|
|
||||||
>{schema.label}
|
>{schema.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button"><Icon icon="magnifying-glass" /></div>
|
<div slot="button"><Icon icon="magnifying-glass" /></div>
|
||||||
@@ -81,7 +76,6 @@
|
|||||||
>{schema.label}
|
>{schema.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -96,8 +90,7 @@
|
|||||||
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
on:click={(e) => openBrowseModal(e, schemas[0].name)}
|
||||||
>
|
>
|
||||||
<Icon icon="magnifying-glass" />
|
<Icon icon="magnifying-glass" />
|
||||||
</button
|
</button>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -109,7 +102,6 @@
|
|||||||
on:cancel={(e) => (inLineCreateRecord = null)}
|
on:cancel={(e) => (inLineCreateRecord = null)}
|
||||||
on:inlinesaved={save}
|
on:inlinesaved={save}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</DialogRecord>
|
</DialogRecord>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {debounce} from "lodash";
|
|
||||||
import {previewTitle} from "../Preview";
|
|
||||||
import { getErrorMessage } from "./errorMessage";
|
import { getErrorMessage } from "./errorMessage";
|
||||||
import { insertEdges } from "./reference.js";
|
import { insertEdges } from "./reference.js";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
@@ -15,20 +13,24 @@
|
|||||||
export let validationErrors;
|
export let validationErrors;
|
||||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
|
||||||
$: references = graph.edges
|
$: references =
|
||||||
|
graph.edges
|
||||||
.filter((edge) => edge.field === field.name)
|
.filter((edge) => edge.field === field.name)
|
||||||
.map((edge) => {
|
.map((edge) => {
|
||||||
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
|
return graph.records.find(
|
||||||
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
(increc) =>
|
||||||
|
increc.id == edge.target && record.id == edge.source,
|
||||||
let search = ""
|
);
|
||||||
$: searchOptions = []
|
})
|
||||||
|
.filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
|
let search = "";
|
||||||
|
$: searchOptions = [];
|
||||||
|
|
||||||
function removeReference(e, recordId) {
|
function removeReference(e, recordId) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph.edges = graph.edges.filter(
|
graph.edges = graph.edges.filter(
|
||||||
(edge) => !(edge.target === recordId && edge.field === field.name)
|
(edge) => !(edge.target === recordId && edge.field === field.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,14 +43,14 @@
|
|||||||
schema: field.collections[0],
|
schema: field.collections[0],
|
||||||
status: "published",
|
status: "published",
|
||||||
data: {
|
data: {
|
||||||
[field.searchField]: newValue
|
[field.searchField]: newValue,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
insert(e, response.data.records[0]);
|
insert(e, response.data.records[0]);
|
||||||
console.log(response)
|
console.log(response);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
searchOptions = [];
|
searchOptions = [];
|
||||||
@@ -58,10 +60,16 @@
|
|||||||
|
|
||||||
function insert(e, insertRecord) {
|
function insert(e, insertRecord) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
|
graph = insertEdges(
|
||||||
search = ""
|
graph,
|
||||||
searchEl.focus()
|
record,
|
||||||
searchEl.blur()
|
[insertRecord],
|
||||||
|
field.name,
|
||||||
|
e.detail.action,
|
||||||
|
);
|
||||||
|
search = "";
|
||||||
|
searchEl.focus();
|
||||||
|
searchEl.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
@@ -82,9 +90,8 @@
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="reference-tags">
|
<div class="reference-tags">
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="invalid-feedback d-block mb-3">
|
<div class="invalid-feedback d-block mb-3">
|
||||||
@@ -104,7 +111,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reference-tags-results">
|
<div class="reference-tags-results">
|
||||||
|
|
||||||
{#if searchOptions}
|
{#if searchOptions}
|
||||||
{#each searchOptions as option (option.id)}
|
{#each searchOptions as option (option.id)}
|
||||||
<div
|
<div
|
||||||
@@ -114,14 +120,10 @@
|
|||||||
on:click={(e) => insert(e, option)}
|
on:click={(e) => insert(e, option)}
|
||||||
on:keypress={(e) => insert(e, option)}
|
on:keypress={(e) => insert(e, option)}
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, option ,graph)}
|
{option.data.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="start-typing">Start typing...</div>
|
||||||
class="start-typing">
|
|
||||||
Start typing...
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{#if search}
|
{#if search}
|
||||||
@@ -146,22 +148,19 @@
|
|||||||
class="record-title"
|
class="record-title"
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, record)}
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => removeReference(e, record.id)}
|
on:click|preventDefault={(e) =>
|
||||||
|
removeReference(e, record.id)}
|
||||||
type="button"
|
type="button"
|
||||||
class="button-text"
|
class="button-text"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<Icon width={12} height={12} icon="close"></Icon>
|
<Icon width={12} height={12} icon="close"></Icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import Tinymce from "../../libs/Tinymce.svelte";
|
|
||||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||||
import { getErrorMessage } from "./errorMessage";
|
import { getErrorMessage } from "./errorMessage";
|
||||||
import Trix from "../../libs/Trix.svelte";
|
import Trix from "../../libs/Trix.svelte";
|
||||||
@@ -19,12 +18,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function insertMedia(e) {
|
function insertMedia(e) {
|
||||||
editor.insertMedia(e.detail)
|
editor.insertMedia(e.detail);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
|
|
||||||
<Trix {field} bind:this={editor} bind:value></Trix>
|
<Trix {field} bind:this={editor} bind:value></Trix>
|
||||||
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
|
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
|
||||||
{#if field.collections.length > 0}
|
{#if field.collections.length > 0}
|
||||||
@@ -34,13 +32,9 @@
|
|||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
on:editor-insert={insertMedia}
|
on:editor-insert={insertMedia}
|
||||||
>
|
></RichEditorFiles>
|
||||||
|
|
||||||
|
|
||||||
</RichEditorFiles>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="invalid-feedback d-block">
|
<div class="invalid-feedback d-block">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import { getErrorMessage } from "./errorMessage";
|
import { getErrorMessage } from "./errorMessage";
|
||||||
@@ -14,12 +13,11 @@
|
|||||||
|
|
||||||
function generateId(e) {
|
function generateId(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
value = uuidv4();
|
value = self.crypto.randomUUID();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { uniqueId } from "lodash";
|
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
const channelurl = getContext("channelurl");
|
const channelurl = getContext("channelurl");
|
||||||
export let field;
|
export let field;
|
||||||
export let value;
|
export let value;
|
||||||
export let schema;
|
export let schema;
|
||||||
let id = uniqueId();
|
let id = self.crypto.randomUUID();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import {uniqBy} from "lodash";
|
import { arrayUniqueCb } from "../../../helpers";
|
||||||
|
|
||||||
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") {
|
export function insertEdges(
|
||||||
|
graph,
|
||||||
|
sourceRecord,
|
||||||
|
targetRecords,
|
||||||
|
fieldName,
|
||||||
|
action = "",
|
||||||
|
) {
|
||||||
let newEdges = targetRecords.map((r) => {
|
let newEdges = targetRecords.map((r) => {
|
||||||
return {
|
return {
|
||||||
target: r.id,
|
target: r.id,
|
||||||
@@ -9,7 +15,7 @@ export function insertEdges(graph, sourceRecord, targetRecords, fieldName, actio
|
|||||||
targetSchema: r.schema,
|
targetSchema: r.schema,
|
||||||
field: fieldName,
|
field: fieldName,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
rank: ""
|
rank: "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +24,13 @@ export function insertEdges(graph, sourceRecord, targetRecords, fieldName, actio
|
|||||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id);
|
graph.records = arrayUniqueCb(
|
||||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth);
|
[...graph.records, ...targetRecords],
|
||||||
|
(r) => r.id,
|
||||||
|
);
|
||||||
|
graph.edges = arrayUniqueCb(
|
||||||
|
[...replacedEdges, ...newEdges],
|
||||||
|
(edge) => edge.source + edge.target + edge.field + edge.depth,
|
||||||
|
);
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
import StatusSelect from "./StatusSelect.svelte";
|
import StatusSelect from "./StatusSelect.svelte";
|
||||||
|
import { apiPost } from "../../../helpers";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let schema;
|
||||||
@@ -12,16 +13,14 @@
|
|||||||
|
|
||||||
function clone(e) {
|
function clone(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
|
apiPost(channel.lucentUrl + "/records/clone/" + record.id)
|
||||||
window.location = channel.lucentUrl + "/records/" + response.data.id;
|
.then((response) => {
|
||||||
}).catch(error => {
|
window.location = channel.lucentUrl + "/records/" + response.id;
|
||||||
|
})
|
||||||
});
|
.catch((error) => {});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style="display: flex;align-items: center; gap:10px;">
|
<div style="display: flex;align-items: center; gap:10px;">
|
||||||
{#if !isCreateMode}
|
{#if !isCreateMode}
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
@@ -44,10 +43,9 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
on:click|preventDefault={(e) =>
|
on:click|preventDefault={(e) => (activeContentTab = "_info")}
|
||||||
(activeContentTab = "_info")}
|
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="{channel.lucentUrl}">Revisions</a
|
href={channel.lucentUrl}>Revisions</a
|
||||||
>
|
>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let schema;
|
export let schema;
|
||||||
export let record;
|
export let record;
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="record-header">
|
<div class="record-header">
|
||||||
|
<a class="schema-name" href="{channel.lucentUrl}/content/{schema.name}"
|
||||||
<a
|
|
||||||
class="schema-name"
|
|
||||||
href="{channel.lucentUrl}/content/{schema.name}"
|
|
||||||
>{schema.label.toUpperCase()}</a
|
>{schema.label.toUpperCase()}</a
|
||||||
>
|
>
|
||||||
<span class="record-title">
|
<span class="record-title">
|
||||||
{#if !isCreateMode}
|
{#if !isCreateMode}
|
||||||
{previewTitle(channel.schemas, record)}
|
{record.data.name}
|
||||||
{:else}
|
{:else}
|
||||||
New Record
|
New Record
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -3,89 +3,75 @@
|
|||||||
|
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
import {fileurl, htmlurl} from "../../files/imageserver.js"
|
|
||||||
import Status from "./../Status.svelte";
|
|
||||||
import Dropdown from "../../common/Dropdown.svelte";
|
import Dropdown from "../../common/Dropdown.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let record;
|
export let file;
|
||||||
export let hasDelete = false;
|
export let hasDelete = false;
|
||||||
export let hasInsert = false;
|
export let hasInsert = false;
|
||||||
|
|
||||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
|
||||||
let cardTitle = previewTitle(channel.schemas, record);
|
|
||||||
let imagePresets = Object.keys(channel.imageFilters);
|
let imagePresets = Object.keys(channel.imageFilters);
|
||||||
|
|
||||||
function remove(e) {
|
function remove(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch("remove", record.id);
|
dispatch("remove_file", file.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(e, preset) {
|
function insert(e, preset) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let html = htmlurl(channel, record, preset)
|
// let html = htmlurl(channel, record, preset);
|
||||||
let url = !preset ? `/${record._file.path}` : `/templates/${preset}/${record._file.path}`;
|
// let url = !preset
|
||||||
dispatch("editor-insert", {
|
// ? `/${record._file.path}`
|
||||||
html: html,
|
// : `/templates/${preset}/${record._file.path}`;
|
||||||
url: channel.filesUrl + url,
|
// dispatch("editor-insert", {
|
||||||
originalUrl: channel.filesUrl + "/" + record._file.path,
|
// html: html,
|
||||||
record: record
|
// url: channel.filesUrl + url,
|
||||||
});
|
// originalUrl: channel.filesUrl + "/" + record._file.path,
|
||||||
|
// record: record,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="preview-file">
|
<div class="preview-file">
|
||||||
<div style="display: flex;align-items: center;gap: 10px;">
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<Preview {record} size="small"/>
|
<Preview {file} size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>
|
<div>
|
||||||
<a
|
{file.filename}
|
||||||
class="record-title"
|
</div>
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="display: flex;gap:4px; align-items: center; margin-right: 10px;"
|
||||||
>
|
>
|
||||||
{cardTitle}
|
|
||||||
</a>
|
|
||||||
<small class="d-block">
|
|
||||||
from {schema.label}
|
|
||||||
{#if record.status === "draft"}
|
|
||||||
<Status status={record.status}/>
|
|
||||||
{/if}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;gap:4px; align-items: center; margin-right: 10px;">
|
|
||||||
{#if hasInsert}
|
{#if hasInsert}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<div slot="button">
|
<div slot="button">
|
||||||
<Icon icon="photo-film" />
|
<Icon icon="photo-film" />
|
||||||
</div>
|
</div>
|
||||||
<button class="dropdown-item button" on:click={e => insert(e,null)}>original</button>
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => insert(e, null)}>original</button
|
||||||
|
>
|
||||||
{#each imagePresets as preset}
|
{#each imagePresets as preset}
|
||||||
<button class="dropdown-item button" on:click={e => insert(e,preset)}>{preset}</button>
|
<button
|
||||||
|
class="dropdown-item button"
|
||||||
|
on:click={(e) => insert(e, preset)}>{preset}</button
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasDelete}
|
{#if hasDelete}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
<button
|
<button class="button" on:click={remove}>
|
||||||
class="button"
|
|
||||||
on:click={remove}
|
|
||||||
>
|
|
||||||
<Icon icon="trash-can" />
|
<Icon icon="trash-can" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import {previewTitle} from "./../Preview";
|
|
||||||
import Status from "./../Status.svelte";
|
import Status from "./../Status.svelte";
|
||||||
import Preview from "../../files/Preview.svelte";
|
import Preview from "../../files/Preview.svelte";
|
||||||
|
|
||||||
@@ -12,10 +11,15 @@
|
|||||||
export let record;
|
export let record;
|
||||||
export let hasDelete = false;
|
export let hasDelete = false;
|
||||||
|
|
||||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
let schema = channel.schemas.find(
|
||||||
let cardTitle = previewTitle(channel.schemas, record, graph);
|
(aschema) => aschema.name === record.schema,
|
||||||
const cardImageEdge = graph.edges.find(e => e.source === record.id && e.field === schema.cardImage);
|
);
|
||||||
let cardImageRecord = graph.records.find(r => r.id === cardImageEdge?.target);
|
const cardImageEdge = graph.edges.find(
|
||||||
|
(e) => e.source === record.id && e.field === schema.cardImage,
|
||||||
|
);
|
||||||
|
let cardImageRecord = graph.records.find(
|
||||||
|
(r) => r.id === cardImageEdge?.target,
|
||||||
|
);
|
||||||
|
|
||||||
function remove(e) {
|
function remove(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -23,10 +27,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="preview-reference">
|
<div class="preview-reference">
|
||||||
<div style="display: flex;align-items: center;gap: 10px;">
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
|
|
||||||
{#if cardImageRecord}
|
{#if cardImageRecord}
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<Preview record={cardImageRecord} size="small" />
|
<Preview record={cardImageRecord} size="small" />
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
class="record-title"
|
class="record-title"
|
||||||
href="{channel.lucentUrl}/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
>
|
>
|
||||||
{cardTitle}
|
{record.data.name}
|
||||||
</a>
|
</a>
|
||||||
<small class="d-block">
|
<small class="d-block">
|
||||||
from {schema.label}
|
from {schema.label}
|
||||||
@@ -46,20 +48,14 @@
|
|||||||
<Status status={record.status} />
|
<Status status={record.status} />
|
||||||
{/if}
|
{/if}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if hasDelete}
|
{#if hasDelete}
|
||||||
<div class="reference-action">
|
<div class="reference-action">
|
||||||
<button
|
<button class="button" on:click={remove}>
|
||||||
class="button"
|
|
||||||
on:click={remove}
|
|
||||||
>
|
|
||||||
<Icon icon="trash-can" />
|
<Icon icon="trash-can" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Preview from "../../files/Preview.svelte";
|
|
||||||
import PreviewCardSmall from "../PreviewCardSmall.svelte";
|
import PreviewCardSmall from "../PreviewCardSmall.svelte";
|
||||||
|
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||||
|
|
||||||
export let field;
|
export let field;
|
||||||
export let side;
|
export let side;
|
||||||
@@ -8,26 +8,23 @@
|
|||||||
export let colorClass;
|
export let colorClass;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if ["reference", "file"].includes(field.info.name)}
|
{#if ["reference"].includes(field.info.name)}
|
||||||
<div class="{colorClass} field-content">
|
<div class="{colorClass} field-content">
|
||||||
<div class="d-flex align-items-center text-center flex-wrap">
|
<div class="d-flex align-items-center text-center flex-wrap">
|
||||||
{#each edges[field.name] as edgeRecord}
|
{#each edges[field.name] as edgeRecord}
|
||||||
{#if edgeRecord._file?.path}
|
|
||||||
<div
|
|
||||||
class="ms-2 "
|
|
||||||
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">
|
<div class="ms-2">
|
||||||
<PreviewCardSmall record={edgeRecord} />
|
<PreviewCardSmall record={edgeRecord} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +40,6 @@
|
|||||||
|
|
||||||
<!-- {/if} -->
|
<!-- {/if} -->
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.field-content {
|
.field-content {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Step from "./Step.svelte"
|
|
||||||
|
|
||||||
export let steps;
|
|
||||||
export let allSuccess = false;
|
|
||||||
|
|
||||||
console.log(steps);
|
|
||||||
</script>
|
|
||||||
<div class="wrapper-tiny">
|
|
||||||
|
|
||||||
{#each steps as step}
|
|
||||||
<Step {step}></Step>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div style="text-align: center;margin-top: 30px;">
|
|
||||||
{#if allSuccess}
|
|
||||||
<a href="/lucent/register" class="bt">Create the first user</a>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Icon from "../common/Icon.svelte"
|
|
||||||
|
|
||||||
export let step;
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="step step-{step.status}">
|
|
||||||
<div class="step-icon">
|
|
||||||
{#if step.status === "success"}
|
|
||||||
<Icon icon="check"></Icon>
|
|
||||||
{:else}
|
|
||||||
<Icon icon="close"></Icon>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div style="width:100%">
|
|
||||||
<h4>{step.name}</h4>
|
|
||||||
<details>
|
|
||||||
<summary>Instuctions</summary>
|
|
||||||
<code class="instructions">{step.instructions}</code>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.step-success .step-icon{
|
|
||||||
background: var(--suc10);
|
|
||||||
color: var(--suc100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-fail .step-icon{
|
|
||||||
background: var(--err10);
|
|
||||||
color: var(--err100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon{
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instructions {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--p10);
|
|
||||||
white-space: break-spaces;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Generated
+290
-2841
File diff suppressed because it is too large
Load Diff
@@ -11,24 +11,15 @@
|
|||||||
"@codemirror/lang-markdown": "^6.2.5",
|
"@codemirror/lang-markdown": "^6.2.5",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
"axios": "^1.7.4",
|
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"htmx.org": "^2.0.1",
|
|
||||||
"install": "^0.13.0",
|
|
||||||
"laravel-vite-plugin": "^1.0.5",
|
"laravel-vite-plugin": "^1.0.5",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"mustache": "^4.2.0",
|
|
||||||
"npm": "^10.8.2",
|
|
||||||
"postcss": "8.4.31",
|
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
"tinymce": "^6.8.4",
|
|
||||||
"trix": "^2.1.5",
|
"trix": "^2.1.5",
|
||||||
"uuid": "^10.0.0",
|
|
||||||
"vite": "5.2.6"
|
"vite": "5.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.autocomplete {
|
.autocomplete {
|
||||||
position: relative;
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
.autocomplete-option {
|
.autocomplete-option {
|
||||||
@@ -16,8 +15,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.autocomplete-selected-value {
|
.autocomplete-selected-value {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -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,37 +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="{{url('lucent/login')}}" >
|
|
||||||
@csrf
|
|
||||||
<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
|
|
||||||
@@ -1,24 +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">Welcome to Lucent</h2>
|
|
||||||
|
|
||||||
<form hx-post="{{url('lucent/verify')}}" hx-redirect="{{url('lucent')}}" hx-target-error=".form-errors" >
|
|
||||||
<input type="hidden" value="{{$email}}" name="email" />
|
|
||||||
<input type="hidden" value="{{$token}}" name="token" />
|
|
||||||
@csrf
|
|
||||||
<x-lucent::button-indicator>
|
|
||||||
Enter as {{$email}}
|
|
||||||
</x-lucent::button-indicator>
|
|
||||||
</form>
|
|
||||||
<div class="form-errors"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@php
|
|
||||||
$side = $side ?? 48;
|
|
||||||
|
|
||||||
$colors = [
|
|
||||||
"#00AA55",
|
|
||||||
"#009FD4",
|
|
||||||
"#B381B3",
|
|
||||||
"#939393",
|
|
||||||
"#E3BC00",
|
|
||||||
"#D47500",
|
|
||||||
"#DC2A2A",
|
|
||||||
"#3ede91",
|
|
||||||
"#377dd4",
|
|
||||||
"#0256b0",
|
|
||||||
"#053d82",
|
|
||||||
"#3d026e",
|
|
||||||
"#b378e3",
|
|
||||||
"#c4065c",
|
|
||||||
"#543208",
|
|
||||||
"#d97811",
|
|
||||||
"#0c6b40",
|
|
||||||
];
|
|
||||||
$initials = function($name){
|
|
||||||
$segs = explode(" ",$name);
|
|
||||||
if(count($segs) > 1){
|
|
||||||
return strtoupper($segs[0][0]).strtoupper($segs[1][0]);
|
|
||||||
}
|
|
||||||
return strtoupper($segs[0][0]).strtoupper($segs[0][1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
$name = $user["name"];
|
|
||||||
$charIndex = ord($name[1]) + strlen($name);
|
|
||||||
$colorIndex = $charIndex % 19;
|
|
||||||
$bgColor = $colors[$colorIndex];
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="avatar"
|
|
||||||
title="{{$name}}"
|
|
||||||
style="background-color:{{$bgColor}};height: {{$side}}px;width: {{$side}}px; font-size:{{$side / 2}}px"
|
|
||||||
>
|
|
||||||
<div class="avatar__letters">{{$initials($user["name"])}}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<button class="bt bt-primary">
|
|
||||||
{{$slot}}
|
|
||||||
<img alt="indicator" id="indicator" class="htmx-indicator" src="/img/spinner.svg"/>
|
|
||||||
</button>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<div class="notice {{$type ?? "info"}}">
|
|
||||||
<div class="title">{{$title}}</div>
|
|
||||||
<div class="content">{{ $slot }}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
@if (count($errors) > 0)
|
|
||||||
<x-lucent::notice type="error" title="🛑 Submission failed">
|
|
||||||
<ul>
|
|
||||||
@foreach ($errors as $error)
|
|
||||||
<li>{{ $error }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</x-lucent::notice>
|
|
||||||
@endif
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
@extends("lucent::layouts.channel")
|
|
||||||
|
|
||||||
@section("content")
|
|
||||||
<h3 class="header-small mb-4">Latest Content changes</h3>
|
|
||||||
|
|
||||||
|
|
||||||
@if($records->isNotEmpty())
|
|
||||||
<div class="lx-card mb-4">
|
|
||||||
<div class="lx-table p-0">
|
|
||||||
<table class="">
|
|
||||||
<tbody>
|
|
||||||
@foreach($records as $record)
|
|
||||||
<tr>
|
|
||||||
@include("lucent::records.card-row")
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<div class="d-flex align-items-center ">
|
|
||||||
|
|
||||||
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
|
|
||||||
|
|
||||||
@if($channel->generateCommand)
|
|
||||||
<a href="{channel.lucentUrl}/build-report" class="btn btn-outline-primary btn-sm d-">Build website</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- <div>-->
|
|
||||||
<!-- <form method="GET">-->
|
|
||||||
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
|
|
||||||
<!-- class="form-control" required/>-->
|
|
||||||
<!-- </form>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="nav-item" href="/lucent/profile">
|
|
||||||
<x-lucent::avatar side="28" :user="$user"></x-lucent::avatar>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
||||||
<title>@yield('title') - Lucent Data Platform</title>
|
|
||||||
<meta name="htmx-config" content='{"selfRequestsOnly": false}' />
|
|
||||||
@if(config("lucent.env") === "production")
|
|
||||||
<!-- if production -->
|
|
||||||
<link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
|
|
||||||
<script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
|
|
||||||
@else
|
|
||||||
<!-- if development -->
|
|
||||||
@php
|
|
||||||
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
|
|
||||||
@endphp
|
|
||||||
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
@yield('content')
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
||||||
<title>@yield('title') - Lucent Data Platform</title>
|
|
||||||
@if(config("lucent.env") == "production")
|
|
||||||
<!-- if production -->
|
|
||||||
<link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
|
|
||||||
<script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
|
|
||||||
@else
|
|
||||||
<!-- if development -->
|
|
||||||
@php
|
|
||||||
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
|
|
||||||
@endphp
|
|
||||||
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
|
|
||||||
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
@yield('content')
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
@php
|
|
||||||
$schema = $schemas->where("name",$record->schema)->first();
|
|
||||||
@endphp
|
|
||||||
<td>
|
|
||||||
@if($schema->type === "files")
|
|
||||||
<Preview {record} size="tiny"/>
|
|
||||||
@else
|
|
||||||
<a
|
|
||||||
href="/lucent/records/{{$record->id}}"
|
|
||||||
class="text-decoration-none text-dark d-block"
|
|
||||||
>
|
|
||||||
{{$viewModel->getRecordName($record, $schemas)}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{$record->status->value === "draft" ? "Draft" : ""}}
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td><a
|
|
||||||
class="text-decoration-none lx-small-text"
|
|
||||||
href="/lucent/content/{{$schema->name}}">{{$schema->label}}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{-- <div class="d-flex">--}}
|
|
||||||
{{-- <Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>--}}
|
|
||||||
{{-- <div class="ms-2">--}}
|
|
||||||
{{-- {frieldlyUpdatedAt}--}}
|
|
||||||
{{-- </div>--}}
|
|
||||||
{{-- </div>--}}
|
|
||||||
</td>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
@php
|
|
||||||
$currentSchema = $currentSchema ?? null;
|
|
||||||
$activeClass = $schema->name === $currentSchema?->name ? "active" : "";
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<a class="sidebar-item {{$activeClass}}" aria-current="page"
|
|
||||||
href="/lucent/content/{{$schema->name}}">{{$schema->label}}</a>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<a class="nav-item" href="/lucent">{{$channel->name}}</a>
|
|
||||||
<div class="sidebar">
|
|
||||||
|
|
||||||
<div class="sidebar-header">
|
|
||||||
Content
|
|
||||||
</div>
|
|
||||||
@foreach($schemas->where("type.value", "collection")->where("isEntry",true) as $schema)
|
|
||||||
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
|
|
||||||
@endforeach
|
|
||||||
<div class="sidebar-header">
|
|
||||||
Files
|
|
||||||
</div>
|
|
||||||
@foreach($schemas->where("type.value", "files") as $schema)
|
|
||||||
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
|
|
||||||
@endforeach
|
|
||||||
<div class="sidebar-header">
|
|
||||||
Other
|
|
||||||
</div>
|
|
||||||
@foreach($schemas->where("type.value", "collection")->where("isEntry",false) as $schema)
|
|
||||||
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@@ -1,7 +1,33 @@
|
|||||||
@extends('lucent::layouts.'.$layout)
|
<!DOCTYPE html>
|
||||||
@section('title')
|
<html lang="en">
|
||||||
{{ $title }}
|
|
||||||
@endsection
|
<head>
|
||||||
@section('content')
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title> {{ $title }} - Lucent Data Platform</title>
|
||||||
|
@if(config("lucent.env") === "production")
|
||||||
|
<!-- if production -->
|
||||||
|
<link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
|
||||||
|
<script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
|
||||||
|
@else
|
||||||
|
<!-- if development -->
|
||||||
|
@php
|
||||||
|
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
|
||||||
|
@endphp
|
||||||
|
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
{!! $svelte !!}
|
{!! $svelte !!}
|
||||||
@endsection
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ use Lucent\Primitive\Collection;
|
|||||||
|
|
||||||
readonly class AccountService
|
readonly class AccountService
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AuthService $authService,
|
private AuthService $authService,
|
||||||
private ChannelService $channelService,
|
private ChannelService $channelService,
|
||||||
private UserRepo $userRepo,
|
private UserRepo $userRepo,
|
||||||
)
|
) {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<User>
|
* @return Collection<User>
|
||||||
@@ -25,13 +21,11 @@ readonly class AccountService
|
|||||||
return $this->userRepo->all();
|
return $this->userRepo->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function countUsers(): int
|
public function countUsers(): int
|
||||||
{
|
{
|
||||||
return $this->userRepo->count();
|
return $this->userRepo->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<UserProfile>
|
* @return Collection<UserProfile>
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +48,4 @@ readonly class AccountService
|
|||||||
$roles = $this->authService->currentUserRoles();
|
$roles = $this->authService->currentUserRoles();
|
||||||
return $this->channelService->schemasWritableByRoles($roles);
|
return $this->channelService->schemasWritableByRoles($roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-198
@@ -2,235 +2,52 @@
|
|||||||
|
|
||||||
namespace Lucent\Account;
|
namespace Lucent\Account;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Contracts\Session\Session;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Lucent\Channel\ChannelService;
|
|
||||||
use Lucent\LucentException;
|
use Lucent\LucentException;
|
||||||
use Lucent\Mail\LoginMail;
|
|
||||||
|
|
||||||
readonly class AuthService
|
interface AuthService
|
||||||
{
|
{
|
||||||
|
public function currentUserId(): ?string;
|
||||||
|
|
||||||
public function __construct(
|
public function currentUserRoles(): array;
|
||||||
private ChannelService $channelService,
|
public function getCurrentUser(): User;
|
||||||
private UserRepo $userRepo,
|
|
||||||
public Session $session,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
public function isLoggedIn(): bool;
|
||||||
|
|
||||||
|
|
||||||
public function currentUserId(): ?string
|
|
||||||
{
|
|
||||||
|
|
||||||
if (app()->runningInConsole()) {
|
|
||||||
return config("lucent.systemUserId");
|
|
||||||
} elseif(request()->segment(1) !== "lucent") {
|
|
||||||
return config("lucent.systemUserId");
|
|
||||||
} else {
|
|
||||||
return $this->session->get("user.id");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function currentUserRoles(): array
|
|
||||||
{
|
|
||||||
return $this->session->get("user.roles") ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function isLoggedIn(): bool
|
|
||||||
{
|
|
||||||
return !empty($this->currentUserId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function login(string $email, string $token): void;
|
||||||
function login(string $email, string $token): void
|
|
||||||
{
|
|
||||||
|
|
||||||
$user = $this->userRepo->findByEmail(new Email($email));
|
public function refreshSession();
|
||||||
|
|
||||||
if ($user->isEmpty()) {
|
public function create(string $name, string $email, array $roles): User;
|
||||||
throw new LucentException("Your account was not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->get()->isRemoved()) {
|
|
||||||
throw new LucentException("Your account is not active");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->get()->mailToken !== $token) {
|
|
||||||
throw new LucentException("Token has expired or is invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Carbon::parse($user->get()->loggedInAt)->lte(Carbon::now()->subHours(1))) {
|
|
||||||
throw new LucentException("Token has expired.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$newUser = $user->get();
|
|
||||||
$newUser->updatedAt = Carbon::now()->toJson();
|
|
||||||
$newUser->mailToken = null;
|
|
||||||
$this->userRepo->update($newUser);
|
|
||||||
$this->session->put(["user" => $user->get()->safe()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function refreshSession()
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
$user = $this->userRepo->findById($this->currentUserId());
|
|
||||||
|
|
||||||
if ($user->isEmpty()) {
|
|
||||||
throw new LucentException("Your account was not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->get()->isRemoved()) {
|
|
||||||
throw new LucentException("Your account is not active");
|
|
||||||
}
|
|
||||||
|
|
||||||
$newUser = $user->get();
|
|
||||||
$this->session->put(["user" => $user->get()->safe()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public
|
|
||||||
function create(string $name, string $email, array $roles): User
|
|
||||||
{
|
|
||||||
$user = new User(
|
|
||||||
id: (string)Str::uuid(),
|
|
||||||
name: new Name($name),
|
|
||||||
email: new Email($email),
|
|
||||||
roles: $this->validateRoles($roles),
|
|
||||||
createdAt: Carbon::now()->toJson(),
|
|
||||||
updatedAt: Carbon::now()->toJson(),
|
|
||||||
loggedInAt: Carbon::now()->toJson(),
|
|
||||||
mailToken: Token::new(32),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->userRepo->insert($user);
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function sendLoginEmail(string $email): void
|
|
||||||
{
|
|
||||||
$emailAddress = (new Email($email));
|
|
||||||
$user = $this->userRepo->findByEmail($emailAddress);
|
|
||||||
|
|
||||||
if ($user->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->get()->isRemoved()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$newToken = $this->userRepo->updateLoginToken($user->get()->id);
|
|
||||||
|
|
||||||
Mail::to($email)->send(
|
|
||||||
new LoginMail(
|
|
||||||
$email,
|
|
||||||
$newToken,
|
|
||||||
$this->channelService->channel->lucentUrl
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public function sendLoginEmail(string $email): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function changeRoles(string $userId, array $roles): void;
|
||||||
function changeRoles(string $userId, array $roles): void
|
|
||||||
{
|
|
||||||
$user = $this->userRepo->findById($userId);
|
|
||||||
|
|
||||||
if ($user->isEmpty()) {
|
|
||||||
throw new LucentException("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$newUser = $user->get();
|
|
||||||
$newUser->roles = $this->validateRoles($roles);
|
|
||||||
$newUser->updatedAt = Carbon::now()->toJson();
|
|
||||||
$this->userRepo->update($newUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function updateName(string $name): void;
|
||||||
function updateName(string $name): void
|
|
||||||
{
|
|
||||||
$name = (new Name($name));
|
|
||||||
$this->userRepo->updateName($this->currentUserId(), $name);
|
|
||||||
$user = $this->userRepo->findById($this->currentUserId());
|
|
||||||
$this->session->put(["user" => $user->get()->safe()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function updateEmail(string $email): void;
|
||||||
function updateEmail(string $email): void
|
|
||||||
{
|
|
||||||
$email = (new Email($email));
|
|
||||||
$user = $this->userRepo->findByEmail($email);
|
|
||||||
if ($user->isDefined()) {
|
|
||||||
throw new LucentException("Email already assigned to user");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->userRepo->updateEmail($this->currentUserId(), $email);
|
|
||||||
$user = $this->userRepo->findById($this->currentUserId());
|
|
||||||
$this->session->put(["user" => $user->get()->safe()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function invite(string $name, string $email, array $roles): User;
|
||||||
function invite(
|
|
||||||
string $name,
|
|
||||||
string $email,
|
|
||||||
array $roles
|
|
||||||
): User
|
|
||||||
{
|
|
||||||
$user = $this->create($name, $email, $roles);
|
|
||||||
$this->sendLoginEmail($user->email);
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LucentException
|
* @throws LucentException
|
||||||
*/
|
*/
|
||||||
public
|
public function registerAdmin(string $name, string $email): User;
|
||||||
function registerAdmin(
|
|
||||||
string $name,
|
|
||||||
string $email
|
|
||||||
): User
|
|
||||||
{
|
|
||||||
$user = $this->invite($name, $email, ["admin"]);
|
|
||||||
$this->sendLoginEmail($user->email);
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function validateRoles(array $roles): array
|
|
||||||
{
|
|
||||||
return collect($roles)
|
|
||||||
->filter(fn(string $role) => in_array($role, $this->channelService->channel->roles))
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
public function validateRoles(array $roles): array;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lucent\Account;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Session\Session;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Lucent\Channel\ChannelService;
|
||||||
|
use Lucent\LucentException;
|
||||||
|
use Lucent\Mail\LoginMail;
|
||||||
|
|
||||||
|
readonly class AuthServiceLucent implements AuthService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ChannelService $channelService,
|
||||||
|
private UserRepo $userRepo,
|
||||||
|
public Session $session,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function currentUserId(): ?string
|
||||||
|
{
|
||||||
|
if (app()->runningInConsole()) {
|
||||||
|
return config("lucent.systemUserId");
|
||||||
|
} elseif (request()->segment(1) !== "lucent") {
|
||||||
|
return config("lucent.systemUserId");
|
||||||
|
}
|
||||||
|
return $this->session->get("user.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentUserRoles(): array
|
||||||
|
{
|
||||||
|
return $this->session->get("user.roles") ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUser(): User
|
||||||
|
{
|
||||||
|
return new User(
|
||||||
|
id: $this->session->get("user.id"),
|
||||||
|
name: new Name($this->session->get("user.name")),
|
||||||
|
email: new Email($this->session->get("user.email")),
|
||||||
|
roles: $this->session->get("user.roles"),
|
||||||
|
createdAt: Carbon::parse($this->session->get("user.createdAt")),
|
||||||
|
updatedAt: Carbon::parse($this->session->get("user.updatedAt")),
|
||||||
|
loggedInAt: null,
|
||||||
|
mailToken: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->currentUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function login(string $email, string $token): void
|
||||||
|
{
|
||||||
|
$user = $this->userRepo->findByEmail(new Email($email));
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
throw new LucentException("Your account was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->get()->isRemoved()) {
|
||||||
|
throw new LucentException("Your account is not active");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->get()->mailToken !== $token) {
|
||||||
|
throw new LucentException("Token has expired or is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Carbon::parse($user->get()->loggedInAt)->lte(
|
||||||
|
Carbon::now()->subHours(1),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new LucentException("Token has expired.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUser = $user->get();
|
||||||
|
$newUser->updatedAt = Carbon::now();
|
||||||
|
$newUser->mailToken = null;
|
||||||
|
$this->userRepo->update($newUser);
|
||||||
|
$this->session->put(["user" => $user->get()->safe()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshSession()
|
||||||
|
{
|
||||||
|
$user = $this->userRepo->findById($this->currentUserId());
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
throw new LucentException("Your account was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->get()->isRemoved()) {
|
||||||
|
throw new LucentException("Your account is not active");
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUser = $user->get();
|
||||||
|
$this->session->put(["user" => $user->get()->safe()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $name, string $email, array $roles): User
|
||||||
|
{
|
||||||
|
$user = new User(
|
||||||
|
id: (string) Str::uuid(),
|
||||||
|
name: new Name($name),
|
||||||
|
email: new Email($email),
|
||||||
|
roles: $this->validateRoles($roles),
|
||||||
|
createdAt: Carbon::now(),
|
||||||
|
updatedAt: Carbon::now(),
|
||||||
|
loggedInAt: Carbon::now(),
|
||||||
|
mailToken: Token::new(32),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->userRepo->insert($user);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendLoginEmail(string $email): void
|
||||||
|
{
|
||||||
|
$emailAddress = new Email($email);
|
||||||
|
$user = $this->userRepo->findByEmail($emailAddress);
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->get()->isRemoved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newToken = $this->userRepo->updateLoginToken($user->get()->id);
|
||||||
|
|
||||||
|
Mail::to($email)->send(
|
||||||
|
new LoginMail(
|
||||||
|
$email,
|
||||||
|
$newToken,
|
||||||
|
$this->channelService->channel->lucentUrl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function changeRoles(string $userId, array $roles): void
|
||||||
|
{
|
||||||
|
$user = $this->userRepo->findById($userId);
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
throw new LucentException("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUser = $user->get();
|
||||||
|
$newUser->roles = $this->validateRoles($roles);
|
||||||
|
$newUser->updatedAt = Carbon::now()->toJson();
|
||||||
|
$this->userRepo->update($newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function updateName(string $name): void
|
||||||
|
{
|
||||||
|
$name = new Name($name);
|
||||||
|
$this->userRepo->updateName($this->currentUserId(), $name);
|
||||||
|
$user = $this->userRepo->findById($this->currentUserId());
|
||||||
|
$this->session->put(["user" => $user->get()->safe()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function updateEmail(string $email): void
|
||||||
|
{
|
||||||
|
$email = new Email($email);
|
||||||
|
$user = $this->userRepo->findByEmail($email);
|
||||||
|
if ($user->isDefined()) {
|
||||||
|
throw new LucentException("Email already assigned to user");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->userRepo->updateEmail($this->currentUserId(), $email);
|
||||||
|
$user = $this->userRepo->findById($this->currentUserId());
|
||||||
|
$this->session->put(["user" => $user->get()->safe()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function invite(string $name, string $email, array $roles): User
|
||||||
|
{
|
||||||
|
$user = $this->create($name, $email, $roles);
|
||||||
|
$this->sendLoginEmail($user->email);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function registerAdmin(string $name, string $email): User
|
||||||
|
{
|
||||||
|
$user = $this->invite($name, $email, ["admin"]);
|
||||||
|
$this->sendLoginEmail($user->email);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateRoles(array $roles): array
|
||||||
|
{
|
||||||
|
return collect($roles)
|
||||||
|
->filter(
|
||||||
|
fn(string $role) => in_array(
|
||||||
|
$role,
|
||||||
|
$this->channelService->channel->roles,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lucent\Account;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Session\Session;
|
||||||
|
use Lucent\Channel\ChannelService;
|
||||||
|
use Lucent\LucentException;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
|
||||||
|
readonly class AuthServiceLunar implements AuthService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ChannelService $channelService,
|
||||||
|
private UserRepo $userRepo,
|
||||||
|
public Session $session,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function currentUserId(): ?string
|
||||||
|
{
|
||||||
|
if (app()->runningInConsole()) {
|
||||||
|
return config("lucent.systemUserId");
|
||||||
|
} elseif (request()->segment(1) !== "lucent") {
|
||||||
|
return config("lucent.systemUserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filament::auth()->user()->id ?? null;
|
||||||
|
}
|
||||||
|
public function getCurrentUser(): User
|
||||||
|
{
|
||||||
|
return new User(
|
||||||
|
id: Filament::auth()->user()->id,
|
||||||
|
name: new Name(
|
||||||
|
Filament::auth()->user()->first_name .
|
||||||
|
" " .
|
||||||
|
Filament::auth()->user()->last_name,
|
||||||
|
),
|
||||||
|
email: new Email(Filament::auth()->user()->email),
|
||||||
|
roles: [],
|
||||||
|
createdAt: Filament::auth()->user()->created_at,
|
||||||
|
updatedAt: Filament::auth()->user()->updated_at,
|
||||||
|
loggedInAt: null,
|
||||||
|
mailToken: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentUserRoles(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->currentUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function login(string $email, string $token): void {}
|
||||||
|
|
||||||
|
public function refreshSession() {}
|
||||||
|
|
||||||
|
public function create(string $name, string $email, array $roles): User {}
|
||||||
|
|
||||||
|
public function sendLoginEmail(string $email): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function changeRoles(string $userId, array $roles): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function updateName(string $name): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function updateEmail(string $email): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function invite(string $name, string $email, array $roles): User {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LucentException
|
||||||
|
*/
|
||||||
|
public function registerAdmin(string $name, string $email): User {}
|
||||||
|
|
||||||
|
public function validateRoles(array $roles): array
|
||||||
|
{
|
||||||
|
return collect($roles)
|
||||||
|
->filter(
|
||||||
|
fn(string $role) => in_array(
|
||||||
|
$role,
|
||||||
|
$this->channelService->channel->roles,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-85
@@ -2,111 +2,37 @@
|
|||||||
|
|
||||||
namespace Lucent\Account;
|
namespace Lucent\Account;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Lucent\Database\Database;
|
|
||||||
use Lucent\Primitive\Collection;
|
use Lucent\Primitive\Collection;
|
||||||
use PhpOption\Option;
|
use PhpOption\Option;
|
||||||
|
|
||||||
class UserRepo
|
interface UserRepo
|
||||||
{
|
{
|
||||||
|
public function count(): int;
|
||||||
public function count(): int
|
|
||||||
{
|
|
||||||
return Database::make()->table("users")->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<User>
|
* @return Collection<User>
|
||||||
*/
|
*/
|
||||||
public function all(): Collection
|
public function all(): Collection;
|
||||||
{
|
|
||||||
$usersData = Database::make()->table("users")->get();
|
|
||||||
|
|
||||||
$users = array_map(fn($userData) => $this->fromArray((array)$userData), $usersData->toArray());
|
public function insert(User $user): void;
|
||||||
return new Collection($users);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public function update(User $user): void;
|
||||||
|
|
||||||
public static function insert(User $user): void
|
public function updateLoginToken(string $id): string;
|
||||||
{
|
|
||||||
$userData = toArray($user);
|
|
||||||
$userData["roles"] = json_encode($userData["roles"]);
|
|
||||||
Database::make()->table("users")->insert($userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(User $user): void
|
|
||||||
{
|
|
||||||
$userData = toArray($user);
|
|
||||||
$userData["roles"] = json_encode($userData["roles"]);
|
|
||||||
Database::make()->table("users")->where("id", $user->id)->update($userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function updateLoginToken(string $id): string
|
|
||||||
{
|
|
||||||
$newToken = Token::new(32);
|
|
||||||
|
|
||||||
Database::make()->table("users")
|
|
||||||
->where("id", $id)
|
|
||||||
->update([
|
|
||||||
'loggedInAt' => Carbon::now()->toJson(),
|
|
||||||
'mailToken' => $newToken,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $newToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Option<User>
|
* @return Option<User>
|
||||||
*/
|
*/
|
||||||
public function findByEmail(Email $email): Option
|
public function findByEmail(Email $email): Option;
|
||||||
{
|
|
||||||
$user = Database::make()->table("users")->where("email", $email->value())->first();
|
|
||||||
|
|
||||||
if (empty($user)) {
|
|
||||||
return none();
|
|
||||||
}
|
|
||||||
|
|
||||||
return some($this->fromArray((array)$user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Option<User>
|
* @return Option<User>
|
||||||
*/
|
*/
|
||||||
public function findById(string $id): Option
|
public function findById(string $id): Option;
|
||||||
{
|
|
||||||
$user = Database::make()->table("users")->where("id", $id)->first();
|
|
||||||
|
|
||||||
if (empty($user)) {
|
public function updateName(string $userId, Name $name): void;
|
||||||
return none();
|
|
||||||
}
|
|
||||||
|
|
||||||
return some($this->fromArray((array)$user));
|
public function updateEmail(string $userId, Email $email): void;
|
||||||
}
|
|
||||||
|
|
||||||
|
public function fromArray(array $data): User;
|
||||||
public function updateName(string $userId, Name $name): void
|
|
||||||
{
|
|
||||||
Database::make()->table("users")->where("id", $userId)->update(["name" => $name->value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateEmail(string $userId, Email $email): void
|
|
||||||
{
|
|
||||||
Database::make()->table("users")->where("id", $userId)->update(["email" => $email->value()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromArray(array $data): User
|
|
||||||
{
|
|
||||||
return new User(
|
|
||||||
id: $data["id"],
|
|
||||||
name: new Name($data["name"] ?? ""),
|
|
||||||
email: new Email($data["email"]),
|
|
||||||
roles: json_decode($data["roles"] ?? "[]", true),
|
|
||||||
createdAt: $data["createdAt"],
|
|
||||||
updatedAt: $data["updatedAt"],
|
|
||||||
loggedInAt: $data["loggedInAt"] ?? null,
|
|
||||||
mailToken: $data["mailToken"] ?? null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lucent\Account;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Lucent\Database\Database;
|
||||||
|
use Lucent\Primitive\Collection;
|
||||||
|
use PhpOption\Option;
|
||||||
|
|
||||||
|
class UserRepoLucent implements UserRepo
|
||||||
|
{
|
||||||
|
private $tableName = "lucent_users";
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return Database::make()->table($this->tableName)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<User>
|
||||||
|
*/
|
||||||
|
public function all(): Collection
|
||||||
|
{
|
||||||
|
$usersData = Database::make()->table($this->tableName)->get();
|
||||||
|
|
||||||
|
$users = array_map(
|
||||||
|
fn($userData) => $this->fromArray((array) $userData),
|
||||||
|
$usersData->toArray(),
|
||||||
|
);
|
||||||
|
return new Collection($users);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(User $user): void
|
||||||
|
{
|
||||||
|
$userData = toArray($user);
|
||||||
|
$userData["roles"] = json_encode($userData["roles"]);
|
||||||
|
Database::make()->table($this->tableName)->insert($userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user): void
|
||||||
|
{
|
||||||
|
$userData = toArray($user);
|
||||||
|
$userData["roles"] = json_encode($userData["roles"]);
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $user->id)
|
||||||
|
->update($userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLoginToken(string $id): string
|
||||||
|
{
|
||||||
|
$newToken = Token::new(32);
|
||||||
|
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $id)
|
||||||
|
->update([
|
||||||
|
"loggedInAt" => Carbon::now()->toJson(),
|
||||||
|
"mailToken" => $newToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Option<User>
|
||||||
|
*/
|
||||||
|
public function findByEmail(Email $email): Option
|
||||||
|
{
|
||||||
|
$user = Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("email", $email->value())
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($user)) {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
|
||||||
|
return some($this->fromArray((array) $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Option<User>
|
||||||
|
*/
|
||||||
|
public function findById(string $id): Option
|
||||||
|
{
|
||||||
|
$user = Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($user)) {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
|
||||||
|
return some($this->fromArray((array) $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateName(string $userId, Name $name): void
|
||||||
|
{
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $userId)
|
||||||
|
->update(["name" => $name->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateEmail(string $userId, Email $email): void
|
||||||
|
{
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $userId)
|
||||||
|
->update(["email" => $email->value()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromArray(array $data): User
|
||||||
|
{
|
||||||
|
return new User(
|
||||||
|
id: $data["id"],
|
||||||
|
name: new Name($data["name"] ?? ""),
|
||||||
|
email: new Email($data["email"]),
|
||||||
|
roles: json_decode($data["roles"] ?? "[]", true),
|
||||||
|
createdAt: $data["createdAt"],
|
||||||
|
updatedAt: $data["updatedAt"],
|
||||||
|
loggedInAt: $data["loggedInAt"] ?? null,
|
||||||
|
mailToken: $data["mailToken"] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Lucent\Account;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Lucent\Database\Database;
|
||||||
|
use Lucent\Primitive\Collection;
|
||||||
|
use PhpOption\Option;
|
||||||
|
|
||||||
|
class UserRepoLunar implements UserRepo
|
||||||
|
{
|
||||||
|
private $tableName = "lunar_staff";
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return Database::make()->table($this->tableName)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<User>
|
||||||
|
*/
|
||||||
|
public function all(): Collection
|
||||||
|
{
|
||||||
|
$usersData = Database::make()->table($this->tableName)->get();
|
||||||
|
|
||||||
|
$users = array_map(
|
||||||
|
fn($userData) => $this->fromArray((array) $userData),
|
||||||
|
$usersData->toArray(),
|
||||||
|
);
|
||||||
|
return new Collection($users);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(User $user): void
|
||||||
|
{
|
||||||
|
$userData = toArray($user);
|
||||||
|
$userData["roles"] = json_encode($userData["roles"]);
|
||||||
|
Database::make()->table($this->tableName)->insert($userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user): void
|
||||||
|
{
|
||||||
|
$userData = toArray($user);
|
||||||
|
$userData["roles"] = json_encode($userData["roles"]);
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $user->id)
|
||||||
|
->update($userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLoginToken(string $id): string
|
||||||
|
{
|
||||||
|
$newToken = Token::new(32);
|
||||||
|
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $id)
|
||||||
|
->update([
|
||||||
|
"loggedInAt" => Carbon::now()->toJson(),
|
||||||
|
"mailToken" => $newToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Option<User>
|
||||||
|
*/
|
||||||
|
public function findByEmail(Email $email): Option
|
||||||
|
{
|
||||||
|
$user = Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("email", $email->value())
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($user)) {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
|
||||||
|
return some($this->fromArray((array) $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Option<User>
|
||||||
|
*/
|
||||||
|
public function findById(string $id): Option
|
||||||
|
{
|
||||||
|
$user = Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($user)) {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
|
||||||
|
return some($this->fromArray((array) $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateName(string $userId, Name $name): void
|
||||||
|
{
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $userId)
|
||||||
|
->update(["name" => $name->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateEmail(string $userId, Email $email): void
|
||||||
|
{
|
||||||
|
Database::make()
|
||||||
|
->table($this->tableName)
|
||||||
|
->where("id", $userId)
|
||||||
|
->update(["email" => $email->value()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromArray(array $data): User
|
||||||
|
{
|
||||||
|
return new User(
|
||||||
|
id: $data["id"],
|
||||||
|
name: new Name(
|
||||||
|
$data["first_name"] . " " . $data["last_name"] ?? "",
|
||||||
|
),
|
||||||
|
email: new Email($data["email"]),
|
||||||
|
roles: [],
|
||||||
|
createdAt: $data["created_at"],
|
||||||
|
updatedAt: $data["updated_at"],
|
||||||
|
loggedInAt: null,
|
||||||
|
mailToken: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user