diff --git a/front/js/common/Icon.svelte b/front/js/common/Icon.svelte
index 66000bc..991ae02 100644
--- a/front/js/common/Icon.svelte
+++ b/front/js/common/Icon.svelte
@@ -56,23 +56,23 @@
path: ' ',
viewBox: "0 0 576 512",
},
- "filter": {
+ filter: {
path: ' ',
viewBox: "0 0 512 512",
},
- "calendar": {
+ calendar: {
path: ' ',
viewBox: "0 0 448 512",
},
- "pencil": {
+ pencil: {
path: ' ',
viewBox: "0 0 512 512",
},
- "database": {
+ database: {
path: ' ',
viewBox: "0 0 448 512",
},
- "dice": {
+ dice: {
path: ' ',
viewBox: "0 0 640 512",
},
@@ -81,7 +81,7 @@
path: ' ',
viewBox: "0 0 512 512",
},
- "eye": {
+ eye: {
path: ' ',
viewBox: "0 0 576 512",
},
@@ -93,19 +93,19 @@
path: ' ',
viewBox: "0 0 512 512",
},
- "expand": {
+ expand: {
path: ' ',
viewBox: "0 0 448 512",
},
- "compress": {
+ compress: {
path: ' ',
viewBox: "0 0 448 512",
},
- "check": {
+ check: {
path: ' ',
viewBox: "0 0 448 512",
},
- "close": {
+ close: {
path: ' ',
viewBox: "0 0 24 24",
},
@@ -113,7 +113,7 @@
path: ' ',
viewBox: "0 0 24 24",
},
- "list": {
+ list: {
path: ' ',
viewBox: "0 0 24 24",
},
@@ -121,33 +121,33 @@
path: ' ',
viewBox: "0 0 24 24",
},
- "italic": {
+ italic: {
path: ' ',
viewBox: "0 0 24 24",
- }
+ },
};
-
- export let width = 16;
- export let height = 16;
- export let icon = "";
- export let fill = "currentColor";
- export let stroke = "currentColor";
+ let {
+ width = 16,
+ height = 16,
+ icon,
+ fill = "currentColor",
+ stroke = "currentColor",
+ } = $props();
let selectedIcon = icons[icon];
{@html selectedIcon.path}
@@ -155,6 +155,5 @@
diff --git a/front/js/entry/RecordEditEntry/PublishingOptions.svelte b/front/js/entry/RecordEditEntry/PublishingOptions.svelte
new file mode 100644
index 0000000..43a4ae9
--- /dev/null
+++ b/front/js/entry/RecordEditEntry/PublishingOptions.svelte
@@ -0,0 +1,60 @@
+
+
+
+ {#if status != "trashed"}
+
Publish Now
+ {/if}
+
+
+
+
+
+
+ View Revisions
+ {#if status != "trashed"}
+
+ Move to trash
+
+ {/if}
+
+
+
diff --git a/front/js/entry/RecordEditEntry/RecordEditEntry.svelte b/front/js/entry/RecordEditEntry/RecordEditEntry.svelte
index 4358872..03cc821 100644
--- a/front/js/entry/RecordEditEntry/RecordEditEntry.svelte
+++ b/front/js/entry/RecordEditEntry/RecordEditEntry.svelte
@@ -3,6 +3,8 @@
import { onMount } from "svelte";
import TextField from "./fields/TextField.svelte";
import LocaleChooser from "./LocaleChooser.svelte";
+ import RecordStatus from "./RecordStatus.svelte";
+ import PublishingOptions from "./PublishingOptions.svelte";
import { getSelectedLocales } from "./locale.svelte.js";
let { channel, user, data } = $props();
let selectedLocales = $state(getSelectedLocales());
@@ -16,42 +18,52 @@
{#snippet body()}
-
- {#each data.fields as field}
-
- {#if field.type === "text"}
-
f.id === field.fieldId && f.locale === "main",
- )}
- schemaField={field}
- locale="main"
- dataField={data.draftData.find(
- (f) => f.id === field.id && f.locale === "main",
- )}
- >
- {#if field.translatable}
- {#each selectedLocales as locale (locale)}
-
- f.fieldId === field.id &&
- f.locale === locale,
- )}
- schemaField={field}
- {locale}
- dataField={data.draftData.find(
- (f) => f.id === field.id && f.locale === locale,
- )}
- >
- {/each}
+
+
+
+ {#each data.fields as field}
+
+ {#if field.type === "text"}
+
+ f.fieldId === field.id && f.locale === "main",
+ )}
+ schemaField={field}
+ locale="main"
+ dataField={data.draftData.find(
+ (f) => f.id === field.id && f.locale === "main",
+ )}
+ >
+ {#if field.translatable}
+ {#each selectedLocales as locale (locale)}
+
+ f.fieldId === field.id &&
+ f.locale === locale,
+ )}
+ schemaField={field}
+ {locale}
+ dataField={data.draftData.find(
+ (f) =>
+ f.id === field.id &&
+ f.locale === locale,
+ )}
+ >
+ {/each}
+ {/if}
{/if}
- {/if}
-
- {/each}
+
+ {/each}
+
{/snippet}
diff --git a/front/js/entry/RecordEditEntry/RecordStatus.svelte b/front/js/entry/RecordEditEntry/RecordStatus.svelte
new file mode 100644
index 0000000..b12e1f0
--- /dev/null
+++ b/front/js/entry/RecordEditEntry/RecordStatus.svelte
@@ -0,0 +1,84 @@
+
+
+
+ {#if status === "trashed"}
+
This record is Trashed
+
Restore
+ {:else if status === "scheduled_and_published"}
+
+
+
+ This record was published at {record.publishedAt}
+
+ Unpublish
+
+
+
+ It is scheduled to be republished at {record.scheduledAt}
+
+ Unschedule
+
+
+ {:else if status === "published"}
+
+ This record was published at {record.publishedAt}
+
+
Cancel
+ {:else if status === "scheduled"}
+
+ This record is scheduled to be published at {record.scheduledAt}
+
+
Cancel
+ {:else}
+
This record is a draft. Not yet published
+ {/if}
+
+
+
diff --git a/front/js/entry/RecordEditEntry/fields/TextField.svelte b/front/js/entry/RecordEditEntry/fields/TextField.svelte
index 54e84ea..f5512d9 100644
--- a/front/js/entry/RecordEditEntry/fields/TextField.svelte
+++ b/front/js/entry/RecordEditEntry/fields/TextField.svelte
@@ -2,13 +2,14 @@
import { post } from "../../../modules/remote";
import { getApp } from "../../../app";
import { getLocaleName } from "../locale.svelte.js";
- let { channel, record, schemaField, dataField, locale, errors } = $props();
+ let { channel, record, schemaField, dataField, locale, validationError } =
+ $props();
let originalValue = dataField?.value ?? schemaField.props.default;
let newValue = $state(originalValue);
let valuesChanged = $derived(newValue !== originalValue);
let errorMessage = $state("");
- let validationErrors = $state(errors);
- let hasErrors = $derived(validationErrors.length > 0);
+ // let validationErrorState = $state(validationError);
+ let hasError = $derived(!!validationError);
const app = getApp();
function save() {
@@ -28,7 +29,7 @@
if (err.isNotEmpty()) {
errorMessage = err.first();
} else {
- validationErrors = data.validationErrors;
+ validationError = data.validationError;
originalValue = newValue;
}
},
@@ -119,11 +120,11 @@
onblur={handleBlur}
oncompositionstart={handleCompositionStart}
oncompositionend={handleCompositionEnd}
- aria-invalid={hasErrors ? "true" : ""}
+ aria-invalid={hasError ? "true" : ""}
/>
- {#if hasErrors}
+ {#if hasError}
{validationErrors[0].message} {validationError.message}
{:else if schemaField.help != ""}
{schemaField.help}
diff --git a/src/Core/Data/Record.php b/src/Core/Data/Record.php
index 0ccff81..f1b9911 100644
--- a/src/Core/Data/Record.php
+++ b/src/Core/Data/Record.php
@@ -4,10 +4,6 @@ use Carbon\Carbon;
class Record
{
- /**
- * @param RecordField[] $draftData
- * @param RecordField[] $liveData
- */
public function __construct(
public string $id,
public string $schemaId,
@@ -15,6 +11,8 @@ class Record
public string $createdBy,
public ?Carbon $publishedAt,
public ?string $publishedBy,
+ public ?Carbon $scheduledAt,
+ public ?string $scheduledBy,
public ?Carbon $trashedAt,
public ?string $trashedBy,
) {}
diff --git a/src/Core/Data/RecordStatus.php b/src/Core/Data/RecordStatus.php
new file mode 100644
index 0000000..1abbebc
--- /dev/null
+++ b/src/Core/Data/RecordStatus.php
@@ -0,0 +1,10 @@
+where("mode", RecordMode::DRAFT)
+ ->values()
+ ->map(function (RecordField $f) {
+ $f->mode = RecordMode::LIVE;
+ return $f;
+ })
+ ->toArray();
+
+ DB::transaction(function () use ($recordField) {
+ RecordFieldRepo::deleteLiveByRecordId($recordId);
+ RecordFieldRepo::insertMany($liveData);
+ });
+ }
+
+ public static function unpublish(string $recordId): void
+ {
+ RecordFieldRepo::deleteLiveByRecordId($recordId);
+ }
+
public static function fromDb(stdClass $field): RecordField
{
return new RecordField(
diff --git a/src/Core/Record/RecordModule.php b/src/Core/Record/RecordModule.php
index a32b55a..7647f48 100644
--- a/src/Core/Record/RecordModule.php
+++ b/src/Core/Record/RecordModule.php
@@ -3,22 +3,23 @@
use Carbon\Carbon;
use Lucent\Core\Data\Record;
use Lucent\Core\Data\RecordField;
+use Lucent\Core\Data\RecordStatus;
use stdClass;
class RecordModule
{
- public static function updateField(
- Record $record,
- RecordField $field,
- ): Record {
- $recordFields = collect($record->draftData)->filter(
- fn(RecordField $rf) => !(
- $rf->id == $field->id && $rf->locale == $field->locale
- ),
- );
- $recordFields->push($field);
- $record->draftData = $recordFields->values()->toArray();
- return $record;
+ public static function getStatus(Record $record): RecordStatus
+ {
+ if ($record->trashedBy) {
+ return RecordStatus::TRASHED;
+ } elseif ($record->publishedBy && $record->scheduledAt) {
+ return RecordStatus::SCHEDULED_AND_PUBLISHED;
+ } elseif ($record->publishedBy) {
+ return RecordStatus::PUBLISHED;
+ } elseif ($record->scheduledAt) {
+ return RecordStatus::SCHEDULED;
+ }
+ return RecordStatus::DRAFT;
}
public static function toDb(Record $record): array
@@ -30,11 +31,15 @@ class RecordModule
"created_by" => $record->createdBy,
"published_at" => empty($record->publishedAt)
? null
- : record->publishedAt->toJSON(),
+ : $record->publishedAt->toJSON(),
"published_by" => $record->publishedBy,
+ "scheduled_at" => empty($record->scheduledAt)
+ ? null
+ : $record->scheduledAt->toJSON(),
+ "scheduled_by" => $record->scheduledBy,
"trashed_at" => empty($record->trashedAt)
? null
- : record->trashedAt->toJSON(),
+ : $record->trashedAt->toJSON(),
"trashed_by" => $record->trashedBy,
];
}
@@ -50,6 +55,10 @@ class RecordModule
? null
: Carbon::parse(data_get($data, "published_at")),
publishedBy: data_get($data, "published_by"),
+ scheduledAt: empty(data_get($data, "scheduled_at"))
+ ? null
+ : Carbon::parse(data_get($data, "scheduled_at")),
+ scheduledBy: data_get($data, "scheduled_by"),
trashedAt: empty(data_get($data, "trashed_at"))
? null
: Carbon::parse(data_get($data, "trashed_at")),
diff --git a/src/Core/Record/RecordValidationModule.php b/src/Core/Record/RecordValidationModule.php
index a5376ac..1fe74f2 100644
--- a/src/Core/Record/RecordValidationModule.php
+++ b/src/Core/Record/RecordValidationModule.php
@@ -1,5 +1,6 @@
translatable) {
foreach ($locales as $locale) {
$res = static::validateField(
@@ -31,7 +32,7 @@ class RecordValidationModule
$schemaField,
$recordFields,
);
- $errors = array_merge($errors, $res);
+ $errors[] = $res;
}
}
}
@@ -52,15 +53,27 @@ class RecordValidationModule
string $locale,
Field $schemaField,
array $recordFields,
- ): array {
+ ): ?RecordError {
// General Validations
$error = static::validateRequired($locale, $schemaField, $recordFields);
+ if (!empty($error)) {
+ return $error;
+ }
// Type specific
+ $error = match ($schemaField->type) {
+ "text" => static::validateText(
+ $locale,
+ $schemaField,
+ $recordFields,
+ ),
+ default => static::validateText(
+ $locale,
+ $schemaField,
+ $recordFields,
+ ),
+ };
- return collect([$error])
- ->filter(fn($err) => $err !== null)
- ->values()
- ->toArray();
+ return $error;
}
/**
@@ -77,11 +90,7 @@ class RecordValidationModule
if ($schemaField->required === false) {
return null;
}
- $recordField = collect($recordFields)->first(
- fn(RecordField $field) => $field->id === $schemaField->id &&
- $field->locale === $locale &&
- $field->mode === RecordMode::DRAFT,
- );
+ $recordField = static::findField($recordFields, $schemaField, $locale);
if (empty($recordField) || empty($recordField->value)) {
return new RecordError(
@@ -93,4 +102,64 @@ class RecordValidationModule
return null;
}
+
+ /**
+ *
+ * @param Field $schemaField
+ * @param RecordField[] $recordFields
+ * @return ?RecordError
+ */
+ public static function validateText(
+ string $locale,
+ Field $schemaField,
+ array $recordFields,
+ ): ?RecordError {
+ $recordField = static::findField($recordFields, $schemaField, $locale);
+
+ if (empty($recordField)) {
+ // We have already checked for required fields so only validate existing
+ return null;
+ }
+
+ $rules = "";
+ if ($schemaField->props->min > 0) {
+ $rules = "min:" . $schemaField->props->min;
+ }
+ if ($schemaField->props->max > 0) {
+ $rules = "max:" . $schemaField->props->max;
+ }
+
+ if (empty($rules)) {
+ return null;
+ }
+
+ $validator = Validator::make(
+ ["value" => $recordField->value],
+ [
+ "value" => $rules,
+ ],
+ );
+
+ if ($validator->fails()) {
+ return new RecordError(
+ $schemaField->id,
+ $locale,
+ $validator->errors()->first(),
+ );
+ }
+
+ return null;
+ }
+
+ private static function findField(
+ array $recordFields,
+ Field $schemaField,
+ string $locale,
+ ): ?RecordField {
+ return collect($recordFields)->first(
+ fn(RecordField $field) => $field->id === $schemaField->id &&
+ $field->locale === $locale &&
+ $field->mode === RecordMode::DRAFT,
+ );
+ }
}
diff --git a/src/Core/Repository/RecordFieldRepo.php b/src/Core/Repository/RecordFieldRepo.php
index c9deddf..f237668 100644
--- a/src/Core/Repository/RecordFieldRepo.php
+++ b/src/Core/Repository/RecordFieldRepo.php
@@ -2,6 +2,7 @@
use Illuminate\Support\Facades\DB;
use Lucent\Core\Data\RecordField;
+use Lucent\Core\Data\RecordMode;
use Lucent\Core\Record\RecordFieldModule;
class RecordFieldRepo
@@ -15,6 +16,21 @@ class RecordFieldRepo
);
}
+ public static function insertMany(array $recordFields): void
+ {
+ DB::table(self::TABLE_NAME)->insert(
+ array_map(RecordFieldModule::toDb(...), $recordFields),
+ );
+ }
+
+ public static function deleteLiveByRecordId(string $recordId): void
+ {
+ DB::table(self::TABLE_NAME)
+ ->where("record_id", $recordId)
+ ->where("mode", RecordMode::LIVE->value)
+ ->delete();
+ }
+
public static function delete(RecordField $field): void
{
DB::table(self::TABLE_NAME)
diff --git a/src/Core/Repository/RecordRepo.php b/src/Core/Repository/RecordRepo.php
index 1f97fbe..1778ce0 100644
--- a/src/Core/Repository/RecordRepo.php
+++ b/src/Core/Repository/RecordRepo.php
@@ -13,6 +13,13 @@ class RecordRepo
DB::table(self::TABLE_NAME)->insert(RecordModule::toDb($record));
}
+ public static function update(Record $record): void
+ {
+ DB::table(self::TABLE_NAME)
+ ->where("id", $record->id)
+ ->update(RecordModule::toDb($record));
+ }
+
public static function findOne(string $id): ?Record
{
return DB::table(self::TABLE_NAME)
diff --git a/src/Http/Controller/RecordController.php b/src/Http/Controller/RecordController.php
index 5a5dc90..5db01e5 100644
--- a/src/Http/Controller/RecordController.php
+++ b/src/Http/Controller/RecordController.php
@@ -11,6 +11,7 @@ use Lucent\Core\Data\RecordField;
use Lucent\Core\Repository\FieldRepo;
use Lucent\Core\Repository\SchemaRepo;
use Lucent\Core\Repository\RecordFieldRepo;
+use Lucent\Core\Record\RecordModule;
use Lucent\Id\Id;
use Lucent\LucentException;
use Lucent\Query\Operator\OperatorRegistry;
@@ -28,6 +29,7 @@ use Lucent\Schema\Validator\ValidatorException;
use Lucent\Svelte\Svelte;
use Lucent\ViewModel\ViewModel;
use Lucent\Core\Data\Record;
+use Lucent\Core\Data\RecordStatus;
use Lucent\Core\Data\RecordMode;
use Illuminate\Support\Facades\DB;
use function Lucent\Response\fail;
@@ -265,6 +267,8 @@ class RecordController
$draftData,
);
+ $recordStatus = RecordModule::getStatus($record);
+
return Svelte::view(
view: "recordEdit",
title: "Edit Record",
@@ -276,6 +280,7 @@ class RecordController
"record" => toArray($record),
"draftData" => $draftData,
"validationErrors" => $validationErrors,
+ "recordStatus" => $recordStatus,
],
);
}
@@ -325,6 +330,8 @@ class RecordController
createdBy: $userId,
publishedAt: null,
publishedBy: null,
+ scheduledAt: null,
+ scheduledBy: null,
trashedAt: null,
trashedBy: null,
);
@@ -371,7 +378,7 @@ class RecordController
RecordFieldRepo::insert($recordField);
});
- $validationErrors = RecordValidationModule::validateField(
+ $validationError = RecordValidationModule::validateField(
$locale,
$field,
[$recordField],
@@ -379,7 +386,7 @@ class RecordController
return ok([
"recordField" => toArray($recordField),
- "validationErrors" => $validationErrors,
+ "validationError" => $validationError,
]);
}
@@ -487,4 +494,100 @@ class RecordController
}
return ok();
}
+
+ public function publish(Request $request)
+ {
+ $id = $request->input("id");
+ $record = RecordRepo::findOne($id);
+ $recordStatus = RecordModule::getStatus($record);
+ if ($recordStatus == RecordStatus::TRASHED) {
+ return fail("Record is in trash");
+ }
+ $userId = AuthModule::getCurrentUserId();
+ $record->publishedAt = now();
+ $record->publishedBy = $userId;
+ $record->scheduledAt = null;
+ $record->scheduledBy = null;
+
+ RecordFieldModule::publish($record->id);
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
+
+ public function unpublish(Request $request)
+ {
+ $id = $request->input("id");
+ $record = RecordRepo::findOne($id);
+
+ $record->publishedAt = null;
+ $record->publishedBy = null;
+
+ RecordFieldModule::unpublish($record->id);
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
+
+ public function schedule(Request $request)
+ {
+ $id = $request->input("id");
+ $date = $request->input("date");
+ $record = RecordRepo::findOne($id);
+ $recordStatus = RecordModule::getStatus($record);
+ $userId = AuthModule::getCurrentUserId();
+ if ($recordStatus == RecordStatus::TRASHED) {
+ return fail("Record is in trash");
+ }
+ $record->scheduledAt = Carbon::parse($date);
+ $record->scheduledBy = $userId;
+
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
+
+ public function unschedule(Request $request)
+ {
+ $id = $request->input("id");
+ $record = RecordRepo::findOne($id);
+
+ $record->scheduledAt = null;
+ $record->scheduledBy = null;
+
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
+
+ public function trash(Request $request)
+ {
+ $id = $request->input("id");
+ $record = RecordRepo::findOne($id);
+
+ $userId = AuthModule::getCurrentUserId();
+ $record->trashedAt = now();
+ $record->trashedBy = $userId;
+ $record->publishedAt = null;
+ $record->publishedBy = null;
+ $record->scheduledAt = null;
+ $record->scheduledBy = null;
+
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
+
+ public function untrash(Request $request)
+ {
+ $id = $request->input("id");
+ $record = RecordRepo::findOne($id);
+
+ $record->trashedAt = null;
+ $record->trashedBy = null;
+
+ RecordRepo::update($record);
+
+ return ok(toArray($record));
+ }
}
diff --git a/src/Http/web.php b/src/Http/web.php
index 4ce87de..8453a01 100644
--- a/src/Http/web.php
+++ b/src/Http/web.php
@@ -76,6 +76,27 @@ Route::group(
RecordController::class,
"saveField",
]);
+ Route::post("records/publish", [
+ RecordController::class,
+ "publish",
+ ]);
+ Route::post("records/unpublish", [
+ RecordController::class,
+ "unpublish",
+ ]);
+ Route::post("records/schedule", [
+ RecordController::class,
+ "schedule",
+ ]);
+ Route::post("records/unschedule", [
+ RecordController::class,
+ "unschedule",
+ ]);
+ Route::post("records/trash", [RecordController::class, "trash"]);
+ Route::post("records/untrash", [
+ RecordController::class,
+ "untrash",
+ ]);
});
//OLD