updates
This commit is contained in:
@@ -259,14 +259,14 @@ class Conductor
|
||||
|
||||
// Transform and Includes
|
||||
$includes = $conductor->includes;
|
||||
if(count($limitFields) > 0) {
|
||||
if (count($limitFields) > 0) {
|
||||
$includes = array_intersect($limitFields, $conductor->includes);
|
||||
}
|
||||
|
||||
$conductor->collection = $conductor->collection->map(function ($model) use ($conductor, $includes, $limitFields) {
|
||||
$conductor->applyIncludes($model, $includes);
|
||||
|
||||
if(count($limitFields) > 0) {
|
||||
if (count($limitFields) > 0) {
|
||||
$model->setAppends(array_intersect($model->getAppends(), $limitFields));
|
||||
}
|
||||
|
||||
@@ -316,10 +316,10 @@ class Conductor
|
||||
$requestFields = $request->input('fields');
|
||||
if ($requestFields !== null) {
|
||||
$requestFields = explode(',', $requestFields);
|
||||
if(in_array($key, $requestFields) === false) {
|
||||
foreach($requestFields as $field) {
|
||||
if(strpos($field, $key . '.') === 0) {
|
||||
$fields[] = substr($field, strlen($key) + 1);
|
||||
if (in_array($key, $requestFields) === false) {
|
||||
foreach ($requestFields as $field) {
|
||||
if (strpos($field, $key . '.') === 0) {
|
||||
$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.
|
||||
*
|
||||
* @param mixed $fields The fields to show.
|
||||
* @param Model|null $model The model.
|
||||
* @param mixed $fields The fields to show.
|
||||
* @param Model|null $model The model.
|
||||
* @return array The processed and transformed model data.
|
||||
*/
|
||||
final public static function model(mixed $fields, mixed $model)
|
||||
@@ -349,21 +349,21 @@ class Conductor
|
||||
|
||||
// Limit fields
|
||||
$limitFields = $modelFields;
|
||||
if($fields instanceof Request) {
|
||||
if ($fields instanceof Request) {
|
||||
if ($fields !== null && $fields->has('fields') === true) {
|
||||
$requestFields = $fields->input('fields');
|
||||
if ($requestFields !== null) {
|
||||
$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);
|
||||
}
|
||||
|
||||
if (empty($limitFields) === false) {
|
||||
$modelAppends = $model->getAppends();
|
||||
|
||||
foreach(array_diff($modelFields, $limitFields) as $attribute) {
|
||||
foreach (array_diff($modelFields, $limitFields) as $attribute) {
|
||||
$key = array_search($attribute, $modelAppends);
|
||||
if ($key !== false) {
|
||||
unset($modelAppends[$key]);
|
||||
@@ -476,8 +476,8 @@ class Conductor
|
||||
/**
|
||||
* Paginate the conductor collection.
|
||||
*
|
||||
* @param integer $page The current page to return.
|
||||
* @param integer $limit The limit of items to include or use default.
|
||||
* @param integer $page The current page to return.
|
||||
* @param integer $limit The limit of items to include or use default.
|
||||
* @param integer $offset Offset the page count after this count of rows.
|
||||
* @return void
|
||||
*/
|
||||
@@ -687,8 +687,8 @@ class Conductor
|
||||
/**
|
||||
* Return an array of model fields visible to the current user.
|
||||
*
|
||||
* @param Model $model The model in question.
|
||||
* @param bool $includes Include the includes in the result.
|
||||
* @param Model $model The model in question.
|
||||
* @param boolean $includes Include the includes in the result.
|
||||
* @return array The array of field names.
|
||||
*/
|
||||
public function fields(Model $model)
|
||||
@@ -727,7 +727,7 @@ class Conductor
|
||||
$result[$key] = $this->$transformFunction($value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$result = $this->transformFinal($result);
|
||||
return $result;
|
||||
}
|
||||
@@ -743,7 +743,7 @@ class Conductor
|
||||
$result = $model->toArray();
|
||||
|
||||
$fields = $this->fields($model);
|
||||
|
||||
|
||||
if (is_array($fields) === true) {
|
||||
$result = array_intersect_key($result, array_flip($fields));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Conductors;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
|
||||
class MediaConductor extends Conductor
|
||||
{
|
||||
@@ -19,6 +20,13 @@ class MediaConductor extends Conductor
|
||||
*/
|
||||
protected $sort = 'created_at';
|
||||
|
||||
/**
|
||||
* The included fields
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $includes = ['user'];
|
||||
|
||||
|
||||
/**
|
||||
* Return an array of model fields visible to the current user.
|
||||
@@ -106,4 +114,27 @@ class MediaConductor extends Conductor
|
||||
$user = auth()->user();
|
||||
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
|
||||
*
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $includes = ['attachments', 'user'];
|
||||
|
||||
|
||||
/**
|
||||
* Run a scope query on the collection before anything else.
|
||||
*
|
||||
@@ -105,7 +106,7 @@ class PostConductor extends Conductor
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function transformFinal(array $data)
|
||||
@@ -116,7 +117,7 @@ class PostConductor extends Conductor
|
||||
|
||||
/**
|
||||
* Include Attachments Field.
|
||||
*
|
||||
*
|
||||
* @param Model $model Them model.
|
||||
* @return mixed The model result.
|
||||
*/
|
||||
@@ -129,7 +130,7 @@ class PostConductor extends Conductor
|
||||
|
||||
/**
|
||||
* Include User Field.
|
||||
*
|
||||
*
|
||||
* @param Model $model Them model.
|
||||
* @return mixed The model result.
|
||||
*/
|
||||
@@ -140,7 +141,7 @@ class PostConductor extends Conductor
|
||||
|
||||
/**
|
||||
* Transform the Hero field.
|
||||
*
|
||||
*
|
||||
* @param mixed $value The current 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(
|
||||
mixed $data,
|
||||
array $options = [],
|
||||
$validationFn = null
|
||||
) {
|
||||
$isCollection = $options['isCollection'] ?? false;
|
||||
$appendData = $options['appendData'] ?? null;
|
||||
@@ -144,7 +145,14 @@ class ApiController extends Controller
|
||||
$respondCode = ($options['respondCode'] ?? HttpResponseCodes::HTTP_OK);
|
||||
|
||||
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) {
|
||||
|
||||
@@ -35,7 +35,10 @@ class MediaController extends ApiController
|
||||
$collection,
|
||||
['isCollection' => true,
|
||||
'appendData' => ['total' => $total]
|
||||
]
|
||||
],
|
||||
function ($options) {
|
||||
return $options['total'] === 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ return new class extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('media')->whereNull('mime')->update(['mime' => '']);
|
||||
DB::table('media')->whereNull('permission')->update(['permission' => '']);
|
||||
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->string('storage')->default("cdn");
|
||||
$table->string('description')->default("");
|
||||
@@ -21,18 +24,11 @@ return new class extends Migration
|
||||
$table->string('dimensions')->default("");
|
||||
$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->string('permission')->default("")->nullable(false)->change();
|
||||
|
||||
$table->string('mime')->default("")->nullable(false)->change();
|
||||
// $table->renameColumn('mime', 'mime_type');
|
||||
$table->string('mime_type');
|
||||
$table->renameColumn('mime', 'mime_type');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<td class="attachment-file-icon">
|
||||
<img
|
||||
:src="getFileIconImagePath(file.name || file.title)"
|
||||
height="48"
|
||||
width="48" />
|
||||
height="40"
|
||||
width="40" />
|
||||
</td>
|
||||
<td class="attachment-file-name">
|
||||
<a :href="file.url">{{ file.title || file.name }}</a>
|
||||
@@ -57,15 +57,16 @@ const props = defineProps({
|
||||
|
||||
<style lang="scss">
|
||||
.attachment-list {
|
||||
border: 1px solid $secondary-color;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
max-width: 580px;
|
||||
// max-width: 580px;
|
||||
margin-top: 12px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
.attachment-row {
|
||||
td {
|
||||
border-bottom: 1px solid $secondary-background-color;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@@ -74,7 +75,8 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.attachment-file-icon {
|
||||
width: 64px;
|
||||
width: 56px;
|
||||
padding-left: 8px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
@@ -82,9 +84,18 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.attachment-file-name {
|
||||
font-size: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-download {
|
||||
@@ -109,11 +120,12 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.attachment-file-size {
|
||||
width: 64px;
|
||||
width: 80px;
|
||||
font-size: 75%;
|
||||
color: $secondary-color-dark;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "Button", required: false },
|
||||
type: { type: String, default: "secondary", required: false },
|
||||
type: { type: String, default: "", required: false },
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -193,6 +193,7 @@ const handleClickItem = (item: string) => {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
||||
.button-label {
|
||||
display: inline-block;
|
||||
@@ -254,6 +255,10 @@ const handleClickItem = (item: string) => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.button-dropdown) {
|
||||
filter: brightness(115%);
|
||||
}
|
||||
|
||||
&:hover:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -285,5 +290,9 @@ const handleClickItem = (item: string) => {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--base-color);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
||||
</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,
|
||||
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");
|
||||
rightBtn.classList.add("image-gallery-content-item-right");
|
||||
rightBtn.onclick = function () {
|
||||
|
||||
@@ -48,6 +48,10 @@ const slots = useSlots();
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.body .row {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,55 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'input-control-group',
|
||||
{ 'input-active': active, 'input-invalid': feedbackInvalid },
|
||||
]">
|
||||
<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>
|
||||
<SMControl
|
||||
:class="['control-type-input', { 'input-active': active }]"
|
||||
:invalid="feedbackInvalid">
|
||||
<div v-if="slots.prepend" class="input-control-prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</div>
|
||||
<div v-if="slots.default || feedbackInvalid" class="input-help">
|
||||
<span v-if="feedbackInvalid" class="input-invalid">
|
||||
{{ feedbackInvalid }}
|
||||
</span>
|
||||
<span v-if="slots.default"><slot></slot></span>
|
||||
<div class="control-item">
|
||||
<label class="control-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>
|
||||
<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>
|
||||
|
||||
<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", "keyup"]);
|
||||
const props = defineProps({
|
||||
@@ -225,19 +215,16 @@ const handleKeyup = (event: Event) => {
|
||||
const handleClear = () => {
|
||||
value.value = "";
|
||||
emits("update:modelValue", "");
|
||||
emits("change");
|
||||
// emits("change");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.input-control-group {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
|
||||
.input-control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.control-item {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.input-control-prepend {
|
||||
p {
|
||||
@@ -258,7 +245,7 @@ const handleClear = () => {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
& + .input-control-item .input-control {
|
||||
& + .control-item .input-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
@@ -284,18 +271,15 @@ const handleClear = () => {
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.input-control-item + .input-control-append)
|
||||
> .input-control-item
|
||||
&:has(.control-item + .input-control-append)
|
||||
> .control-item
|
||||
.input-control {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-control-item {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.input-label {
|
||||
.control-item {
|
||||
.control-label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
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-control-item .input-label {
|
||||
.control-item .control-label {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&.input-invalid {
|
||||
.input-control-row .input-control-item {
|
||||
&.control-invalid {
|
||||
.control-row .control-item {
|
||||
.invalid-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<div
|
||||
v-if="props.modelValue > 1"
|
||||
:class="[
|
||||
'item',
|
||||
'previous',
|
||||
@@ -9,17 +8,16 @@
|
||||
]"
|
||||
@click="handleClickPrev">
|
||||
<ion-icon name="chevron-back-outline" />
|
||||
Previous
|
||||
Prev
|
||||
</div>
|
||||
<div
|
||||
:class="['item', { active: page == props.modelValue }]"
|
||||
:class="['item', 'page', { active: page == props.modelValue }]"
|
||||
v-for="(page, idx) of computedPages"
|
||||
:key="idx"
|
||||
@click="handleClickPage(page)">
|
||||
{{ page }}
|
||||
</div>
|
||||
<div
|
||||
v-if="(props.modelValue + 3) * props.perPage <= props.total"
|
||||
:class="['item', 'next', { disabled: computedDisableNextButton }]"
|
||||
@click="handleClickNext">
|
||||
Next
|
||||
@@ -29,6 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { unwatchFile } from "fs";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -54,22 +53,27 @@ const emits = defineEmits(["update:modelValue"]);
|
||||
const computedPages = computed(() => {
|
||||
let pages = [];
|
||||
|
||||
if (props.modelValue - 2 > 0) {
|
||||
pages.push(props.modelValue - 2);
|
||||
let pagesRemaining =
|
||||
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) {
|
||||
pages.push(props.modelValue - 1);
|
||||
for (; pagesBefore > 0; pagesBefore--) {
|
||||
pages.push(props.modelValue - pagesBefore);
|
||||
}
|
||||
|
||||
pages.push(props.modelValue);
|
||||
|
||||
if (props.perPage * (props.modelValue + 1) <= props.total) {
|
||||
pages.push(props.modelValue + 1);
|
||||
}
|
||||
|
||||
if (props.perPage * (props.modelValue + 2) <= props.total) {
|
||||
pages.push(props.modelValue + 2);
|
||||
for (let i = 1; i <= pagesRemaining; i++) {
|
||||
pages.push(props.modelValue + i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
@@ -100,14 +104,18 @@ const computedDisableNextButton = computed(() => {
|
||||
* Handle click on previous button
|
||||
*/
|
||||
const handleClickPrev = (): void => {
|
||||
emits("update:modelValue", props.modelValue - 1);
|
||||
if (computedDisablePrevButton.value == false) {
|
||||
emits("update:modelValue", props.modelValue - 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on next button
|
||||
*/
|
||||
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 => {
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -138,6 +155,11 @@ const handleClickPage = (page: number): void => {
|
||||
padding: 12px 16px;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.page {
|
||||
width: 44px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
@@ -158,9 +180,15 @@ const handleClickPage = (page: number): void => {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
&:hover:not(.active):not(.disabled) {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 auto;
|
||||
margin: 8px auto;
|
||||
align-items: top;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
v-bind="item as any">
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>{{ item[header["value"]] }}</template>
|
||||
<template v-else>
|
||||
{{
|
||||
header["value"]
|
||||
.split(".")
|
||||
.reduce((item, key) => item[key], item)
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -69,6 +75,7 @@ const handleRowClick = (item) => {
|
||||
|
||||
th {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<SMFormCard :loading="dialogLoading">
|
||||
<h1>Change Password</h1>
|
||||
<p class="text-center">Enter your new password below</p>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMInput control="password" type="password" label="New Password" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="secondary"
|
||||
label="Cancel"
|
||||
@click="handleClickCancel" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton type="submit" label="Update" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
</SMFormCard>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMFormCard :loading="dialogLoading">
|
||||
<template #header>
|
||||
<h3>Change Password</h3>
|
||||
<p>Enter your new password below</p>
|
||||
</template>
|
||||
<template #body>
|
||||
<SMInput
|
||||
control="password"
|
||||
type="password"
|
||||
label="New Password"
|
||||
autofocus />
|
||||
</template>
|
||||
<template #footer-space-between>
|
||||
<SMButton label="Cancel" @click="handleClickCancel" />
|
||||
<SMButton type="submit" label="Update" />
|
||||
</template>
|
||||
</SMFormCard>
|
||||
</SMForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Router } from "vue-router";
|
||||
|
||||
export const urlStripAttributes = (url: string): string => {
|
||||
const urlObject = new URL(url);
|
||||
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) {
|
||||
next({ name: "login", query: { redirect: to.fullPath } });
|
||||
next({
|
||||
name: "login",
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -25,17 +25,19 @@ export interface UserState {
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
state: (): UserState => ({
|
||||
id: "",
|
||||
token: "",
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
displayName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
permissions: [],
|
||||
}),
|
||||
state: (): UserState => {
|
||||
return {
|
||||
id: "",
|
||||
token: "",
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
displayName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
permissions: [],
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
async setUserDetails(user: UserDetails) {
|
||||
@@ -63,9 +65,6 @@ export const useUserStore = defineStore({
|
||||
this.$state.email = null;
|
||||
this.$state.phone = null;
|
||||
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 {
|
||||
.support {
|
||||
img {
|
||||
@@ -245,7 +245,7 @@ import SMHero from "../components/SMHero.vue";
|
||||
}
|
||||
.button-row {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 15px;
|
||||
|
||||
.button {
|
||||
text-align: center;
|
||||
|
||||
@@ -83,7 +83,7 @@ const handleSubmit = async () => {
|
||||
? redirectQuery[0]
|
||||
: redirectQuery;
|
||||
|
||||
router.push({ path: redirect });
|
||||
router.push(decodeURIComponent(redirect));
|
||||
} else {
|
||||
router.push({ name: "dashboard" });
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
:to="{ name: 'dashboard-post-list' }"
|
||||
class="admin-card posts">
|
||||
<ion-icon name="newspaper-outline" />
|
||||
<h3>Posts</h3>
|
||||
<h3>Articles</h3>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/users')"
|
||||
|
||||
@@ -3,180 +3,137 @@
|
||||
title="Media"
|
||||
:back-link="{ name: 'dashboard' }"
|
||||
back-title="Return to Dashboard" />
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<SMContainer>
|
||||
<SMContainer class="flex-grow-1">
|
||||
<SMToolbar>
|
||||
<template #left>
|
||||
<SMButton
|
||||
:to="{ name: 'workshops' }"
|
||||
type="primary"
|
||||
label="Upload Media" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMInput
|
||||
v-model="search"
|
||||
label="Search"
|
||||
:small="true"
|
||||
style="max-width: 350px"
|
||||
:show-clear="true">
|
||||
<template #append>
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Search"
|
||||
icon="search-outline"
|
||||
@click="handleClickSearch" />
|
||||
</template>
|
||||
</SMInput>
|
||||
</template>
|
||||
<SMButton
|
||||
:to="{ name: 'workshops' }"
|
||||
type="primary"
|
||||
label="Upload Media" />
|
||||
<SMInput
|
||||
v-model="search"
|
||||
label="Search"
|
||||
style="max-width: 350px"
|
||||
@keyup.enter="handleClickSearch">
|
||||
<template #append>
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Search"
|
||||
icon="search-outline"
|
||||
@click="handleClickSearch" />
|
||||
</template>
|
||||
</SMInput>
|
||||
</SMToolbar>
|
||||
|
||||
<SMPagination
|
||||
:model-value="pageValue"
|
||||
:total="total"
|
||||
:per-page="perPage" />
|
||||
<SMTable :headers="headers" :items="items" @row-click="handleRowClick">
|
||||
<template #item-size="item">
|
||||
{{ bytesReadable(item.size) }}
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<SMButton
|
||||
label="Edit"
|
||||
:dropdown="{
|
||||
download: 'Download',
|
||||
delete: 'Delete',
|
||||
}"
|
||||
size="medium"
|
||||
@click="handleClick(item, $event)"></SMButton>
|
||||
</template>
|
||||
</SMTable>
|
||||
<SMLoading large v-if="pageLoading" />
|
||||
<template v-else>
|
||||
<SMPagination
|
||||
v-if="items.length < totalFound"
|
||||
v-model="page"
|
||||
:total="totalFound"
|
||||
:per-page="perPage" />
|
||||
<SMNoItems v-if="items.length == 0" text="No Media Found" />
|
||||
<SMTable
|
||||
v-else
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
@row-click="handleEdit">
|
||||
<template #item-size="item">
|
||||
{{ bytesReadable(item.size) }}
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<SMButton
|
||||
label="Edit"
|
||||
:dropdown="{
|
||||
download: 'Download',
|
||||
delete: 'Delete',
|
||||
}"
|
||||
size="medium"
|
||||
@click="handleClick(item, $event)"></SMButton>
|
||||
</template>
|
||||
</SMTable>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.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 { api } from "../../helpers/api";
|
||||
import { Media, UserResponse } from "../../helpers/api.types";
|
||||
import { Media } from "../../helpers/api.types";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { debounce } from "../../helpers/debounce";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMMastHead from "../../components/SMMastHead.vue";
|
||||
import SMTable from "../../components/SMTable.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 search = ref("");
|
||||
const userStore = useUserStore();
|
||||
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 = [
|
||||
{ text: "Name", value: "title", sortable: true },
|
||||
{ text: "Size", value: "size", sortable: true },
|
||||
// { text: "Permission", value: "permission", sortable: true },
|
||||
{ text: "Uploaded By", value: "username", sortable: true },
|
||||
{ text: "Created", value: "created_at", sortable: true },
|
||||
// { text: "Updated", value: "updated_at", sortable: true },
|
||||
{ text: "Uploaded By", value: "user.display_name", sortable: true },
|
||||
{ text: "Actions", value: "actions" },
|
||||
];
|
||||
|
||||
const items = ref([]);
|
||||
let users = {};
|
||||
const formLoading = ref(false);
|
||||
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 handleClickSearch = () => {
|
||||
page.value = 1;
|
||||
handleLoad();
|
||||
};
|
||||
|
||||
const handleClick = (item, extra: string): void => {
|
||||
if (extra.length == 0) {
|
||||
handleEdit(item);
|
||||
} else if (extra.toLowerCase() == "download") {
|
||||
handleDownload(item);
|
||||
} else if (extra.toLowerCase() == "delete") {
|
||||
handleDelete(item);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFromServer = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "alert-circle-outline";
|
||||
formMessage.message = "";
|
||||
/**
|
||||
* Watch if page number changes.
|
||||
*/
|
||||
watch(page, () => {
|
||||
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 {
|
||||
let params = {};
|
||||
if (serverOptions.value.sortBy) {
|
||||
params["sort"] = serverOptions.value.sortBy;
|
||||
if (
|
||||
serverOptions.value.sortType &&
|
||||
serverOptions.value.sortType === "desc"
|
||||
) {
|
||||
params["sort"] = "-" + params["sort"];
|
||||
}
|
||||
}
|
||||
|
||||
params["page"] = serverOptions.value.page;
|
||||
params["limit"] = serverOptions.value.rowsPerPage;
|
||||
let params = {
|
||||
page: page.value,
|
||||
limit: perPage,
|
||||
};
|
||||
|
||||
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({
|
||||
@@ -190,27 +147,6 @@ const loadFromServer = async () => {
|
||||
items.value = [];
|
||||
|
||||
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") {
|
||||
row.created_at = new SMDate(row.created_at, {
|
||||
format: "ymd",
|
||||
@@ -227,32 +163,22 @@ const loadFromServer = async () => {
|
||||
items.value.push(row);
|
||||
});
|
||||
|
||||
serverItemsLength.value = res.data.total;
|
||||
} catch (err) {
|
||||
// formMessage.message = parseErrorTyp(err);
|
||||
totalFound.value = res.data.total;
|
||||
} catch (error) {
|
||||
if (error.status != 404) {
|
||||
toastStore.addToast({
|
||||
title: "Server Error",
|
||||
content: error.message,
|
||||
//"An error occurred retrieving the list from the server.",
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
pageLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
loadFromServer();
|
||||
|
||||
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 } });
|
||||
};
|
||||
handleLoad();
|
||||
|
||||
const handleEdit = (item) => {
|
||||
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.`,
|
||||
type: "success",
|
||||
});
|
||||
loadFromServer();
|
||||
handleLoad();
|
||||
} catch (error) {
|
||||
toastStore.addToast({
|
||||
title: "Error Deleting File",
|
||||
@@ -305,18 +231,19 @@ const handleDelete = async (item: Media) => {
|
||||
};
|
||||
|
||||
const handleDownload = (item) => {
|
||||
window.open(item.url, "_blank");
|
||||
window.open(`${item.url}?download=1`, "_blank");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body[data-route-name="page-dashboard-media-list"] {
|
||||
.table tr td:last-of-type {
|
||||
padding-left: 0;
|
||||
text-align: center;
|
||||
.page-dashboard-media-list {
|
||||
.table tr {
|
||||
td:first-of-type {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
td:not(:first-of-type) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<SMMastHead
|
||||
:title="pageHeading"
|
||||
:back-link="{ name: 'dashboard' }"
|
||||
back-title="Back to Dashboard" />
|
||||
:back-link="
|
||||
route.params.id
|
||||
? { name: 'dashboard-user-list' }
|
||||
: { name: 'dashboard' }
|
||||
"
|
||||
:back-title="route.params.id ? 'Back to Users' : 'Back to Dashboard'" />
|
||||
<SMContainer>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
@@ -16,9 +20,35 @@
|
||||
<SMRow>
|
||||
<SMColumn><SMInput control="email" /></SMColumn>
|
||||
<SMColumn
|
||||
><SMInput control="phone">This field is optional</SMInput>
|
||||
><SMInput control="phone"
|
||||
><template #help
|
||||
>This field is optional</template
|
||||
></SMInput
|
||||
>
|
||||
</SMColumn>
|
||||
</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>
|
||||
<SMColumn>
|
||||
<SMFormFooter>
|
||||
@@ -37,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
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 SMMastHead from "../../components/SMMastHead.vue";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import SMCheckbox from "../../components/SMCheckbox.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -68,6 +99,10 @@ let form = reactive(
|
||||
})
|
||||
);
|
||||
|
||||
const permissions = ref({
|
||||
users: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
@@ -163,7 +198,10 @@ loadData();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-page-user-edit {
|
||||
background-color: #f8f8f8;
|
||||
.page-dashboard-account-details {
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
<template>
|
||||
<SMPage permission="admin/users">
|
||||
<SMHeading heading="Users" />
|
||||
<SMMastHead
|
||||
title="Users"
|
||||
:back-link="{ name: 'dashboard' }"
|
||||
back-title="Return to Dashboard" />
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:icon="formMessage.icon"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message" />
|
||||
<EasyDataTable
|
||||
v-model:server-options="serverOptions"
|
||||
:server-items-length="serverItemsLength"
|
||||
:loading="formLoading"
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:search-value="searchValue"
|
||||
:header-item-class-name="headerItemClassNameFunction"
|
||||
:body-item-class-name="bodyItemClassNameFunction">
|
||||
<template #loading>
|
||||
<SMLoadingIcon />
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<div class="action-wrapper"></div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
|
||||
<SMContainer>
|
||||
<SMTable
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
@row-click="handleRowClick">
|
||||
<template #item-actions="item">
|
||||
<SMButton
|
||||
label="Edit"
|
||||
:dropdown="{
|
||||
download: 'Download',
|
||||
delete: 'Delete',
|
||||
}"
|
||||
size="medium" />
|
||||
</template>
|
||||
</SMTable>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
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 { api } from "../../helpers/api";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import SMTable from "../../components/SMTable.vue";
|
||||
import SMMastHead from "../../components/SMMastHead.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const searchValue = ref("");
|
||||
@@ -67,6 +70,10 @@ const serverOptions = ref({
|
||||
sortType: null,
|
||||
});
|
||||
|
||||
const handleRowClick = (item) => {
|
||||
router.push({ name: "dashboard-user-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const loadFromServer = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
|
||||
Reference in New Issue
Block a user