updates
This commit is contained in:
@@ -259,14 +259,14 @@ class Conductor
|
|||||||
|
|
||||||
// Transform and Includes
|
// Transform and Includes
|
||||||
$includes = $conductor->includes;
|
$includes = $conductor->includes;
|
||||||
if(count($limitFields) > 0) {
|
if (count($limitFields) > 0) {
|
||||||
$includes = array_intersect($limitFields, $conductor->includes);
|
$includes = array_intersect($limitFields, $conductor->includes);
|
||||||
}
|
}
|
||||||
|
|
||||||
$conductor->collection = $conductor->collection->map(function ($model) use ($conductor, $includes, $limitFields) {
|
$conductor->collection = $conductor->collection->map(function ($model) use ($conductor, $includes, $limitFields) {
|
||||||
$conductor->applyIncludes($model, $includes);
|
$conductor->applyIncludes($model, $includes);
|
||||||
|
|
||||||
if(count($limitFields) > 0) {
|
if (count($limitFields) > 0) {
|
||||||
$model->setAppends(array_intersect($model->getAppends(), $limitFields));
|
$model->setAppends(array_intersect($model->getAppends(), $limitFields));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,10 +316,10 @@ class Conductor
|
|||||||
$requestFields = $request->input('fields');
|
$requestFields = $request->input('fields');
|
||||||
if ($requestFields !== null) {
|
if ($requestFields !== null) {
|
||||||
$requestFields = explode(',', $requestFields);
|
$requestFields = explode(',', $requestFields);
|
||||||
if(in_array($key, $requestFields) === false) {
|
if (in_array($key, $requestFields) === false) {
|
||||||
foreach($requestFields as $field) {
|
foreach ($requestFields as $field) {
|
||||||
if(strpos($field, $key . '.') === 0) {
|
if (strpos($field, $key . '.') === 0) {
|
||||||
$fields[] = substr($field, strlen($key) + 1);
|
$fields[] = substr($field, (strlen($key) + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,8 +332,8 @@ class Conductor
|
|||||||
/**
|
/**
|
||||||
* Run the conductor on a Model with the data stored in a Request.
|
* Run the conductor on a Model with the data stored in a Request.
|
||||||
*
|
*
|
||||||
* @param mixed $fields The fields to show.
|
* @param mixed $fields The fields to show.
|
||||||
* @param Model|null $model The model.
|
* @param Model|null $model The model.
|
||||||
* @return array The processed and transformed model data.
|
* @return array The processed and transformed model data.
|
||||||
*/
|
*/
|
||||||
final public static function model(mixed $fields, mixed $model)
|
final public static function model(mixed $fields, mixed $model)
|
||||||
@@ -349,21 +349,21 @@ class Conductor
|
|||||||
|
|
||||||
// Limit fields
|
// Limit fields
|
||||||
$limitFields = $modelFields;
|
$limitFields = $modelFields;
|
||||||
if($fields instanceof Request) {
|
if ($fields instanceof Request) {
|
||||||
if ($fields !== null && $fields->has('fields') === true) {
|
if ($fields !== null && $fields->has('fields') === true) {
|
||||||
$requestFields = $fields->input('fields');
|
$requestFields = $fields->input('fields');
|
||||||
if ($requestFields !== null) {
|
if ($requestFields !== null) {
|
||||||
$limitFields = array_intersect(explode(',', $requestFields), $modelFields);
|
$limitFields = array_intersect(explode(',', $requestFields), $modelFields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(is_array($fields) && count($fields) > 0) {
|
} elseif (is_array($fields) && count($fields) > 0) {
|
||||||
$limitFields = array_intersect($fields, $modelFields);
|
$limitFields = array_intersect($fields, $modelFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($limitFields) === false) {
|
if (empty($limitFields) === false) {
|
||||||
$modelAppends = $model->getAppends();
|
$modelAppends = $model->getAppends();
|
||||||
|
|
||||||
foreach(array_diff($modelFields, $limitFields) as $attribute) {
|
foreach (array_diff($modelFields, $limitFields) as $attribute) {
|
||||||
$key = array_search($attribute, $modelAppends);
|
$key = array_search($attribute, $modelAppends);
|
||||||
if ($key !== false) {
|
if ($key !== false) {
|
||||||
unset($modelAppends[$key]);
|
unset($modelAppends[$key]);
|
||||||
@@ -476,8 +476,8 @@ class Conductor
|
|||||||
/**
|
/**
|
||||||
* Paginate the conductor collection.
|
* Paginate the conductor collection.
|
||||||
*
|
*
|
||||||
* @param integer $page The current page to return.
|
* @param integer $page The current page to return.
|
||||||
* @param integer $limit The limit of items to include or use default.
|
* @param integer $limit The limit of items to include or use default.
|
||||||
* @param integer $offset Offset the page count after this count of rows.
|
* @param integer $offset Offset the page count after this count of rows.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -687,8 +687,8 @@ class Conductor
|
|||||||
/**
|
/**
|
||||||
* Return an array of model fields visible to the current user.
|
* Return an array of model fields visible to the current user.
|
||||||
*
|
*
|
||||||
* @param Model $model The model in question.
|
* @param Model $model The model in question.
|
||||||
* @param bool $includes Include the includes in the result.
|
* @param boolean $includes Include the includes in the result.
|
||||||
* @return array The array of field names.
|
* @return array The array of field names.
|
||||||
*/
|
*/
|
||||||
public function fields(Model $model)
|
public function fields(Model $model)
|
||||||
@@ -727,7 +727,7 @@ class Conductor
|
|||||||
$result[$key] = $this->$transformFunction($value);
|
$result[$key] = $this->$transformFunction($value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->transformFinal($result);
|
$result = $this->transformFinal($result);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@@ -743,7 +743,7 @@ class Conductor
|
|||||||
$result = $model->toArray();
|
$result = $model->toArray();
|
||||||
|
|
||||||
$fields = $this->fields($model);
|
$fields = $this->fields($model);
|
||||||
|
|
||||||
if (is_array($fields) === true) {
|
if (is_array($fields) === true) {
|
||||||
$result = array_intersect_key($result, array_flip($fields));
|
$result = array_intersect_key($result, array_flip($fields));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Conductors;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Foundation\Auth\User;
|
||||||
|
|
||||||
class MediaConductor extends Conductor
|
class MediaConductor extends Conductor
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,13 @@ class MediaConductor extends Conductor
|
|||||||
*/
|
*/
|
||||||
protected $sort = 'created_at';
|
protected $sort = 'created_at';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The included fields
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $includes = ['user'];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array of model fields visible to the current user.
|
* Return an array of model fields visible to the current user.
|
||||||
@@ -106,4 +114,27 @@ class MediaConductor extends Conductor
|
|||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
|
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the final model data
|
||||||
|
*
|
||||||
|
* @param array $data The model data to transform.
|
||||||
|
* @return array The transformed model.
|
||||||
|
*/
|
||||||
|
public function transformFinal(array $data)
|
||||||
|
{
|
||||||
|
unset($data['user_id']);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include User Field.
|
||||||
|
*
|
||||||
|
* @param Model $model Them model.
|
||||||
|
* @return mixed The model result.
|
||||||
|
*/
|
||||||
|
public function includeUser(Model $model)
|
||||||
|
{
|
||||||
|
return UserConductor::includeModel(request(), 'user', User::find($model['user_id']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ class PostConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The included fields
|
* The included fields
|
||||||
*
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $includes = ['attachments', 'user'];
|
protected $includes = ['attachments', 'user'];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a scope query on the collection before anything else.
|
* Run a scope query on the collection before anything else.
|
||||||
*
|
*
|
||||||
@@ -105,7 +106,7 @@ class PostConductor extends Conductor
|
|||||||
/**
|
/**
|
||||||
* Transform the final model data
|
* Transform the final model data
|
||||||
*
|
*
|
||||||
* @param Array $data The model data to transform.
|
* @param array $data The model data to transform.
|
||||||
* @return array The transformed model.
|
* @return array The transformed model.
|
||||||
*/
|
*/
|
||||||
public function transformFinal(array $data)
|
public function transformFinal(array $data)
|
||||||
@@ -116,7 +117,7 @@ class PostConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Include Attachments Field.
|
* Include Attachments Field.
|
||||||
*
|
*
|
||||||
* @param Model $model Them model.
|
* @param Model $model Them model.
|
||||||
* @return mixed The model result.
|
* @return mixed The model result.
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +130,7 @@ class PostConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Include User Field.
|
* Include User Field.
|
||||||
*
|
*
|
||||||
* @param Model $model Them model.
|
* @param Model $model Them model.
|
||||||
* @return mixed The model result.
|
* @return mixed The model result.
|
||||||
*/
|
*/
|
||||||
@@ -140,7 +141,7 @@ class PostConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the Hero field.
|
* Transform the Hero field.
|
||||||
*
|
*
|
||||||
* @param mixed $value The current value.
|
* @param mixed $value The current value.
|
||||||
* @return array The new value.
|
* @return array The new value.
|
||||||
*/
|
*/
|
||||||
|
|||||||
50
app/Console/Commands/MigrateUploads.php
Normal file
50
app/Console/Commands/MigrateUploads.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\StoreUploadedFileJob;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\Media;
|
||||||
|
use File;
|
||||||
|
|
||||||
|
class MigrateUploads extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'uploads:migrate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Migrate the uploads folder to the CDN';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$files = File::allFiles(public_path('uploads'));
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = pathinfo($file, PATHINFO_BASENAME);
|
||||||
|
|
||||||
|
// echo $filename . "\n";
|
||||||
|
$medium = Media::where('name', $filename)->first();
|
||||||
|
|
||||||
|
if ($medium !== null) {
|
||||||
|
$medium->update(['status' => 'Processing media']);
|
||||||
|
StoreUploadedFileJob::dispatch($medium, $file)->onQueue('media');
|
||||||
|
} else {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,6 +137,7 @@ class ApiController extends Controller
|
|||||||
protected function respondAsResource(
|
protected function respondAsResource(
|
||||||
mixed $data,
|
mixed $data,
|
||||||
array $options = [],
|
array $options = [],
|
||||||
|
$validationFn = null
|
||||||
) {
|
) {
|
||||||
$isCollection = $options['isCollection'] ?? false;
|
$isCollection = $options['isCollection'] ?? false;
|
||||||
$appendData = $options['appendData'] ?? null;
|
$appendData = $options['appendData'] ?? null;
|
||||||
@@ -144,7 +145,14 @@ class ApiController extends Controller
|
|||||||
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
|
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
|
||||||
|
|
||||||
if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
|
if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
|
||||||
return $this->respondNotFound();
|
$validationData = [];
|
||||||
|
if (array_key_exists('appendData', $options) === true) {
|
||||||
|
$validationData = $options['appendData'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($validationFn === null || $validationFn($validationData) === true) {
|
||||||
|
return $this->respondNotFound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($resourceName) === true || empty($resourceName) === true) {
|
if (is_null($resourceName) === true || empty($resourceName) === true) {
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ class MediaController extends ApiController
|
|||||||
$collection,
|
$collection,
|
||||||
['isCollection' => true,
|
['isCollection' => true,
|
||||||
'appendData' => ['total' => $total]
|
'appendData' => ['total' => $total]
|
||||||
]
|
],
|
||||||
|
function ($options) {
|
||||||
|
return $options['total'] === 0;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
|
DB::table('media')->whereNull('mime')->update(['mime' => '']);
|
||||||
|
DB::table('media')->whereNull('permission')->update(['permission' => '']);
|
||||||
|
|
||||||
Schema::table('media', function (Blueprint $table) {
|
Schema::table('media', function (Blueprint $table) {
|
||||||
$table->string('storage')->default("cdn");
|
$table->string('storage')->default("cdn");
|
||||||
$table->string('description')->default("");
|
$table->string('description')->default("");
|
||||||
@@ -21,18 +24,11 @@ return new class extends Migration
|
|||||||
$table->string('dimensions')->default("");
|
$table->string('dimensions')->default("");
|
||||||
$table->text('variants');
|
$table->text('variants');
|
||||||
|
|
||||||
// Update null 'mime' values to empty strings
|
|
||||||
DB::table('media')->whereNull('mime')->update(['mime' => '']);
|
|
||||||
|
|
||||||
// // Update null 'permission' values to empty strings
|
|
||||||
DB::table('media')->whereNull('permission')->update(['permission' => '']);
|
|
||||||
|
|
||||||
$table->bigInteger('size')->default(0)->change();
|
$table->bigInteger('size')->default(0)->change();
|
||||||
$table->string('permission')->default("")->nullable(false)->change();
|
$table->string('permission')->default("")->nullable(false)->change();
|
||||||
|
|
||||||
$table->string('mime')->default("")->nullable(false)->change();
|
$table->string('mime')->default("")->nullable(false)->change();
|
||||||
// $table->renameColumn('mime', 'mime_type');
|
$table->renameColumn('mime', 'mime_type');
|
||||||
$table->string('mime_type');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<td class="attachment-file-icon">
|
<td class="attachment-file-icon">
|
||||||
<img
|
<img
|
||||||
:src="getFileIconImagePath(file.name || file.title)"
|
:src="getFileIconImagePath(file.name || file.title)"
|
||||||
height="48"
|
height="40"
|
||||||
width="48" />
|
width="40" />
|
||||||
</td>
|
</td>
|
||||||
<td class="attachment-file-name">
|
<td class="attachment-file-name">
|
||||||
<a :href="file.url">{{ file.title || file.name }}</a>
|
<a :href="file.url">{{ file.title || file.name }}</a>
|
||||||
@@ -57,15 +57,16 @@ const props = defineProps({
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.attachment-list {
|
.attachment-list {
|
||||||
|
border: 1px solid $secondary-color;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 580px;
|
// max-width: 580px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
background-color: var(--base-color-light);
|
||||||
|
|
||||||
.attachment-row {
|
.attachment-row {
|
||||||
td {
|
td {
|
||||||
border-bottom: 1px solid $secondary-background-color;
|
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,8 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.attachment-file-icon {
|
.attachment-file-icon {
|
||||||
width: 64px;
|
width: 56px;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -82,9 +84,18 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.attachment-file-name {
|
.attachment-file-name {
|
||||||
|
font-size: 80%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-download {
|
.attachment-download {
|
||||||
@@ -109,11 +120,12 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.attachment-file-size {
|
.attachment-file-size {
|
||||||
width: 64px;
|
width: 80px;
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
color: $secondary-color-dark;
|
color: $secondary-color-dark;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ import SMLoadingIcon from "./SMLoadingIcon.vue";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: { type: String, default: "Button", required: false },
|
label: { type: String, default: "Button", required: false },
|
||||||
type: { type: String, default: "secondary", required: false },
|
type: { type: String, default: "", required: false },
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
@@ -193,6 +193,7 @@ const handleClickItem = (item: string) => {
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
.button-label {
|
.button-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -254,6 +255,10 @@ const handleClickItem = (item: string) => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(.button-dropdown) {
|
||||||
|
filter: brightness(115%);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:disabled {
|
&:hover:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@@ -285,5 +290,9 @@ const handleClickItem = (item: string) => {
|
|||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: var(--base-color);
|
color: var(--base-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
188
resources/js/components/SMCheckbox.vue
Normal file
188
resources/js/components/SMCheckbox.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<SMControl class="control-type-checkbox">
|
||||||
|
<div :class="['control-item', { disabled: disabled }]">
|
||||||
|
<label class="control-label" v-bind="{ for: id }"
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox-control"
|
||||||
|
:checked="props.modelValue"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@input="handleInput" />
|
||||||
|
<span class="checkbox-control-box">
|
||||||
|
<span class="checkbox-control-tick"></span>
|
||||||
|
</span>
|
||||||
|
{{ label }}</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{{ id }}
|
||||||
|
<template v-if="slots.help" #help><slot name="help"></slot></template>
|
||||||
|
</SMControl>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, watch, ref, useSlots } from "vue";
|
||||||
|
import { isEmpty } from "../helpers/utils";
|
||||||
|
import { toTitleCase } from "../helpers/string";
|
||||||
|
import SMControl from "./SMControl.vue";
|
||||||
|
|
||||||
|
const emits = defineEmits(["update:modelValue", "blur"]);
|
||||||
|
const props = defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const form = inject("form", props.form);
|
||||||
|
const control =
|
||||||
|
typeof props.control === "object"
|
||||||
|
? props.control
|
||||||
|
: !isEmpty(form) &&
|
||||||
|
typeof props.control === "string" &&
|
||||||
|
props.control !== "" &&
|
||||||
|
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||||
|
? form.controls[props.control]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const label = ref(
|
||||||
|
props.label != undefined
|
||||||
|
? props.label
|
||||||
|
: typeof props.control == "string"
|
||||||
|
? toTitleCase(props.control)
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
const value = ref(
|
||||||
|
props.modelValue != undefined
|
||||||
|
? props.modelValue
|
||||||
|
: control != null
|
||||||
|
? control.value
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
const id = ref(
|
||||||
|
props.id != undefined
|
||||||
|
? props.id
|
||||||
|
: typeof props.control == "string" && props.control.length > 0
|
||||||
|
? props.control
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const disabled = ref(props.disabled);
|
||||||
|
|
||||||
|
if (typeof control === "object" && control !== null) {
|
||||||
|
watch(
|
||||||
|
() => control.value,
|
||||||
|
(newValue) => {
|
||||||
|
value.value = newValue;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
watch(
|
||||||
|
() => form.loading(),
|
||||||
|
(newValue) => {
|
||||||
|
disabled.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = async () => {
|
||||||
|
emits("blur");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
value.value = target.checked;
|
||||||
|
emits("update:modelValue", target.checked);
|
||||||
|
|
||||||
|
if (control) {
|
||||||
|
control.value = target.checked;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.control-group.control-type-checkbox {
|
||||||
|
.control-row {
|
||||||
|
.control-item {
|
||||||
|
.control-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
padding-left: 32px;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.checkbox-control {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
&:checked + .checkbox-control-box {
|
||||||
|
.checkbox-control-tick {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-control-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid var(--base-color-darker);
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--base-color-light);
|
||||||
|
|
||||||
|
.checkbox-control-tick {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
border-right: 3px solid var(--base-color-text);
|
||||||
|
border-bottom: 3px solid var(--base-color-text);
|
||||||
|
top: 1px;
|
||||||
|
left: 7px;
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled label {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
80
resources/js/components/SMControl.vue
Normal file
80
resources/js/components/SMControl.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['control-group', { 'control-invalid': invalid.length > 0 }]">
|
||||||
|
<div class="control-row">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="slots.help || invalid" class="control-help">
|
||||||
|
<span v-if="invalid" class="control-feedback">
|
||||||
|
{{ invalid }}
|
||||||
|
</span>
|
||||||
|
<span v-if="slots.help"><slot name="help"></slot></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch, ref, useSlots } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
invalid: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const invalid = ref(props.invalid);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.invalid,
|
||||||
|
(newValue) => {
|
||||||
|
invalid.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.control-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
// position: absolute;
|
||||||
|
// display: block;
|
||||||
|
// transform-origin: top left;
|
||||||
|
// transform: translate(16px, 16px) scale(1);
|
||||||
|
// transition: all 0.1s ease-in-out;
|
||||||
|
// color: var(--base-color-darker);
|
||||||
|
// pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-help {
|
||||||
|
display: block;
|
||||||
|
font-size: 70%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.control-feedback {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
span + span:before {
|
||||||
|
content: "-";
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -110,5 +110,12 @@ export function openDialog<C extends Component>(
|
|||||||
wrapper,
|
wrapper,
|
||||||
resolve,
|
resolve,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
const autofocusElement = document.querySelector("[autofocus]");
|
||||||
|
if (autofocusElement) {
|
||||||
|
autofocusElement.focus();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,8 +609,6 @@ const imageBrowser = (callback, value, meta, gallery = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(selected);
|
|
||||||
|
|
||||||
const rightBtn = document.createElement("div");
|
const rightBtn = document.createElement("div");
|
||||||
rightBtn.classList.add("image-gallery-content-item-right");
|
rightBtn.classList.add("image-gallery-content-item-right");
|
||||||
rightBtn.onclick = function () {
|
rightBtn.onclick = function () {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ const slots = useSlots();
|
|||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.body .row {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,55 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<SMControl
|
||||||
:class="[
|
:class="['control-type-input', { 'input-active': active }]"
|
||||||
'input-control-group',
|
:invalid="feedbackInvalid">
|
||||||
{ 'input-active': active, 'input-invalid': feedbackInvalid },
|
<div v-if="slots.prepend" class="input-control-prepend">
|
||||||
]">
|
<slot name="prepend"></slot>
|
||||||
<div class="input-control-row">
|
|
||||||
<div v-if="slots.prepend" class="input-control-prepend">
|
|
||||||
<slot name="prepend"></slot>
|
|
||||||
</div>
|
|
||||||
<div class="input-control-item">
|
|
||||||
<label class="input-label" v-bind="{ for: id }">{{
|
|
||||||
label
|
|
||||||
}}</label>
|
|
||||||
<ion-icon
|
|
||||||
class="invalid-icon"
|
|
||||||
name="alert-circle-outline"></ion-icon>
|
|
||||||
<ion-icon
|
|
||||||
v-if="
|
|
||||||
props.showClear && value?.length > 0 && !feedbackInvalid
|
|
||||||
"
|
|
||||||
class="clear-icon"
|
|
||||||
name="close-outline"
|
|
||||||
@click.stop="handleClear"></ion-icon>
|
|
||||||
<input
|
|
||||||
:type="props.type"
|
|
||||||
class="input-control"
|
|
||||||
:disabled="disabled"
|
|
||||||
v-bind="{ id: id, autofocus: props.autofocus }"
|
|
||||||
v-model="value"
|
|
||||||
@focus="handleFocus"
|
|
||||||
@blur="handleBlur"
|
|
||||||
@input="handleInput"
|
|
||||||
@keyup="handleKeyup" />
|
|
||||||
</div>
|
|
||||||
<div v-if="slots.append" class="input-control-append">
|
|
||||||
<slot name="append"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slots.default || feedbackInvalid" class="input-help">
|
<div class="control-item">
|
||||||
<span v-if="feedbackInvalid" class="input-invalid">
|
<label class="control-label" v-bind="{ for: id }">{{
|
||||||
{{ feedbackInvalid }}
|
label
|
||||||
</span>
|
}}</label>
|
||||||
<span v-if="slots.default"><slot></slot></span>
|
<ion-icon
|
||||||
|
class="invalid-icon"
|
||||||
|
name="alert-circle-outline"></ion-icon>
|
||||||
|
<ion-icon
|
||||||
|
v-if="props.showClear && value?.length > 0 && !feedbackInvalid"
|
||||||
|
class="clear-icon"
|
||||||
|
name="close-outline"
|
||||||
|
@click.stop="handleClear"></ion-icon>
|
||||||
|
<input
|
||||||
|
:type="props.type"
|
||||||
|
class="input-control"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||||
|
v-model="value"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="slots.append" class="input-control-append">
|
||||||
|
<slot name="append"></slot>
|
||||||
|
</div>
|
||||||
|
<template v-if="slots.help" #help><slot name="help"></slot></template>
|
||||||
|
</SMControl>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, watch, ref, useSlots } from "vue";
|
import { inject, watch, ref, useSlots } from "vue";
|
||||||
import { isEmpty } from "../helpers/utils";
|
import { isEmpty } from "../helpers/utils";
|
||||||
import { toTitleCase } from "../helpers/string";
|
import { toTitleCase } from "../helpers/string";
|
||||||
|
import SMControl from "./SMControl.vue";
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -225,19 +215,16 @@ const handleKeyup = (event: Event) => {
|
|||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
value.value = "";
|
value.value = "";
|
||||||
emits("update:modelValue", "");
|
emits("update:modelValue", "");
|
||||||
emits("change");
|
// emits("change");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.input-control-group {
|
.control-group.control-type-input {
|
||||||
margin-bottom: 16px;
|
.control-row {
|
||||||
width: 100%;
|
.control-item {
|
||||||
|
align-items: start;
|
||||||
.input-control-row {
|
}
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.input-control-prepend {
|
.input-control-prepend {
|
||||||
p {
|
p {
|
||||||
@@ -258,7 +245,7 @@ const handleClear = () => {
|
|||||||
border-radius: 8px 0 0 8px;
|
border-radius: 8px 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& + .input-control-item .input-control {
|
& + .control-item .input-control {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
@@ -284,18 +271,15 @@ const handleClear = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.input-control-item + .input-control-append)
|
&:has(.control-item + .input-control-append)
|
||||||
> .input-control-item
|
> .control-item
|
||||||
.input-control {
|
.input-control {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-control-item {
|
.control-item {
|
||||||
flex: 1;
|
.control-label {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.input-label {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
@@ -345,29 +329,14 @@ const handleClear = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-help {
|
|
||||||
display: block;
|
|
||||||
font-size: 70%;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.input-invalid {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
span + span:before {
|
|
||||||
content: "-";
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.input-active {
|
&.input-active {
|
||||||
.input-control-item .input-label {
|
.control-item .control-label {
|
||||||
transform: translate(16px, 6px) scale(0.7);
|
transform: translate(16px, 6px) scale(0.7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.input-invalid {
|
&.control-invalid {
|
||||||
.input-control-row .input-control-item {
|
.control-row .control-item {
|
||||||
.invalid-icon {
|
.invalid-icon {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<div
|
<div
|
||||||
v-if="props.modelValue > 1"
|
|
||||||
:class="[
|
:class="[
|
||||||
'item',
|
'item',
|
||||||
'previous',
|
'previous',
|
||||||
@@ -9,17 +8,16 @@
|
|||||||
]"
|
]"
|
||||||
@click="handleClickPrev">
|
@click="handleClickPrev">
|
||||||
<ion-icon name="chevron-back-outline" />
|
<ion-icon name="chevron-back-outline" />
|
||||||
Previous
|
Prev
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="['item', { active: page == props.modelValue }]"
|
:class="['item', 'page', { active: page == props.modelValue }]"
|
||||||
v-for="(page, idx) of computedPages"
|
v-for="(page, idx) of computedPages"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
@click="handleClickPage(page)">
|
@click="handleClickPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(props.modelValue + 3) * props.perPage <= props.total"
|
|
||||||
:class="['item', 'next', { disabled: computedDisableNextButton }]"
|
:class="['item', 'next', { disabled: computedDisableNextButton }]"
|
||||||
@click="handleClickNext">
|
@click="handleClickNext">
|
||||||
Next
|
Next
|
||||||
@@ -29,6 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { unwatchFile } from "fs";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -54,22 +53,27 @@ const emits = defineEmits(["update:modelValue"]);
|
|||||||
const computedPages = computed(() => {
|
const computedPages = computed(() => {
|
||||||
let pages = [];
|
let pages = [];
|
||||||
|
|
||||||
if (props.modelValue - 2 > 0) {
|
let pagesRemaining =
|
||||||
pages.push(props.modelValue - 2);
|
Math.ceil(props.total / props.perPage) - props.modelValue;
|
||||||
|
let pagesBefore = Math.max(0, props.modelValue - 1);
|
||||||
|
|
||||||
|
if (pagesRemaining + pagesBefore > 4) {
|
||||||
|
if (pagesRemaining < 2) {
|
||||||
|
pagesBefore = Math.min(pagesBefore, 4 - pagesRemaining);
|
||||||
|
} else if (pagesBefore < 2) {
|
||||||
|
pagesRemaining = Math.min(pagesRemaining, 4 - pagesBefore);
|
||||||
|
} else {
|
||||||
|
pagesRemaining = 2;
|
||||||
|
pagesBefore = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.modelValue - 1 > 0) {
|
for (; pagesBefore > 0; pagesBefore--) {
|
||||||
pages.push(props.modelValue - 1);
|
pages.push(props.modelValue - pagesBefore);
|
||||||
}
|
}
|
||||||
|
|
||||||
pages.push(props.modelValue);
|
pages.push(props.modelValue);
|
||||||
|
for (let i = 1; i <= pagesRemaining; i++) {
|
||||||
if (props.perPage * (props.modelValue + 1) <= props.total) {
|
pages.push(props.modelValue + i);
|
||||||
pages.push(props.modelValue + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.perPage * (props.modelValue + 2) <= props.total) {
|
|
||||||
pages.push(props.modelValue + 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
@@ -100,14 +104,18 @@ const computedDisableNextButton = computed(() => {
|
|||||||
* Handle click on previous button
|
* Handle click on previous button
|
||||||
*/
|
*/
|
||||||
const handleClickPrev = (): void => {
|
const handleClickPrev = (): void => {
|
||||||
emits("update:modelValue", props.modelValue - 1);
|
if (computedDisablePrevButton.value == false) {
|
||||||
|
emits("update:modelValue", props.modelValue - 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle click on next button
|
* Handle click on next button
|
||||||
*/
|
*/
|
||||||
const handleClickNext = (): void => {
|
const handleClickNext = (): void => {
|
||||||
emits("update:modelValue", props.modelValue + 1);
|
if (computedDisableNextButton.value == false) {
|
||||||
|
emits("update:modelValue", props.modelValue + 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +126,15 @@ const handleClickNext = (): void => {
|
|||||||
const handleClickPage = (page: number): void => {
|
const handleClickPage = (page: number): void => {
|
||||||
emits("update:modelValue", page);
|
emits("update:modelValue", page);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (props.modelValue < 1) {
|
||||||
|
emits("update:modelValue", 1);
|
||||||
|
} else {
|
||||||
|
const totalPages = computedTotalPages.value;
|
||||||
|
if (totalPages < props.modelValue) {
|
||||||
|
emits("update:modelValue", totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -138,6 +155,11 @@ const handleClickPage = (page: number): void => {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&.page {
|
||||||
|
width: 44px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
@@ -158,9 +180,15 @@ const handleClickPage = (page: number): void => {
|
|||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.active) {
|
&:hover:not(.active):not(.disabled) {
|
||||||
background-color: var(--primary-color-hover);
|
background-color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--base-color-darker);
|
||||||
|
background-color: var(--base-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ defineProps({
|
|||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 0 auto;
|
margin: 8px auto;
|
||||||
align-items: top;
|
align-items: top;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|||||||
@@ -22,7 +22,13 @@
|
|||||||
v-bind="item as any">
|
v-bind="item as any">
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>{{ item[header["value"]] }}</template>
|
<template v-else>
|
||||||
|
{{
|
||||||
|
header["value"]
|
||||||
|
.split(".")
|
||||||
|
.reduce((item, key) => item[key], item)
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -69,6 +75,7 @@ const handleRowClick = (item) => {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMFormCard :loading="dialogLoading">
|
<SMForm :model-value="form" @submit="handleSubmit">
|
||||||
<h1>Change Password</h1>
|
<SMFormCard :loading="dialogLoading">
|
||||||
<p class="text-center">Enter your new password below</p>
|
<template #header>
|
||||||
<SMForm :model-value="form" @submit="handleSubmit">
|
<h3>Change Password</h3>
|
||||||
<SMInput control="password" type="password" label="New Password" />
|
<p>Enter your new password below</p>
|
||||||
<SMFormFooter>
|
</template>
|
||||||
<template #left>
|
<template #body>
|
||||||
<SMButton
|
<SMInput
|
||||||
type="secondary"
|
control="password"
|
||||||
label="Cancel"
|
type="password"
|
||||||
@click="handleClickCancel" />
|
label="New Password"
|
||||||
</template>
|
autofocus />
|
||||||
<template #right>
|
</template>
|
||||||
<SMButton type="submit" label="Update" />
|
<template #footer-space-between>
|
||||||
</template>
|
<SMButton label="Cancel" @click="handleClickCancel" />
|
||||||
</SMFormFooter>
|
<SMButton type="submit" label="Update" />
|
||||||
</SMForm>
|
</template>
|
||||||
</SMFormCard>
|
</SMFormCard>
|
||||||
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
export const urlStripAttributes = (url: string): string => {
|
export const urlStripAttributes = (url: string): string => {
|
||||||
const urlObject = new URL(url);
|
const urlObject = new URL(url);
|
||||||
urlObject.search = "";
|
urlObject.search = "";
|
||||||
@@ -73,3 +75,23 @@ export const urlMatches = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRouterParams = (router: Router, params: Params): void => {
|
||||||
|
const query = { ...router.currentRoute.value.query };
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value === "") {
|
||||||
|
if (key in params) {
|
||||||
|
delete query[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push({ query });
|
||||||
|
};
|
||||||
|
|||||||
@@ -456,7 +456,10 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (to.meta.middleware == "authenticated" && !userStore.id) {
|
if (to.meta.middleware == "authenticated" && !userStore.id) {
|
||||||
next({ name: "login", query: { redirect: to.fullPath } });
|
next({
|
||||||
|
name: "login",
|
||||||
|
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,19 @@ export interface UserState {
|
|||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: "user",
|
id: "user",
|
||||||
state: (): UserState => ({
|
state: (): UserState => {
|
||||||
id: "",
|
return {
|
||||||
token: "",
|
id: "",
|
||||||
username: "",
|
token: "",
|
||||||
firstName: "",
|
username: "",
|
||||||
lastName: "",
|
firstName: "",
|
||||||
displayName: "",
|
lastName: "",
|
||||||
email: "",
|
displayName: "",
|
||||||
phone: "",
|
email: "",
|
||||||
permissions: [],
|
phone: "",
|
||||||
}),
|
permissions: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async setUserDetails(user: UserDetails) {
|
async setUserDetails(user: UserDetails) {
|
||||||
@@ -63,9 +65,6 @@ export const useUserStore = defineStore({
|
|||||||
this.$state.email = null;
|
this.$state.email = null;
|
||||||
this.$state.phone = null;
|
this.$state.phone = null;
|
||||||
this.$state.permissions = [];
|
this.$state.permissions = [];
|
||||||
|
|
||||||
this.$reset();
|
|
||||||
localStorage.removeItem(this.$id);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ import SMHero from "../components/SMHero.vue";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 896px) {
|
@media only screen and (max-width: 1000px) {
|
||||||
.page-home {
|
.page-home {
|
||||||
.support {
|
.support {
|
||||||
img {
|
img {
|
||||||
@@ -245,7 +245,7 @@ import SMHero from "../components/SMHero.vue";
|
|||||||
}
|
}
|
||||||
.button-row {
|
.button-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 15px;
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const handleSubmit = async () => {
|
|||||||
? redirectQuery[0]
|
? redirectQuery[0]
|
||||||
: redirectQuery;
|
: redirectQuery;
|
||||||
|
|
||||||
router.push({ path: redirect });
|
router.push(decodeURIComponent(redirect));
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: "dashboard" });
|
router.push({ name: "dashboard" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
:to="{ name: 'dashboard-post-list' }"
|
:to="{ name: 'dashboard-post-list' }"
|
||||||
class="admin-card posts">
|
class="admin-card posts">
|
||||||
<ion-icon name="newspaper-outline" />
|
<ion-icon name="newspaper-outline" />
|
||||||
<h3>Posts</h3>
|
<h3>Articles</h3>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/users')"
|
v-if="userStore.permissions.includes('admin/users')"
|
||||||
|
|||||||
@@ -3,180 +3,137 @@
|
|||||||
title="Media"
|
title="Media"
|
||||||
:back-link="{ name: 'dashboard' }"
|
:back-link="{ name: 'dashboard' }"
|
||||||
back-title="Return to Dashboard" />
|
back-title="Return to Dashboard" />
|
||||||
<SMMessage
|
<SMContainer class="flex-grow-1">
|
||||||
v-if="formMessage.message"
|
|
||||||
:type="formMessage.type"
|
|
||||||
:message="formMessage.message"
|
|
||||||
:icon="formMessage.icon" />
|
|
||||||
<SMContainer>
|
|
||||||
<SMToolbar>
|
<SMToolbar>
|
||||||
<template #left>
|
<SMButton
|
||||||
<SMButton
|
:to="{ name: 'workshops' }"
|
||||||
:to="{ name: 'workshops' }"
|
type="primary"
|
||||||
type="primary"
|
label="Upload Media" />
|
||||||
label="Upload Media" />
|
<SMInput
|
||||||
</template>
|
v-model="search"
|
||||||
<template #right>
|
label="Search"
|
||||||
<SMInput
|
style="max-width: 350px"
|
||||||
v-model="search"
|
@keyup.enter="handleClickSearch">
|
||||||
label="Search"
|
<template #append>
|
||||||
:small="true"
|
<SMButton
|
||||||
style="max-width: 350px"
|
type="primary"
|
||||||
:show-clear="true">
|
label="Search"
|
||||||
<template #append>
|
icon="search-outline"
|
||||||
<SMButton
|
@click="handleClickSearch" />
|
||||||
type="primary"
|
</template>
|
||||||
label="Search"
|
</SMInput>
|
||||||
icon="search-outline"
|
|
||||||
@click="handleClickSearch" />
|
|
||||||
</template>
|
|
||||||
</SMInput>
|
|
||||||
</template>
|
|
||||||
</SMToolbar>
|
</SMToolbar>
|
||||||
|
<SMLoading large v-if="pageLoading" />
|
||||||
<SMPagination
|
<template v-else>
|
||||||
:model-value="pageValue"
|
<SMPagination
|
||||||
:total="total"
|
v-if="items.length < totalFound"
|
||||||
:per-page="perPage" />
|
v-model="page"
|
||||||
<SMTable :headers="headers" :items="items" @row-click="handleRowClick">
|
:total="totalFound"
|
||||||
<template #item-size="item">
|
:per-page="perPage" />
|
||||||
{{ bytesReadable(item.size) }}
|
<SMNoItems v-if="items.length == 0" text="No Media Found" />
|
||||||
</template>
|
<SMTable
|
||||||
<template #item-actions="item">
|
v-else
|
||||||
<SMButton
|
:headers="headers"
|
||||||
label="Edit"
|
:items="items"
|
||||||
:dropdown="{
|
@row-click="handleEdit">
|
||||||
download: 'Download',
|
<template #item-size="item">
|
||||||
delete: 'Delete',
|
{{ bytesReadable(item.size) }}
|
||||||
}"
|
</template>
|
||||||
size="medium"
|
<template #item-actions="item">
|
||||||
@click="handleClick(item, $event)"></SMButton>
|
<SMButton
|
||||||
</template>
|
label="Edit"
|
||||||
</SMTable>
|
:dropdown="{
|
||||||
|
download: 'Download',
|
||||||
|
delete: 'Delete',
|
||||||
|
}"
|
||||||
|
size="medium"
|
||||||
|
@click="handleClick(item, $event)"></SMButton>
|
||||||
|
</template>
|
||||||
|
</SMTable>
|
||||||
|
</template>
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
|
|
||||||
<!-- @click-row="handleClickRow" -->
|
|
||||||
<!-- <EasyDataTable
|
|
||||||
v-model:server-options="serverOptions"
|
|
||||||
:server-items-length="serverItemsLength"
|
|
||||||
:loading="formLoading"
|
|
||||||
:headers="headers"
|
|
||||||
:items="items"
|
|
||||||
:search-value="search">
|
|
||||||
<template #loading>
|
|
||||||
<SMLoadingIcon />
|
|
||||||
</template>
|
|
||||||
<template #item-size="item">
|
|
||||||
{{ bytesReadable(item.size) }}
|
|
||||||
</template>
|
|
||||||
<template #item-actions="item">
|
|
||||||
<div class="action-wrapper">
|
|
||||||
<SMButton
|
|
||||||
label="Edit"
|
|
||||||
:dropdown="{
|
|
||||||
download: 'Download',
|
|
||||||
delete: 'Delete',
|
|
||||||
}"
|
|
||||||
size="medium"
|
|
||||||
@click="handleClick(item, $event)"></SMButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</EasyDataTable> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import EasyDataTable from "vue3-easy-data-table";
|
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
import SMButton from "../../components/SMButton.vue";
|
import SMButton from "../../components/SMButton.vue";
|
||||||
import SMFileLink from "../../components/SMFileLink.vue";
|
|
||||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
|
||||||
import SMToolbar from "../../components/SMToolbar.vue";
|
import SMToolbar from "../../components/SMToolbar.vue";
|
||||||
import { api } from "../../helpers/api";
|
import { api } from "../../helpers/api";
|
||||||
import { Media, UserResponse } from "../../helpers/api.types";
|
import { Media } from "../../helpers/api.types";
|
||||||
import { SMDate } from "../../helpers/datetime";
|
import { SMDate } from "../../helpers/datetime";
|
||||||
import { debounce } from "../../helpers/debounce";
|
|
||||||
import { bytesReadable } from "../../helpers/types";
|
import { bytesReadable } from "../../helpers/types";
|
||||||
import { useUserStore } from "../../store/UserStore";
|
|
||||||
import { useToastStore } from "../../store/ToastStore";
|
import { useToastStore } from "../../store/ToastStore";
|
||||||
import SMInput from "../../components/SMInput.vue";
|
import SMInput from "../../components/SMInput.vue";
|
||||||
import SMMastHead from "../../components/SMMastHead.vue";
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
import SMTable from "../../components/SMTable.vue";
|
import SMTable from "../../components/SMTable.vue";
|
||||||
import SMPagination from "../../components/SMPagination.vue";
|
import SMPagination from "../../components/SMPagination.vue";
|
||||||
|
import SMNoItems from "../../components/SMNoItems.vue";
|
||||||
|
import SMLoading from "../../components/SMLoading.vue";
|
||||||
|
import { updateRouterParams } from "../../helpers/url";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = ref("");
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
|
const pageLoading = ref(true);
|
||||||
|
const search = ref(route.query.search || "");
|
||||||
|
const items = ref([]);
|
||||||
|
const totalFound = ref(0);
|
||||||
|
const perPage = 25;
|
||||||
|
const page = ref(parseInt((route.query.page as string) || "1"));
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{ text: "Name", value: "title", sortable: true },
|
{ text: "Name", value: "title", sortable: true },
|
||||||
{ text: "Size", value: "size", sortable: true },
|
{ text: "Size", value: "size", sortable: true },
|
||||||
// { text: "Permission", value: "permission", sortable: true },
|
{ text: "Uploaded By", value: "user.display_name", sortable: true },
|
||||||
{ text: "Uploaded By", value: "username", sortable: true },
|
|
||||||
{ text: "Created", value: "created_at", sortable: true },
|
|
||||||
// { text: "Updated", value: "updated_at", sortable: true },
|
|
||||||
{ text: "Actions", value: "actions" },
|
{ text: "Actions", value: "actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = ref([]);
|
const handleClickSearch = () => {
|
||||||
let users = {};
|
page.value = 1;
|
||||||
const formLoading = ref(false);
|
handleLoad();
|
||||||
const formMessage = reactive({
|
|
||||||
message: "",
|
|
||||||
type: "error",
|
|
||||||
icon: "",
|
|
||||||
});
|
|
||||||
const serverItemsLength = ref(0);
|
|
||||||
const serverOptions = ref({
|
|
||||||
page: 1,
|
|
||||||
rowsPerPage: 25,
|
|
||||||
sortBy: null,
|
|
||||||
sortType: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = 108;
|
|
||||||
const perPage = 25;
|
|
||||||
const pageValue = ref(1);
|
|
||||||
|
|
||||||
const handleRowClick = (item) => {
|
|
||||||
alert(JSON.stringify(item));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (item, extra: string): void => {
|
const handleClick = (item, extra: string): void => {
|
||||||
if (extra.length == 0) {
|
if (extra.length == 0) {
|
||||||
handleEdit(item);
|
handleEdit(item);
|
||||||
|
} else if (extra.toLowerCase() == "download") {
|
||||||
|
handleDownload(item);
|
||||||
} else if (extra.toLowerCase() == "delete") {
|
} else if (extra.toLowerCase() == "delete") {
|
||||||
handleDelete(item);
|
handleDelete(item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFromServer = async () => {
|
/**
|
||||||
formLoading.value = true;
|
* Watch if page number changes.
|
||||||
formMessage.type = "error";
|
*/
|
||||||
formMessage.icon = "alert-circle-outline";
|
watch(page, () => {
|
||||||
formMessage.message = "";
|
handleLoad();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLoad = async () => {
|
||||||
|
pageLoading.value = true;
|
||||||
|
items.value = [];
|
||||||
|
totalFound.value = 0;
|
||||||
|
|
||||||
|
let routerParams = {
|
||||||
|
search: search.value as string,
|
||||||
|
page: page.value == 1 ? "" : page.value.toString(),
|
||||||
|
};
|
||||||
|
updateRouterParams(router, routerParams);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let params = {};
|
let params = {
|
||||||
if (serverOptions.value.sortBy) {
|
page: page.value,
|
||||||
params["sort"] = serverOptions.value.sortBy;
|
limit: perPage,
|
||||||
if (
|
};
|
||||||
serverOptions.value.sortType &&
|
|
||||||
serverOptions.value.sortType === "desc"
|
|
||||||
) {
|
|
||||||
params["sort"] = "-" + params["sort"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params["page"] = serverOptions.value.page;
|
|
||||||
params["limit"] = serverOptions.value.rowsPerPage;
|
|
||||||
|
|
||||||
if (search.value.length > 0) {
|
if (search.value.length > 0) {
|
||||||
params["title"] = search.value;
|
params[
|
||||||
|
"filter"
|
||||||
|
] = `title:${search.value},OR,name:${search.value},OR,description:${search.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await api.get({
|
let res = await api.get({
|
||||||
@@ -190,27 +147,6 @@ const loadFromServer = async () => {
|
|||||||
items.value = [];
|
items.value = [];
|
||||||
|
|
||||||
res.data.media.forEach(async (row) => {
|
res.data.media.forEach(async (row) => {
|
||||||
if (Object.keys(users).includes(row.user_id) === false) {
|
|
||||||
try {
|
|
||||||
const userResult = await api.get({
|
|
||||||
url: "/users/{id}",
|
|
||||||
params: {
|
|
||||||
id: row.user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = userResult.data as UserResponse;
|
|
||||||
users[row.user_id] = data.user.username;
|
|
||||||
} catch (error) {
|
|
||||||
users[row.user_id] = "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(users).includes(row.user_id)) {
|
|
||||||
row["username"] = users[row.user_id];
|
|
||||||
} else {
|
|
||||||
row["username"] = "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.created_at !== "undefined") {
|
if (row.created_at !== "undefined") {
|
||||||
row.created_at = new SMDate(row.created_at, {
|
row.created_at = new SMDate(row.created_at, {
|
||||||
format: "ymd",
|
format: "ymd",
|
||||||
@@ -227,32 +163,22 @@ const loadFromServer = async () => {
|
|||||||
items.value.push(row);
|
items.value.push(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
serverItemsLength.value = res.data.total;
|
totalFound.value = res.data.total;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
// formMessage.message = parseErrorTyp(err);
|
if (error.status != 404) {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Server Error",
|
||||||
|
content: error.message,
|
||||||
|
//"An error occurred retrieving the list from the server.",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false;
|
pageLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadFromServer();
|
handleLoad();
|
||||||
|
|
||||||
watch(
|
|
||||||
serverOptions,
|
|
||||||
(value) => {
|
|
||||||
loadFromServer();
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedFilter = debounce(loadFromServer, 1000);
|
|
||||||
watch(search, (value) => {
|
|
||||||
debouncedFilter();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClickRow = (item) => {
|
|
||||||
router.push({ name: "dashboard-media-edit", params: { id: item.id } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (item) => {
|
const handleEdit = (item) => {
|
||||||
router.push({ name: "dashboard-media-edit", params: { id: item.id } });
|
router.push({ name: "dashboard-media-edit", params: { id: item.id } });
|
||||||
@@ -291,7 +217,7 @@ const handleDelete = async (item: Media) => {
|
|||||||
content: `The file ${item.title} has been deleted.`,
|
content: `The file ${item.title} has been deleted.`,
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
loadFromServer();
|
handleLoad();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastStore.addToast({
|
toastStore.addToast({
|
||||||
title: "Error Deleting File",
|
title: "Error Deleting File",
|
||||||
@@ -305,18 +231,19 @@ const handleDelete = async (item: Media) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (item) => {
|
const handleDownload = (item) => {
|
||||||
window.open(item.url, "_blank");
|
window.open(`${item.url}?download=1`, "_blank");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
body[data-route-name="page-dashboard-media-list"] {
|
.page-dashboard-media-list {
|
||||||
.table tr td:last-of-type {
|
.table tr {
|
||||||
padding-left: 0;
|
td:first-of-type {
|
||||||
text-align: center;
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
td:not(:first-of-type) {
|
||||||
display: none;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMMastHead
|
<SMMastHead
|
||||||
:title="pageHeading"
|
:title="pageHeading"
|
||||||
:back-link="{ name: 'dashboard' }"
|
:back-link="
|
||||||
back-title="Back to Dashboard" />
|
route.params.id
|
||||||
|
? { name: 'dashboard-user-list' }
|
||||||
|
: { name: 'dashboard' }
|
||||||
|
"
|
||||||
|
:back-title="route.params.id ? 'Back to Users' : 'Back to Dashboard'" />
|
||||||
<SMContainer>
|
<SMContainer>
|
||||||
<SMForm :model-value="form" @submit="handleSubmit">
|
<SMForm :model-value="form" @submit="handleSubmit">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
@@ -16,9 +20,35 @@
|
|||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn><SMInput control="email" /></SMColumn>
|
<SMColumn><SMInput control="email" /></SMColumn>
|
||||||
<SMColumn
|
<SMColumn
|
||||||
><SMInput control="phone">This field is optional</SMInput>
|
><SMInput control="phone"
|
||||||
|
><template #help
|
||||||
|
>This field is optional</template
|
||||||
|
></SMInput
|
||||||
|
>
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
|
<template v-if="userStore.permissions.includes('admin/users')">
|
||||||
|
<SMRow
|
||||||
|
><SMColumn><h3>Permissions</h3></SMColumn></SMRow
|
||||||
|
>
|
||||||
|
<SMRow>
|
||||||
|
<SMColumn
|
||||||
|
><SMCheckbox
|
||||||
|
label="Edit Users"
|
||||||
|
v-model="permissions.users"
|
||||||
|
/></SMColumn>
|
||||||
|
<SMColumn
|
||||||
|
><SMCheckbox
|
||||||
|
label="Edit Posts"
|
||||||
|
v-model="permissions.users"
|
||||||
|
/></SMColumn>
|
||||||
|
<SMColumn
|
||||||
|
><SMCheckbox
|
||||||
|
label="Edit Events"
|
||||||
|
v-model="permissions.users"
|
||||||
|
/></SMColumn>
|
||||||
|
</SMRow>
|
||||||
|
</template>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
@@ -37,7 +67,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
|
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
|
||||||
@@ -52,6 +82,7 @@ import { And, Email, Phone, Required } from "../../helpers/validate";
|
|||||||
import { useUserStore } from "../../store/UserStore";
|
import { useUserStore } from "../../store/UserStore";
|
||||||
import SMMastHead from "../../components/SMMastHead.vue";
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
import { useToastStore } from "../../store/ToastStore";
|
import { useToastStore } from "../../store/ToastStore";
|
||||||
|
import SMCheckbox from "../../components/SMCheckbox.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -68,6 +99,10 @@ let form = reactive(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const permissions = ref({
|
||||||
|
users: false,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the page data.
|
* Load the page data.
|
||||||
*/
|
*/
|
||||||
@@ -163,7 +198,10 @@ loadData();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.sm-page-user-edit {
|
.page-dashboard-account-details {
|
||||||
background-color: #f8f8f8;
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMPage permission="admin/users">
|
<SMPage permission="admin/users">
|
||||||
<SMHeading heading="Users" />
|
<SMMastHead
|
||||||
|
title="Users"
|
||||||
|
:back-link="{ name: 'dashboard' }"
|
||||||
|
back-title="Return to Dashboard" />
|
||||||
<SMMessage
|
<SMMessage
|
||||||
v-if="formMessage.message"
|
v-if="formMessage.message"
|
||||||
:icon="formMessage.icon"
|
:icon="formMessage.icon"
|
||||||
:type="formMessage.type"
|
:type="formMessage.type"
|
||||||
:message="formMessage.message" />
|
:message="formMessage.message" />
|
||||||
<EasyDataTable
|
|
||||||
v-model:server-options="serverOptions"
|
<SMContainer>
|
||||||
:server-items-length="serverItemsLength"
|
<SMTable
|
||||||
:loading="formLoading"
|
:headers="headers"
|
||||||
:headers="headers"
|
:items="items"
|
||||||
:items="items"
|
@row-click="handleRowClick">
|
||||||
:search-value="searchValue"
|
<template #item-actions="item">
|
||||||
:header-item-class-name="headerItemClassNameFunction"
|
<SMButton
|
||||||
:body-item-class-name="bodyItemClassNameFunction">
|
label="Edit"
|
||||||
<template #loading>
|
:dropdown="{
|
||||||
<SMLoadingIcon />
|
download: 'Download',
|
||||||
</template>
|
delete: 'Delete',
|
||||||
<template #item-actions="item">
|
}"
|
||||||
<div class="action-wrapper"></div>
|
size="medium" />
|
||||||
</template>
|
</template>
|
||||||
</EasyDataTable>
|
</SMTable>
|
||||||
|
</SMContainer>
|
||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from "vue";
|
import { reactive, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import EasyDataTable from "vue3-easy-data-table";
|
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
import SMHeading from "../../components/SMHeading.vue";
|
|
||||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMMessage from "../../components/SMMessage.vue";
|
||||||
import { api } from "../../helpers/api";
|
import { api } from "../../helpers/api";
|
||||||
import { SMDate } from "../../helpers/datetime";
|
import { SMDate } from "../../helpers/datetime";
|
||||||
|
import SMTable from "../../components/SMTable.vue";
|
||||||
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchValue = ref("");
|
const searchValue = ref("");
|
||||||
@@ -67,6 +70,10 @@ const serverOptions = ref({
|
|||||||
sortType: null,
|
sortType: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRowClick = (item) => {
|
||||||
|
router.push({ name: "dashboard-user-edit", params: { id: item.id } });
|
||||||
|
};
|
||||||
|
|
||||||
const loadFromServer = async () => {
|
const loadFromServer = async () => {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
formMessage.type = "error";
|
formMessage.type = "error";
|
||||||
|
|||||||
Reference in New Issue
Block a user