add: full multi-tenancy control
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
@props([
|
||||
'isActive' => true,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'box-shadow rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900']) }}>
|
||||
<v-accordion
|
||||
is-active="{{ $isActive }}"
|
||||
{{ $attributes }}
|
||||
>
|
||||
<x-admin::shimmer.accordion class="h-[271px] w-[360px]" />
|
||||
|
||||
@isset($header)
|
||||
<template v-slot:header="{ toggle, isOpen }">
|
||||
<div {{ $header->attributes->merge(['class' => 'flex items-center justify-between p-1.5']) }}>
|
||||
{{ $header }}
|
||||
|
||||
<span
|
||||
:class="`cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950 ${isOpen ? 'icon-up-arrow' : 'icon-down-arrow'}`"
|
||||
@click="toggle"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($content)
|
||||
<template v-slot:content="{ isOpen }">
|
||||
<div
|
||||
{{ $content->attributes->merge(['class' => 'px-4 pb-4']) }}
|
||||
v-show="isOpen"
|
||||
>
|
||||
{{ $content }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
</v-accordion>
|
||||
</div>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-accordion-template"
|
||||
>
|
||||
<div>
|
||||
<slot
|
||||
name="header"
|
||||
:toggle="toggle"
|
||||
:isOpen="isOpen"
|
||||
>
|
||||
Default Header
|
||||
</slot>
|
||||
|
||||
<slot
|
||||
name="content"
|
||||
:isOpen="isOpen"
|
||||
>
|
||||
Default Content
|
||||
</slot>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-accordion', {
|
||||
template: '#v-accordion-template',
|
||||
|
||||
props: [
|
||||
'isActive',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: this.isActive,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isOpen = ! this.isOpen;
|
||||
|
||||
this.$emit('toggle', { isActive: this.isOpen });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,285 @@
|
||||
@props([
|
||||
'entity' => null,
|
||||
'entityControlName' => null,
|
||||
])
|
||||
|
||||
<!-- Activity Button -->
|
||||
<div>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.create_btn.before') !!}
|
||||
|
||||
<button
|
||||
class="flex h-[74px] w-[84px] flex-col items-center justify-center gap-1 rounded-lg border border-transparent bg-blue-200 font-medium text-blue-800 transition-all hover:border-blue-400"
|
||||
@click="$refs.actionComponent.openModal('mail')"
|
||||
>
|
||||
<span class="icon-activity text-2xl dark:!text-blue-800"></span>
|
||||
|
||||
@lang('admin::app.components.activities.actions.activity.btn')
|
||||
</button>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.create_btn.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.before') !!}
|
||||
|
||||
<!-- Note Action Vue Component -->
|
||||
<v-activity
|
||||
ref="actionComponent"
|
||||
:entity="{{ json_encode($entity) }}"
|
||||
entity-control-name="{{ $entityControlName }}"
|
||||
></v-activity>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.after') !!}
|
||||
</div>
|
||||
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-activity-template">
|
||||
<Teleport to="body">
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.before') !!}
|
||||
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, save)">
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.before') !!}
|
||||
|
||||
<x-admin::modal
|
||||
ref="activityModal"
|
||||
position="bottom-right"
|
||||
>
|
||||
<x-slot:header>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.header.dropdown.before') !!}
|
||||
|
||||
<x-admin::dropdown>
|
||||
<x-slot:toggle>
|
||||
<h3 class="flex cursor-pointer items-center gap-1 text-base font-semibold dark:text-white">
|
||||
@lang('admin::app.components.activities.actions.activity.title') - @{{ selectedType.label }}
|
||||
|
||||
<span class="icon-down-arrow text-2xl"></span>
|
||||
</h3>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:menu>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.header.dropdown.menu_item.before') !!}
|
||||
|
||||
<x-admin::dropdown.menu.item
|
||||
::class="{ 'bg-gray-100 dark:bg-gray-950': selectedType.value === type.value }"
|
||||
v-for="type in availableTypes"
|
||||
@click="selectedType = type"
|
||||
>
|
||||
@{{ type.label }}
|
||||
</x-admin::dropdown.menu.item>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.header.dropdown.menu_item.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.header.dropdown.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.content.controls.before') !!}
|
||||
|
||||
<!-- Activity Type -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
name="type"
|
||||
v-model="selectedType.value"
|
||||
/>
|
||||
|
||||
<!-- Id -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="entityControlName"
|
||||
::value="entity.id"
|
||||
/>
|
||||
|
||||
<!-- Title -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.activity.title-control')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
name="title"
|
||||
rules="required|max:80"
|
||||
:label="trans('admin::app.components.activities.actions.activity.title-control')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="title" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Description -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.activity.description')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
name="comment"
|
||||
rules="max:500"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="comment" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Participants -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.activity.participants.title')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::activities.actions.activity.participants />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Schedule Date -->
|
||||
<div class="flex gap-4">
|
||||
<!-- Started From -->
|
||||
<x-admin::form.control-group class="w-full">
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.activity.schedule-from')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="datetime"
|
||||
name="schedule_from"
|
||||
rules="required"
|
||||
:label="trans('admin::app.components.activities.actions.activity.schedule-from')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="schedule_from" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Started To -->
|
||||
<x-admin::form.control-group class="w-full">
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.activity.schedule-to')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="datetime"
|
||||
name="schedule_to"
|
||||
rules="required"
|
||||
:label="trans('admin::app.components.activities.actions.activity.schedule-to')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="schedule_to" />
|
||||
</x-admin::form.control-group>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<x-admin::form.control-group class="!mb-0">
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.activity.location')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
name="location"
|
||||
/>
|
||||
</x-admin::form.control-group>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.content.controls.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:footer>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.footer.save_button.before') !!}
|
||||
|
||||
<x-admin::button
|
||||
class="primary-button"
|
||||
:title="trans('admin::app.components.activities.actions.activity.save-btn')"
|
||||
::loading="isStoring"
|
||||
::disabled="isStoring"
|
||||
/>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.footer.save_button.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.modal.after') !!}
|
||||
</form>
|
||||
</x-admin::form>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.form_controls.after') !!}
|
||||
</Teleport>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-activity', {
|
||||
template: '#v-activity-template',
|
||||
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {}
|
||||
},
|
||||
|
||||
entityControlName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
isStoring: false,
|
||||
|
||||
selectedType: {
|
||||
label: "{{ trans('admin::app.components.activities.actions.activity.call') }}",
|
||||
value: 'call'
|
||||
},
|
||||
|
||||
availableTypes: [
|
||||
{
|
||||
label: "{{ trans('admin::app.components.activities.actions.activity.call') }}",
|
||||
value: 'call'
|
||||
}, {
|
||||
label: "{{ trans('admin::app.components.activities.actions.activity.meeting') }}",
|
||||
value: 'meeting'
|
||||
}, {
|
||||
label: "{{ trans('admin::app.components.activities.actions.activity.lunch') }}",
|
||||
value: 'lunch'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openModal(type) {
|
||||
this.$refs.activityModal.open();
|
||||
},
|
||||
|
||||
save(params) {
|
||||
this.isStoring = true;
|
||||
|
||||
this.$axios.post("{{ route('admin.activities.store') }}", params)
|
||||
.then (response => {
|
||||
this.isStoring = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$emitter.emit('on-activity-added', response.data.data);
|
||||
|
||||
this.$refs.activityModal.close();
|
||||
})
|
||||
.catch (error => {
|
||||
this.isStoring = false;
|
||||
|
||||
if (error.response.status == 422) {
|
||||
setErrors(error.response.data.errors);
|
||||
} else {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$refs.activityModal.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,245 @@
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.before') !!}
|
||||
|
||||
<!-- Participants Vue Component -->
|
||||
<v-activity-participants></v-activity-participants>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.after') !!}
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-activity-participants-template">
|
||||
<!-- Search Button -->
|
||||
<div class="relative">
|
||||
<div
|
||||
class="relative rounded border border-gray-200 px-2 py-1 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:hover:border-gray-400"
|
||||
role="button"
|
||||
>
|
||||
<ul class="flex flex-wrap items-center gap-1">
|
||||
<template v-for="userType in ['users', 'persons']">
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.user_type.before') !!}
|
||||
|
||||
<li
|
||||
class="flex items-center gap-1 rounded-md bg-slate-100 pl-2 dark:bg-gray-950 dark:text-gray-300"
|
||||
v-for="(user, index) in addedParticipants[userType]"
|
||||
>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.user_type.user.before') !!}
|
||||
|
||||
<!-- User Id -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="'participants.' + userType + '[' + index + ']'"
|
||||
::value="user.id"
|
||||
/>
|
||||
|
||||
@{{ user.name }}
|
||||
|
||||
<span
|
||||
class="icon-cross-large cursor-pointer p-0.5 text-xl"
|
||||
@click="remove(userType, user)"
|
||||
></span>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.user_type.user.after') !!}
|
||||
</li>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.user_type.after') !!}
|
||||
</template>
|
||||
|
||||
<li>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.search_term.before') !!}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="w-full px-1 py-1 dark:bg-gray-900 dark:text-gray-300"
|
||||
placeholder="@lang('admin::app.components.activities.actions.activity.participants.placeholder')"
|
||||
v-model.lazy="searchTerm"
|
||||
v-debounce="500"
|
||||
/>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.search_term.after') !!}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<template v-if="! isSearching.users && ! isSearching.persons">
|
||||
<span
|
||||
class="absolute right-1.5 top-1.5 text-2xl"
|
||||
:class="[searchTerm.length >= 2 ? 'icon-up-arrow' : 'icon-down-arrow']"
|
||||
></span>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<x-admin::spinner class="absolute right-2 top-2" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.before') !!}
|
||||
|
||||
<!-- Search Dropdown -->
|
||||
<div
|
||||
class="absolute z-10 w-full rounded bg-white shadow-[0px_10px_20px_0px_#0000001F] dark:bg-gray-900"
|
||||
v-if="searchTerm.length >= 2"
|
||||
>
|
||||
<ul class="flex flex-col gap-1 p-2">
|
||||
<!-- Users -->
|
||||
<li
|
||||
class="flex flex-col gap-2"
|
||||
v-for="userType in ['users', 'persons']"
|
||||
>
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.user_type.before') !!}
|
||||
|
||||
<h3 class="text-sm font-bold text-gray-600 dark:text-gray-300">
|
||||
<template v-if="userType === 'users'">
|
||||
@lang('admin::app.components.activities.actions.activity.participants.users')
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@lang('admin::app.components.activities.actions.activity.participants.persons')
|
||||
</template>
|
||||
</h3>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.user_type.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.no_results.before') !!}
|
||||
|
||||
<ul>
|
||||
<li
|
||||
class="rounded-sm px-5 py-2 text-sm text-gray-800 dark:text-white"
|
||||
v-if="! searchedParticipants[userType].length && ! isSearching[userType]"
|
||||
>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@lang('admin::app.components.activities.actions.activity.participants.no-results')
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="cursor-pointer rounded-sm px-3 py-2 text-sm text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-950"
|
||||
v-for="user in searchedParticipants[userType]"
|
||||
@click="add(userType, user)"
|
||||
>
|
||||
@{{ user.name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.no_results.after') !!}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.activity.participants.dropdown.after') !!}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-activity-participants', {
|
||||
template: '#v-activity-participants-template',
|
||||
|
||||
props: {
|
||||
participants: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
users: [],
|
||||
|
||||
persons: [],
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
isSearching: {
|
||||
users: false,
|
||||
|
||||
persons: false,
|
||||
},
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
addedParticipants: {
|
||||
users: [],
|
||||
|
||||
persons: [],
|
||||
},
|
||||
|
||||
searchedParticipants: {
|
||||
users: [],
|
||||
|
||||
persons: [],
|
||||
},
|
||||
|
||||
searchEnpoints: {
|
||||
users: "{{ route('admin.settings.users.search') }}",
|
||||
|
||||
persons: "{{ route('admin.contacts.persons.search') }}",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm(newVal, oldVal) {
|
||||
this.search('users');
|
||||
|
||||
this.search('persons');
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.addedParticipants = this.participants;
|
||||
},
|
||||
|
||||
methods: {
|
||||
search(userType) {
|
||||
if (this.searchTerm.length <= 1) {
|
||||
this.searchedParticipants[userType] = [];
|
||||
|
||||
this.isSearching[userType] = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching[userType] = true;
|
||||
|
||||
let self = this;
|
||||
|
||||
this.$axios.get(this.searchEnpoints[userType], {
|
||||
params: {
|
||||
search: 'name:' + this.searchTerm,
|
||||
searchFields: 'name:like',
|
||||
}
|
||||
})
|
||||
.then (function(response) {
|
||||
self.addedParticipants[userType].forEach(function(addedParticipant) {
|
||||
response.data.data = response.data.data.filter(function(participant) {
|
||||
return participant.id !== addedParticipant.id;
|
||||
});
|
||||
});
|
||||
|
||||
self.searchedParticipants[userType] = response.data.data;
|
||||
|
||||
self.isSearching[userType] = false;
|
||||
})
|
||||
.catch (function (error) {
|
||||
self.isSearching[userType] = false;
|
||||
});
|
||||
},
|
||||
|
||||
add(userType, participant) {
|
||||
this.addedParticipants[userType].push(participant);
|
||||
|
||||
this.searchTerm = '';
|
||||
|
||||
this.searchedParticipants = {
|
||||
users: [],
|
||||
|
||||
persons: [],
|
||||
};
|
||||
},
|
||||
|
||||
remove(userType, participant) {
|
||||
this.addedParticipants[userType] = this.addedParticipants[userType].filter(function(addedParticipant) {
|
||||
return addedParticipant.id !== participant.id;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,216 @@
|
||||
@props([
|
||||
'entity' => null,
|
||||
'entityControlName' => null,
|
||||
])
|
||||
|
||||
<!-- File Button -->
|
||||
<div>
|
||||
{!! view_render_event('admin.components.activities.actions.file.create_btn.before') !!}
|
||||
|
||||
<button
|
||||
class="flex h-[74px] w-[84px] flex-col items-center justify-center gap-1 rounded-lg border border-transparent bg-cyan-200 font-medium text-cyan-900 transition-all hover:border-cyan-400"
|
||||
@click="$refs.fileActionComponent.openModal('mail')"
|
||||
>
|
||||
<span class="icon-file text-2xl dark:!text-cyan-900"></span>
|
||||
|
||||
@lang('admin::app.components.activities.actions.file.btn')
|
||||
</button>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.create_btn.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.before') !!}
|
||||
|
||||
<!-- File Action Vue Component -->
|
||||
<v-file-activity
|
||||
ref="fileActionComponent"
|
||||
:entity="{{ json_encode($entity) }}"
|
||||
entity-control-name="{{ $entityControlName }}"
|
||||
></v-file-activity>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.after') !!}
|
||||
</div>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-file-activity-template">
|
||||
<Teleport to="body">
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.before') !!}
|
||||
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, save)">
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.before') !!}
|
||||
|
||||
<x-admin::modal
|
||||
ref="fileActivityModal"
|
||||
position="bottom-right"
|
||||
>
|
||||
<x-slot:header>
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.header.title.before') !!}
|
||||
|
||||
<h3 class="text-base font-semibold dark:text-white">
|
||||
@lang('admin::app.components.activities.actions.file.title')
|
||||
</h3>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.header.title.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content>
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.content.controls.before') !!}
|
||||
|
||||
<!-- Activity Type -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
name="type"
|
||||
value="file"
|
||||
/>
|
||||
|
||||
<!-- Id -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="entityControlName"
|
||||
::value="entity.id"
|
||||
/>
|
||||
|
||||
<!-- Title -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.file.title-control')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
name="title"
|
||||
/>
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Description -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.file.description')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
name="comment"
|
||||
/>
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- File Name -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.file.name')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
name="name"
|
||||
/>
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- File -->
|
||||
<x-admin::form.control-group class="!mb-0">
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.file.file')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="file"
|
||||
id="file"
|
||||
name="file"
|
||||
rules="required"
|
||||
:label="trans('admin::app.components.activities.actions.file.file')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="file" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.content.controls.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:footer>
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.footer.save_buton.before') !!}
|
||||
|
||||
<x-admin::button
|
||||
class="primary-button"
|
||||
:title="trans('admin::app.components.activities.actions.file.save-btn')"
|
||||
::loading="isStoring"
|
||||
::disabled="isStoring"
|
||||
/>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.footer.save_buton.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.modal.after') !!}
|
||||
</form>
|
||||
</x-admin::form>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.file.form_controls.after') !!}
|
||||
</Teleport>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-file-activity', {
|
||||
template: '#v-file-activity-template',
|
||||
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {}
|
||||
},
|
||||
|
||||
entityControlName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
isStoring: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openModal(type) {
|
||||
this.$refs.fileActivityModal.open();
|
||||
},
|
||||
|
||||
save(params, { setErrors }) {
|
||||
this.isStoring = true;
|
||||
|
||||
this.$axios.post("{{ route('admin.activities.store') }}", params, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
})
|
||||
.then (response => {
|
||||
this.isStoring = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$emitter.emit('on-activity-added', response.data.data);
|
||||
|
||||
this.$refs.fileActivityModal.close();
|
||||
})
|
||||
.catch (error => {
|
||||
this.isStoring = false;
|
||||
|
||||
if (error.response.status == 422) {
|
||||
setErrors(error.response.data.errors);
|
||||
} else {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$refs.fileActivityModal.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,293 @@
|
||||
@props([
|
||||
'entity' => null,
|
||||
'entityControlName' => null,
|
||||
])
|
||||
|
||||
<!-- Mail Button -->
|
||||
<div>
|
||||
{!! view_render_event('admin.components.activities.actions.mail.create_btn.before') !!}
|
||||
|
||||
<button
|
||||
class="flex h-[74px] w-[84px] flex-col items-center justify-center gap-1 rounded-lg border border-transparent bg-green-200 font-medium text-green-900 transition-all hover:border-green-400"
|
||||
@click="$refs.mailActionComponent.openModal('mail')"
|
||||
>
|
||||
<span class="icon-mail text-2xl dark:!text-green-900"></span>
|
||||
|
||||
@lang('admin::app.components.activities.actions.mail.btn')
|
||||
</button>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.create_btn.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.before') !!}
|
||||
|
||||
<!-- Mail Activity Action Vue Component -->
|
||||
<v-mail-activity
|
||||
ref="mailActionComponent"
|
||||
:entity="{{ json_encode($entity) }}"
|
||||
entity-control-name="{{ $entityControlName }}"
|
||||
></v-mail-activity>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.after') !!}
|
||||
</div>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-mail-activity-template">
|
||||
<Teleport to="body">
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.before') !!}
|
||||
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
enctype="multipart/form-data"
|
||||
as="div"
|
||||
>
|
||||
<form
|
||||
@submit="handleSubmit($event, save)"
|
||||
ref="mailActionForm"
|
||||
>
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.before') !!}
|
||||
|
||||
<x-admin::modal
|
||||
ref="mailActivityModal"
|
||||
position="bottom-right"
|
||||
>
|
||||
<x-slot:header>
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.header.before') !!}
|
||||
|
||||
<h3 class="text-base font-semibold dark:text-white">
|
||||
@lang('admin::app.components.activities.actions.mail.title')
|
||||
</h3>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.header.before') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content>
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.content.controls.before') !!}
|
||||
|
||||
<!-- Activity Type -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
name="type"
|
||||
value="email"
|
||||
/>
|
||||
|
||||
<!-- Id -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="entityControlName"
|
||||
::value="entity.id"
|
||||
/>
|
||||
|
||||
<!-- To -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.mail.to')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<div class="relative">
|
||||
<x-admin::form.control-group.control
|
||||
type="tags"
|
||||
name="reply_to"
|
||||
rules="required"
|
||||
input-rules="email"
|
||||
:label="trans('admin::app.components.activities.actions.mail.to')"
|
||||
:placeholder="trans('admin::app.components.activities.actions.mail.enter-emails')"
|
||||
/>
|
||||
|
||||
<div class="absolute top-[9px] flex items-center gap-2 ltr:right-2 rtl:left-2">
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline dark:text-white"
|
||||
@click="showCC = ! showCC"
|
||||
>
|
||||
@lang('admin::app.components.activities.actions.mail.cc')
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline dark:text-white"
|
||||
@click="showBCC = ! showBCC"
|
||||
>
|
||||
@lang('admin::app.components.activities.actions.mail.bcc')
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error control-name="reply_to" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<template v-if="showCC">
|
||||
<!-- Cc -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.mail.cc')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="tags"
|
||||
name="cc"
|
||||
input-rules="email"
|
||||
:label="trans('admin::app.components.activities.actions.mail.cc')"
|
||||
:placeholder="trans('admin::app.components.activities.actions.mail.enter-emails')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="cc" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<template v-if="showBCC">
|
||||
<!-- Cc -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label>
|
||||
@lang('admin::app.components.activities.actions.mail.bcc')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="tags"
|
||||
name="bcc"
|
||||
input-rules="email"
|
||||
:label="trans('admin::app.components.activities.actions.mail.bcc')"
|
||||
:placeholder="trans('admin::app.components.activities.actions.mail.enter-emails')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="bcc" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<!-- Subject -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.mail.subject')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
rules="required"
|
||||
:label="trans('admin::app.components.activities.actions.mail.subject')"
|
||||
:placeholder="trans('admin::app.components.activities.actions.mail.subject')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="subject" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Content -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
name="reply"
|
||||
id="reply"
|
||||
rules="required"
|
||||
{{-- tinymce="true" --}}
|
||||
:label="trans('admin::app.components.activities.actions.mail.message')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="reply" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Attachments -->
|
||||
<x-admin::form.control-group class="!mb-0">
|
||||
<x-admin::attachments
|
||||
allow-multiple="true"
|
||||
hide-button="true"
|
||||
/>
|
||||
</x-admin::form.control-group>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.content.controls.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:footer>
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.footer.save_button.before') !!}
|
||||
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<label
|
||||
class="icon-attachment cursor-pointer p-1 text-2xl hover:rounded-md hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
for="file-upload"
|
||||
></label>
|
||||
|
||||
<x-admin::button
|
||||
class="primary-button"
|
||||
:title="trans('admin::app.components.activities.actions.mail.send-btn')"
|
||||
::loading="isStoring"
|
||||
::disabled="isStoring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.footer.save_button.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.modal.after') !!}
|
||||
</form>
|
||||
</x-admin::form>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.mail.form_controls.after') !!}
|
||||
</Teleport>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-mail-activity', {
|
||||
template: '#v-mail-activity-template',
|
||||
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {}
|
||||
},
|
||||
|
||||
entityControlName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showCC: false,
|
||||
|
||||
showBCC: false,
|
||||
|
||||
isStoring: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openModal(type) {
|
||||
this.$refs.mailActivityModal.open();
|
||||
},
|
||||
|
||||
save(params, { resetForm, setErrors }) {
|
||||
this.isStoring = true;
|
||||
|
||||
let formData = new FormData(this.$refs.mailActionForm);
|
||||
|
||||
this.$axios.post("{{ route('admin.leads.emails.store', 'replaceLeadId') }}".replace('replaceLeadId', this.entity.id), formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then (response => {
|
||||
this.isStoring = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$emitter.emit('on-activity-added', response.data.data);
|
||||
|
||||
this.$refs.mailActivityModal.close();
|
||||
})
|
||||
.catch (error => {
|
||||
this.isStoring = false;
|
||||
|
||||
if (error.response.status == 422) {
|
||||
setErrors(error.response.data.errors);
|
||||
} else {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$refs.mailActivityModal.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,175 @@
|
||||
@props([
|
||||
'entity' => null,
|
||||
'entityControlName' => null,
|
||||
])
|
||||
|
||||
<!-- Note Button -->
|
||||
<div>
|
||||
{!! view_render_event('admin.components.activities.actions.note.create_btn.before') !!}
|
||||
|
||||
<button
|
||||
class="flex h-[74px] w-[84px] flex-col items-center justify-center gap-1 rounded-lg border border-transparent bg-orange-200 font-medium text-orange-800 transition-all hover:border-orange-400"
|
||||
@click="$refs.noteActionComponent.openModal('mail')"
|
||||
>
|
||||
<span class="icon-note text-2xl dark:!text-orange-800"></span>
|
||||
|
||||
@lang('admin::app.components.activities.actions.note.btn')
|
||||
</button>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.create_btn.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.before') !!}
|
||||
|
||||
<!-- Note Action Vue Component -->
|
||||
<v-note-activity
|
||||
ref="noteActionComponent"
|
||||
:entity="{{ json_encode($entity) }}"
|
||||
entity-control-name="{{ $entityControlName }}"
|
||||
></v-note-activity>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.after') !!}
|
||||
</div>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-note-activity-template">
|
||||
<Teleport to="body">
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.before') !!}
|
||||
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, save)">
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.before') !!}
|
||||
|
||||
<x-admin::modal
|
||||
ref="noteActivityModal"
|
||||
position="bottom-right"
|
||||
>
|
||||
<x-slot:header>
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.title.before') !!}
|
||||
|
||||
<h3 class="text-base font-semibold dark:text-white">
|
||||
@lang('admin::app.components.activities.actions.note.title')
|
||||
</h3>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.title.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content>
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.content.controls.before') !!}
|
||||
|
||||
<!-- Activity Type -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
name="type"
|
||||
value="note"
|
||||
/>
|
||||
|
||||
<!-- Id -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="entityControlName"
|
||||
::value="entity.id"
|
||||
/>
|
||||
|
||||
<!-- Comment -->
|
||||
<x-admin::form.control-group class="!mb-0">
|
||||
<x-admin::form.control-group.label class="required">
|
||||
@lang('admin::app.components.activities.actions.note.comment')
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
name="comment"
|
||||
rules="required"
|
||||
:label="trans('admin::app.components.activities.actions.note.comment')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error control-name="comment" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.content.controls.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:footer>
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.footer.save_button.before') !!}
|
||||
|
||||
<x-admin::button
|
||||
class="primary-button"
|
||||
:title="trans('admin::app.components.activities.actions.note.save-btn')"
|
||||
::loading="isStoring"
|
||||
::disabled="isStoring"
|
||||
/>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.header.footer.save_button.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.modal.after') !!}
|
||||
</form>
|
||||
</x-admin::form>
|
||||
|
||||
{!! view_render_event('admin.components.activities.actions.note.form_controls.after') !!}
|
||||
</Teleport>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-note-activity', {
|
||||
template: '#v-note-activity-template',
|
||||
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {}
|
||||
},
|
||||
|
||||
entityControlName: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
isStoring: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openModal(type) {
|
||||
this.$refs.noteActivityModal.open();
|
||||
},
|
||||
|
||||
save(params) {
|
||||
this.isStoring = true;
|
||||
|
||||
this.$axios.post("{{ route('admin.activities.store') }}", params)
|
||||
.then (response => {
|
||||
this.isStoring = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$emitter.emit('on-activity-added', response.data.data);
|
||||
|
||||
this.$refs.noteActivityModal.close();
|
||||
})
|
||||
.catch (error => {
|
||||
this.isStoring = false;
|
||||
|
||||
if (error.response.status == 422) {
|
||||
setErrors(error.response.data.errors);
|
||||
} else {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$refs.noteActivityModal.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,640 @@
|
||||
@props([
|
||||
'endpoint',
|
||||
'emailDetachEndpoint' => null,
|
||||
'activeType' => 'all',
|
||||
'types' => null,
|
||||
'extraTypes' => null,
|
||||
])
|
||||
|
||||
{!! view_render_event('admin.components.activities.before') !!}
|
||||
|
||||
<!-- Lead Activities Vue Component -->
|
||||
<v-activities
|
||||
endpoint="{{ $endpoint }}"
|
||||
email-detach-endpoint="{{ $emailDetachEndpoint }}"
|
||||
active-type="{{ $activeType }}"
|
||||
@if($types):types='@json($types)'@endif
|
||||
@if($extraTypes):extra-types='@json($extraTypes)'@endif
|
||||
ref="activities"
|
||||
>
|
||||
<!-- Shimmer -->
|
||||
<x-admin::shimmer.activities />
|
||||
|
||||
@foreach ($extraTypes ?? [] as $type)
|
||||
<template v-slot:{{ $type['name'] }}>
|
||||
{{ ${$type['name']} ?? '' }}
|
||||
</template>
|
||||
@endforeach
|
||||
</v-activities>
|
||||
|
||||
{!! view_render_event('admin.components.activities.after') !!}
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script type="text/x-template" id="v-activities-template">
|
||||
<template v-if="isLoading">
|
||||
<!-- Shimmer -->
|
||||
<x-admin::shimmer.activities />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{!! view_render_event('admin.components.activities.content.before') !!}
|
||||
|
||||
<div class="w-full rounded-md border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex gap-2 overflow-x-auto border-b border-gray-200 dark:border-gray-800">
|
||||
{!! view_render_event('admin.components.activities.content.types.before') !!}
|
||||
|
||||
<div
|
||||
v-for="type in types"
|
||||
class="cursor-pointer px-3 py-2.5 text-sm font-medium dark:text-white"
|
||||
:class="{'border-brandColor border-b-2 !text-brandColor transition': selectedType == type.name }"
|
||||
@click="selectedType = type.name"
|
||||
>
|
||||
@{{ type.label }}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.types.after') !!}
|
||||
</div>
|
||||
|
||||
<!-- Show Default Activities if selectedType not in extraTypes -->
|
||||
<template v-if="! extraTypes.find(type => type.name == selectedType)">
|
||||
<div class="animate-[on-fade_0.5s_ease-in-out] p-4">
|
||||
{!! view_render_event('admin.components.activities.content.activity.list.before') !!}
|
||||
|
||||
<!-- Activity List -->
|
||||
<div class="flex flex-col gap-4">
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.before') !!}
|
||||
|
||||
<!-- Activity Item -->
|
||||
<div
|
||||
class="flex gap-2"
|
||||
v-for="(activity, index) in filteredActivities"
|
||||
>
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.icon.before') !!}
|
||||
|
||||
<!-- Activity Icon -->
|
||||
<div
|
||||
class="mt-2 flex h-9 min-h-9 w-9 min-w-9 items-center justify-center rounded-full text-xl"
|
||||
:class="typeClasses[activity.type] ?? typeClasses['default']"
|
||||
>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.icon.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.details.before') !!}
|
||||
|
||||
<!-- Activity Details -->
|
||||
<div
|
||||
class="flex w-full justify-between gap-4 rounded-md p-4"
|
||||
:class="{'bg-gray-100 dark:bg-gray-950': index % 2 != 0 }"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.title.before') !!}
|
||||
|
||||
<!-- Activity Title -->
|
||||
<div
|
||||
class="flex flex-col gap-1"
|
||||
v-if="activity.title"
|
||||
>
|
||||
<p class="flex flex-wrap items-center gap-1 font-medium dark:text-white">
|
||||
@{{ activity.title }}
|
||||
|
||||
<template v-if="activity.type == 'system' && activity.additional">
|
||||
<p class="flex items-center gap-1">
|
||||
<span>:</span>
|
||||
|
||||
<span class="break-words">
|
||||
@{{ (activity.additional.old.label ? String(activity.additional.old.label).replaceAll('<br>', ' ') : "@lang('admin::app.components.activities.index.empty')") }}
|
||||
</span>
|
||||
|
||||
<span class="icon-stats-up rotate-90 text-xl"></span>
|
||||
|
||||
<span class="break-words">
|
||||
@{{ (activity.additional.new.label ? String(activity.additional.new.label).replaceAll('<br>', ' ') : "@lang('admin::app.components.activities.index.empty')") }}
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<template v-if="activity.type == 'email'">
|
||||
<p class="dark:text-white">
|
||||
@lang('admin::app.components.activities.index.from'):
|
||||
|
||||
@{{ activity.additional.from }}
|
||||
</p>
|
||||
|
||||
<p class="dark:text-white">
|
||||
@lang('admin::app.components.activities.index.to'):
|
||||
|
||||
@{{ activity.additional.to.join(', ') }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="activity.additional.cc"
|
||||
class="dark:text-white"
|
||||
>
|
||||
@lang('admin::app.components.activities.index.cc'):
|
||||
|
||||
@{{ activity.additional.cc.join(', ') }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="activity.additional.bcc"
|
||||
class="dark:text-white"
|
||||
>
|
||||
@lang('admin::app.components.activities.index.bcc'):
|
||||
|
||||
@{{ activity.additional.bcc.join(', ') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Activity Schedule -->
|
||||
<p
|
||||
v-if="activity.schedule_from && activity.schedule_from"
|
||||
class="dark:text-white"
|
||||
>
|
||||
@lang('admin::app.components.activities.index.scheduled-on'):
|
||||
|
||||
@{{ $admin.formatDate(activity.schedule_from, 'd MMM yyyy, h:mm A', timezone) + ' - ' + $admin.formatDate(activity.schedule_to, 'd MMM yyyy, h:mm A', timezone) }}
|
||||
</p>
|
||||
|
||||
<!-- Activity Participants -->
|
||||
<p
|
||||
v-if="activity.participants?.length"
|
||||
class="dark:text-white"
|
||||
>
|
||||
@lang('admin::app.components.activities.index.participants'):
|
||||
|
||||
<span class="after:content-[',_'] last:after:content-['']" v-for="(participant, index) in activity.participants">
|
||||
@{{ participant.user?.name ?? participant.person.name }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Activity Location -->
|
||||
<p
|
||||
v-if="activity.location"
|
||||
class="dark:text-white"
|
||||
>
|
||||
@lang('admin::app.components.activities.index.location'):
|
||||
|
||||
@{{ activity.location }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.title.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.description.before') !!}
|
||||
|
||||
<!-- Activity Description -->
|
||||
<p
|
||||
class="dark:text-white"
|
||||
v-if="activity.comment"
|
||||
v-safe-html="activity.comment"
|
||||
></p>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.description.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.attachments.before') !!}
|
||||
|
||||
<!-- Attachments -->
|
||||
<div
|
||||
class="flex flex-wrap gap-2"
|
||||
v-if="activity.files.length"
|
||||
>
|
||||
<a
|
||||
:href="
|
||||
activity.type == 'email'
|
||||
? `{{ route('admin.mail.attachment_download', 'replaceID') }}`.replace('replaceID', file.id)
|
||||
: `{{ route('admin.activities.file_download', 'replaceID') }}`.replace('replaceID', file.id)
|
||||
"
|
||||
class="flex cursor-pointer items-center gap-1 rounded-md p-1.5"
|
||||
target="_blank"
|
||||
v-for="(file, index) in activity.files"
|
||||
>
|
||||
<span class="icon-attached-file text-xl"></span>
|
||||
|
||||
<span class="font-medium text-brandColor">
|
||||
@{{ file.name }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.attachments.after') !!}
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.time_and_user.before') !!}
|
||||
|
||||
<!-- Activity Time and User -->
|
||||
<div class="text-gray-500 dark:text-gray-300">
|
||||
@{{ $admin.formatDate(activity.created_at, 'd MMM yyyy, h:mm A', timezone) }},
|
||||
|
||||
@{{ "@lang('admin::app.components.activities.index.by-user', ['user' => 'replace'])".replace('replace', activity.user?.name ?? '@lang('admin::app.components.activities.index.system')') }}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.time_and_user.after') !!}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.before') !!}
|
||||
|
||||
<!-- Activity More Options -->
|
||||
<template v-if="activity.type != 'system'">
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.after') !!}
|
||||
|
||||
<x-admin::dropdown position="bottom-{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'left' : 'right' }}">
|
||||
<x-slot:toggle>
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.toggle.before') !!}
|
||||
|
||||
<template v-if="! isUpdating[activity.id]">
|
||||
<button
|
||||
class="icon-more flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
></button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<x-admin::spinner />
|
||||
</template>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.toggle.after') !!}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:menu class="!min-w-40">
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.menu_item.before') !!}
|
||||
|
||||
<template v-if="activity.type != 'email'">
|
||||
@if (bouncer()->hasPermission('activities.edit'))
|
||||
<x-admin::dropdown.menu.item
|
||||
v-if="! activity.is_done && ['call', 'meeting', 'lunch'].includes(activity.type)"
|
||||
@click="markAsDone(activity)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-tick text-2xl"></span>
|
||||
|
||||
@lang('admin::app.components.activities.index.mark-as-done')
|
||||
</div>
|
||||
</x-admin::dropdown.menu.item>
|
||||
|
||||
<x-admin::dropdown.menu.item v-if="['call', 'meeting', 'lunch'].includes(activity.type)">
|
||||
<a
|
||||
class="flex items-center gap-2"
|
||||
:href="'{{ route('admin.activities.edit', 'replaceId') }}'.replace('replaceId', activity.id)"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="icon-edit text-2xl"></span>
|
||||
|
||||
@lang('admin::app.components.activities.index.edit')
|
||||
</a>
|
||||
</x-admin::dropdown.menu.item>
|
||||
@endif
|
||||
|
||||
@if (bouncer()->hasPermission('activities.delete'))
|
||||
<x-admin::dropdown.menu.item @click="remove(activity)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-delete text-2xl"></span>
|
||||
|
||||
@lang('admin::app.components.activities.index.delete')
|
||||
</div>
|
||||
</x-admin::dropdown.menu.item>
|
||||
@endif
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@if (bouncer()->hasPermission('mail.view'))
|
||||
<x-admin::dropdown.menu.item>
|
||||
<a
|
||||
:href="'{{ route('admin.mail.view', ['route' => 'replaceFolder', 'id' => 'replaceMailId']) }}'.replace('replaceFolder', activity.additional.folders[0]).replace('replaceMailId', activity.id)"
|
||||
class="flex items-center gap-2"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="icon-eye text-2xl"></span>
|
||||
|
||||
@lang('admin::app.components.activities.index.view')
|
||||
</a>
|
||||
</x-admin::dropdown.menu.item>
|
||||
@endif
|
||||
|
||||
<x-admin::dropdown.menu.item @click="unlinkEmail(activity)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-attachment text-2xl"></span>
|
||||
|
||||
@lang('admin::app.components.activities.index.unlink')
|
||||
</div>
|
||||
</x-admin::dropdown.menu.item>
|
||||
</template>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.menu_item.after') !!}
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.dropdown.after') !!}
|
||||
</template>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.more_actions.after') !!}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.details.after') !!}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.item.after') !!}
|
||||
|
||||
<!-- Empty Placeholder -->
|
||||
<div
|
||||
class="grid justify-center justify-items-center gap-3.5 py-12"
|
||||
v-if="! filteredActivities.length"
|
||||
>
|
||||
<img
|
||||
class="dark:mix-blend-exclusion dark:invert"
|
||||
:src="typeIllustrations[selectedType]?.image ?? typeIllustrations['all'].image"
|
||||
>
|
||||
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<p class="text-xl font-semibold dark:text-white">
|
||||
@{{ typeIllustrations[selectedType]?.title ?? typeIllustrations['all'].title }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-400 dark:text-gray-400">
|
||||
@{{ typeIllustrations[selectedType]?.description ?? typeIllustrations['all'].description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.list.after') !!}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-for="type in extraTypes">
|
||||
{!! view_render_event('admin.components.activities.content.activity.extra_types.before') !!}
|
||||
|
||||
<div v-show="selectedType == type.name">
|
||||
<slot :name="type.name"></slot>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.activity.extra_types.after') !!}
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.components.activities.content.after') !!}
|
||||
</template>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-activities', {
|
||||
template: '#v-activities-template',
|
||||
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
emailDetachEndpoint: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
activeType: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
},
|
||||
|
||||
types: {
|
||||
type: Array,
|
||||
default: [
|
||||
{
|
||||
name: 'all',
|
||||
label: "{{ trans('admin::app.components.activities.index.all') }}",
|
||||
}, {
|
||||
name: 'planned',
|
||||
label: "{{ trans('admin::app.components.activities.index.planned') }}",
|
||||
}, {
|
||||
name: 'note',
|
||||
label: "{{ trans('admin::app.components.activities.index.notes') }}",
|
||||
}, {
|
||||
name: 'call',
|
||||
label: "{{ trans('admin::app.components.activities.index.calls') }}",
|
||||
}, {
|
||||
name: 'meeting',
|
||||
label: "{{ trans('admin::app.components.activities.index.meetings') }}",
|
||||
}, {
|
||||
name: 'lunch',
|
||||
label: "{{ trans('admin::app.components.activities.index.lunches') }}",
|
||||
}, {
|
||||
name: 'file',
|
||||
label: "{{ trans('admin::app.components.activities.index.files') }}",
|
||||
}, {
|
||||
name: 'email',
|
||||
label: "{{ trans('admin::app.components.activities.index.emails') }}",
|
||||
}, {
|
||||
name: 'system',
|
||||
label: "{{ trans('admin::app.components.activities.index.change-log') }}",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
extraTypes: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
|
||||
isUpdating: {},
|
||||
|
||||
activities: [],
|
||||
|
||||
selectedType: this.activeType,
|
||||
|
||||
typeClasses: {
|
||||
email: 'icon-mail bg-green-200 text-green-900 dark:!text-green-900',
|
||||
note: 'icon-note bg-orange-200 text-orange-800 dark:!text-orange-800',
|
||||
call: 'icon-call bg-cyan-200 text-cyan-800 dark:!text-cyan-800',
|
||||
meeting: 'icon-activity bg-blue-200 text-blue-800 dark:!text-blue-800',
|
||||
lunch: 'icon-activity bg-blue-200 text-blue-800 dark:!text-blue-800',
|
||||
file: 'icon-file bg-green-200 text-green-900 dark:!text-green-900',
|
||||
system: 'icon-system-generate bg-yellow-200 text-yellow-900 dark:!text-yellow-900',
|
||||
default: 'icon-activity bg-blue-200 text-blue-800 dark:!text-blue-800',
|
||||
},
|
||||
|
||||
typeIllustrations: {
|
||||
all: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/activities.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.all.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.all.description') }}",
|
||||
},
|
||||
|
||||
planned: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/plans.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.planned.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.planned.description') }}",
|
||||
},
|
||||
|
||||
note: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/notes.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.notes.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.notes.description') }}",
|
||||
},
|
||||
|
||||
call: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/calls.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.calls.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.calls.description') }}",
|
||||
},
|
||||
|
||||
meeting: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/meetings.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.meetings.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.meetings.description') }}",
|
||||
},
|
||||
|
||||
lunch: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/lunches.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.lunches.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.lunches.description') }}",
|
||||
},
|
||||
|
||||
file: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/files.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.files.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.files.description') }}",
|
||||
},
|
||||
|
||||
email: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/emails.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.emails.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.emails.description') }}",
|
||||
},
|
||||
|
||||
system: {
|
||||
image: "{{ vite()->asset('images/empty-placeholders/activities.svg') }}",
|
||||
title: "{{ trans('admin::app.components.activities.index.empty-placeholders.system.title') }}",
|
||||
description: "{{ trans('admin::app.components.activities.index.empty-placeholders.system.description') }}",
|
||||
}
|
||||
},
|
||||
|
||||
timezone: "{{ config('app.timezone') }}",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredActivities() {
|
||||
if (this.selectedType == 'all') {
|
||||
return this.activities;
|
||||
} else if (this.selectedType == 'planned') {
|
||||
return this.activities.filter(activity => ! activity.is_done);
|
||||
}
|
||||
|
||||
return this.activities.filter(activity => activity.type == this.selectedType);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.get();
|
||||
|
||||
if (this.extraTypes?.length) {
|
||||
this.extraTypes.forEach(type => {
|
||||
this.types.push(type);
|
||||
});
|
||||
}
|
||||
|
||||
this.$emitter.on('on-activity-added', (activity) => this.activities.unshift(activity));
|
||||
},
|
||||
|
||||
methods: {
|
||||
get() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.$axios.get(this.endpoint)
|
||||
.then(response => {
|
||||
this.activities = response.data.data;
|
||||
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
|
||||
markAsDone(activity) {
|
||||
this.$emitter.emit('open-confirm-modal', {
|
||||
agree: () => {
|
||||
this.isUpdating[activity.id] = true;
|
||||
|
||||
this.$axios.put("{{ route('admin.activities.update', 'replaceId') }}".replace('replaceId', activity.id), {
|
||||
'is_done': 1
|
||||
})
|
||||
.then((response) => {
|
||||
this.isUpdating[activity.id] = false;
|
||||
|
||||
activity.is_done = 1;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isUpdating[activity.id] = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
remove(activity) {
|
||||
this.$emitter.emit('open-confirm-modal', {
|
||||
agree: () => {
|
||||
this.isUpdating[activity.id] = true;
|
||||
|
||||
this.$axios.delete("{{ route('admin.activities.delete', 'replaceId') }}".replace('replaceId', activity.id))
|
||||
.then((response) => {
|
||||
this.isUpdating[activity.id] = false;
|
||||
|
||||
this.activities.splice(this.activities.indexOf(activity), 1);
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isUpdating[activity.id] = false;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
unlinkEmail(activity) {
|
||||
this.$emitter.emit('open-confirm-modal', {
|
||||
agree: () => {
|
||||
let emailId = activity.parent_id ?? activity.id;
|
||||
|
||||
this.$axios.delete(this.emailDetachEndpoint, {
|
||||
data: {
|
||||
email_id: emailId,
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
let relatedActivities = this.activities.filter(activity => activity.parent_id == emailId || activity.id == emailId);
|
||||
|
||||
relatedActivities.forEach(activity => {
|
||||
const index = this.activities.findIndex(a => a === activity);
|
||||
|
||||
if (index !== -1) {
|
||||
this.activities.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,184 @@
|
||||
@props([
|
||||
'name' => 'attachments',
|
||||
'validations' => null,
|
||||
'uploadedAttachments' => [],
|
||||
'allowMultiple' => false,
|
||||
'hideButton' => false,
|
||||
])
|
||||
|
||||
<v-attachments
|
||||
name="{{ $name }}"
|
||||
validations="{{ $validations }}"
|
||||
:uploaded-attachments='{{ json_encode($uploadedAttachments) }}'
|
||||
:allow-multiple="{{ $allowMultiple }}"
|
||||
:hide-button="{{ $hideButton }}"
|
||||
>
|
||||
</v-attachments>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-attachments-template"
|
||||
>
|
||||
<!-- File Attachment Input -->
|
||||
<div
|
||||
class="relative items-center"
|
||||
v-show="! hideButton"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
id="file-upload"
|
||||
accept="attachment/*"
|
||||
:multiple="allowMultiple"
|
||||
:ref="$.uid + '_attachmentInput'"
|
||||
@change="add"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-1"
|
||||
for="file-upload"
|
||||
>
|
||||
<i class="icon-attachment text-xl font-medium"></i>
|
||||
|
||||
<span class="font-semibold">
|
||||
@lang('Add Attachments')
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Uploaded attachments -->
|
||||
<div
|
||||
v-if="attachments?.length"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<template v-for="(attachment, index) in attachments">
|
||||
<v-attachment-item
|
||||
:name="name"
|
||||
:index="index"
|
||||
:attachment="attachment"
|
||||
@onRemove="remove($event)"
|
||||
>
|
||||
</v-attachment-item>
|
||||
</template>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="v-attachment-item-template">
|
||||
<div class="flex items-center gap-2 rounded-md bg-gray-100 px-2.5 py-1 dark:bg-gray-950">
|
||||
<span class="max-w-xs truncate dark:text-white">
|
||||
@{{ attachment.name }}
|
||||
</span>
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="file"
|
||||
::name="name + '[]'"
|
||||
class="hidden"
|
||||
::ref="$.uid + '_attachmentInput_' + index"
|
||||
/>
|
||||
|
||||
<i
|
||||
class="icon-cross-large cursor-pointer rounded-md p-0.5 text-xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
@click="remove"
|
||||
></i>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-attachments', {
|
||||
template: '#v-attachments-template',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: 'attachments',
|
||||
},
|
||||
|
||||
validations: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
uploadedAttachments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
allowMultiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
hideButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
attachments: [],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.attachments = this.uploadedAttachments;
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
let attachmentInput = this.$refs[this.$.uid + '_attachmentInput'];
|
||||
|
||||
if (attachmentInput.files == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
attachmentInput.files.forEach((file, index) => {
|
||||
this.attachments.push({
|
||||
id: 'attachment_' + this.attachments.length,
|
||||
name: file.name,
|
||||
file: file
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
remove(attachment) {
|
||||
let index = this.attachments.indexOf(attachment);
|
||||
|
||||
this.attachments.splice(index, 1);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
app.component('v-attachment-item', {
|
||||
template: '#v-attachment-item-template',
|
||||
|
||||
props: ['index', 'attachment', 'name'],
|
||||
|
||||
mounted() {
|
||||
if (this.attachment.file instanceof File) {
|
||||
this.setFile(this.attachment.file);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove() {
|
||||
this.$emit('onRemove', this.attachment)
|
||||
},
|
||||
|
||||
setFile(file) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
this.$refs[this.$.uid + '_attachmentInput_' + this.index].files = dataTransfer.files;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,165 @@
|
||||
@if (isset($attribute))
|
||||
<v-address-component
|
||||
:attribute='@json($attribute)'
|
||||
:validations="'{{ $validations }}'"
|
||||
:data='@json(old($attribute->code) ?: $value)'
|
||||
>
|
||||
<!-- Addresses Shimmer -->
|
||||
<x-admin::shimmer.common.address />
|
||||
</v-address-component>
|
||||
@endif
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-address-component-template"
|
||||
>
|
||||
<div class="flex gap-4 max-md:flex-wrap">
|
||||
<div class="w-full">
|
||||
<!-- Address (Textarea field) -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
::name="attribute['code'] + '[address]'"
|
||||
rows="10"
|
||||
::value="data ? data['address'] : ''"
|
||||
:label="trans('admin::app.common.custom-attributes.address')"
|
||||
::rules="attribute.is_required ? 'required|' + validations : validations"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[address]'" />
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.address'" />
|
||||
</x-admin::form.control-group>
|
||||
</div>
|
||||
|
||||
<div class="grid w-full">
|
||||
<!-- Country Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::name="attribute['code'] + '[country]'"
|
||||
::rules="attribute.is_required ? 'required|' + validations : validations"
|
||||
:label="trans('admin::app.common.custom-attributes.country')"
|
||||
v-model="country"
|
||||
>
|
||||
<option value="">@lang('admin::app.common.custom-attributes.select-country')</option>
|
||||
|
||||
@foreach (core()->countries() as $country)
|
||||
<option value="{{ $country->code }}">{{ $country->name }}</option>
|
||||
@endforeach
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[country]'" />
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.country'" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- State Field -->
|
||||
<template v-if="haveStates()">
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::name="attribute['code'] + '[state]'"
|
||||
v-model="state"
|
||||
:label="trans('admin::app.common.custom-attributes.state')"
|
||||
::rules="attribute.is_required ? 'required|' + validations : validations"
|
||||
>
|
||||
<option value="">@lang('admin::app.common.custom-attributes.select-state')</option>
|
||||
|
||||
<option
|
||||
v-for='(state, index) in countryStates[country]'
|
||||
:value="state.code"
|
||||
>
|
||||
@{{ state.name }}
|
||||
</option>
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[state]'" />
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.state'" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="attribute['code'] + '[state]'"
|
||||
:placeholder="trans('admin::app.common.custom-attributes.state')"
|
||||
:label="trans('admin::app.common.custom-attributes.state')"
|
||||
::rules="attribute.is_required ? 'required|' + validations : validations"
|
||||
v-model="state"
|
||||
>
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[state]'" />
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.state'" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<!-- City Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="attribute['code'] + '[city]'"
|
||||
::value="data && data['city'] ? data['city'] : ''"
|
||||
:placeholder="trans('admin::app.common.custom-attributes.city')"
|
||||
:label="trans('admin::app.common.custom-attributes.city')"
|
||||
::rules="attribute.is_required ? 'required|' + validations : validations"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[city]'"/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.city'" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Postcode Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="attribute['code'] + '[postcode]'"
|
||||
::value="data && data['postcode'] ? data['postcode'] : ''"
|
||||
:placeholder="trans('admin::app.common.custom-attributes.postcode')"
|
||||
:label="trans('admin::app.common.custom-attributes.postcode')"
|
||||
::rules="attribute.is_required ? 'required|postcode' : 'postcode'"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '[postcode]'" />
|
||||
|
||||
<x-admin::form.control-group.error ::name="attribute['code'] + '.postcode'" />
|
||||
</x-admin::form.control-group>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-address-component', {
|
||||
template: '#v-address-component-template',
|
||||
|
||||
props: ['attribute', 'data', 'validations'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
country: this.data?.country || '',
|
||||
|
||||
state: this.data?.state || '',
|
||||
|
||||
countryStates: @json(core()->groupedStatesByCountries()),
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
haveStates() {
|
||||
/*
|
||||
* The double negation operator is used to convert the value to a boolean.
|
||||
* It ensures that the final result is a boolean value,
|
||||
* true if the array has a length greater than 0, and otherwise false.
|
||||
*/
|
||||
return !!this.countryStates[this.country]?.length;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php $selectedOption = old($attribute->code) ?: $value ?>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ $attribute->code }}"
|
||||
value="0"
|
||||
>
|
||||
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="{{ $attribute->code }}"
|
||||
value="1"
|
||||
id="{{ $attribute->code }}"
|
||||
class="peer sr-only"
|
||||
{{ $selectedOption ? 'checked' : '' }}
|
||||
>
|
||||
|
||||
<div class="peer h-5 w-9 cursor-pointer rounded-full bg-gray-200 after:absolute after:top-0.5 after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-brandColor peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-blue-300 dark:bg-gray-800 dark:after:border-white dark:after:bg-white dark:peer-checked:bg-gray-950 after:ltr:left-0.5 peer-checked:after:ltr:translate-x-full after:rtl:right-0.5 peer-checked:after:rtl:-translate-x-full"></div>
|
||||
</label>
|
||||
@@ -0,0 +1,32 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
|
||||
$selectedOption = old($attribute->code, $value);
|
||||
|
||||
$selectedOption = is_array($selectedOption) ? $selectedOption : explode(',', $selectedOption);
|
||||
@endphp
|
||||
|
||||
<input type="hidden" name="{{ $attribute->code }}" />
|
||||
|
||||
@foreach ($options as $option)
|
||||
<x-admin::form.control-group class="!mb-2 flex items-center gap-2.5">
|
||||
<x-admin::form.control-group.control
|
||||
type="checkbox"
|
||||
:id="$option->id"
|
||||
name="{{ $attribute->code }}[]"
|
||||
:value="$option->id"
|
||||
:for="$option->id"
|
||||
:label="$option->name"
|
||||
:checked="in_array($option->id, $selectedOption)"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="cursor-pointer text-xs font-medium text-gray-600 dark:text-gray-300"
|
||||
for="{{ $option->id }}"
|
||||
>
|
||||
{{ $option->name }}
|
||||
</label>
|
||||
</x-admin::form.control-group>
|
||||
@endforeach
|
||||
@@ -0,0 +1,18 @@
|
||||
@php
|
||||
if (! empty($value)) {
|
||||
if ($value instanceof \Carbon\Carbon) {
|
||||
$value = $value->format('Y-m-d');
|
||||
} elseif (is_string($value)) {
|
||||
$value = \Carbon\Carbon::parse($value)->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="date"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
:value="$value"
|
||||
:rules="$validations.'|regex:^\d{4}-\d{2}-\d{2}$'"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-admin::form.control-group.control
|
||||
type="datetime"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
:value="old($attribute->code) ?? $value"
|
||||
:rules="$validations.'|regex:^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
@@ -0,0 +1,179 @@
|
||||
@if (isset($attribute))
|
||||
<v-email-component
|
||||
:attribute="{{ json_encode($attribute) }}"
|
||||
:validations="'{{ $validations }}'"
|
||||
:value="{{ json_encode(old($attribute->code) ?? $value) }}"
|
||||
>
|
||||
<div class="mb-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded rounded-r-none border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
>
|
||||
|
||||
<div class="relative">
|
||||
<select class="custom-select w-full rounded rounded-l-none border bg-white px-2.5 py-2 text-sm font-normal text-gray-800 hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 ltr:mr-6 ltr:pr-8 rtl:ml-6 rtl:pl-8">
|
||||
<option value="work" selected>@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex cursor-pointer items-center gap-2 text-brandColor">
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</span>
|
||||
</v-email-component>
|
||||
@endif
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-email-component-template"
|
||||
>
|
||||
<template v-for="(email, index) in emails">
|
||||
<div class="mb-2 flex items-center">
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="attribute.code"
|
||||
::name="`${attribute['code']}[${index}][value]`"
|
||||
class="rounded-r-none"
|
||||
::rules="getValidation"
|
||||
::label="attribute.name"
|
||||
v-model="email['value']"
|
||||
::disabled="isDisabled"
|
||||
/>
|
||||
|
||||
<div class="relative">
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="attribute.code"
|
||||
::name="`${attribute['code']}[${index}][label]`"
|
||||
class="rounded-l-none ltr:mr-6 ltr:pr-8 rtl:ml-6 rtl:pl-8"
|
||||
rules="required"
|
||||
::label="attribute.name"
|
||||
v-model="email['label']"
|
||||
::disabled="isDisabled"
|
||||
>
|
||||
<option value="work">@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</x-admin::form.control-group.control>
|
||||
</div>
|
||||
|
||||
<i
|
||||
v-if="emails.length > 1"
|
||||
class="icon-delete ml-1 cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="remove(email)"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${attribute['code']}[${index}][value]`"/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${attribute['code']}[${index}].value`"/>
|
||||
</template>
|
||||
|
||||
<span
|
||||
class="flex w-fit cursor-pointer items-center gap-2 text-brandColor"
|
||||
@click="add"
|
||||
v-if="! isDisabled"
|
||||
>
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-email-component', {
|
||||
template: '#v-email-component-template',
|
||||
|
||||
props: ['validations', 'isDisabled', 'attribute', 'value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
emails: this.value || [{'value': '', 'label': 'work'}],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (
|
||||
JSON.stringify(newValue)
|
||||
!== JSON.stringify(oldValue)
|
||||
) {
|
||||
this.emails = newValue || [{'value': '', 'label': 'work'}];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
getValidation() {
|
||||
return {
|
||||
email: true,
|
||||
unique_email: this.emails ?? [],
|
||||
...(this.validations === 'required' ? { required: true } : {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.extendValidations();
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.emails.push({
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
});
|
||||
},
|
||||
|
||||
remove(email) {
|
||||
this.emails = this.emails.filter(item => item !== email);
|
||||
},
|
||||
|
||||
extendValidations() {
|
||||
defineRule('unique_email', async (value, emails) => {
|
||||
if (! value || ! value.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const foundEmails = emails.filter(email => email.value === value).length;
|
||||
|
||||
if (foundEmails > 1) {
|
||||
return 'This email is already in use.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the email is unique. This support is only for person emails only.
|
||||
*/
|
||||
if (this.attribute.code === 'person[emails]') {
|
||||
try {
|
||||
const { data } = await this.$axios.get('{{ route('admin.settings.attributes.check_unique_validation') }}', {
|
||||
params: {
|
||||
entity_id: this.attribute.id,
|
||||
entity_type: 'persons',
|
||||
attribute_code: 'emails',
|
||||
attribute_value: value
|
||||
}
|
||||
});
|
||||
|
||||
if (! data.validated) {
|
||||
return 'This email is already in use.';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking email: ', error);
|
||||
|
||||
return 'Error validating email. Please try again.';
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="flex items-center gap-2">
|
||||
@if ($value)
|
||||
<a
|
||||
href="{{ route('admin.settings.attributes.download', ['path' => $value]) }}"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="w-full max-w-max cursor-pointer rounded-md p-1.5 text-gray-600 hover:bg-gray-200 active:border-gray-300 dark:text-gray-300 dark:hover:bg-gray-800">
|
||||
<i class="icon-download text-2xl"></i>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="file"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
:rules="$validations"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if ($value)
|
||||
<div class="flex cursor-pointer items-center gap-2.5">
|
||||
<x-admin::form.control-group.control
|
||||
type="checkbox"
|
||||
name="{{ $attribute->code }}[delete]"
|
||||
id="{{ $attribute->code }}[delete]"
|
||||
for="{{ $attribute->code }}[delete]"
|
||||
value="1"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="cursor-pointer !text-gray-600 dark:!text-gray-300"
|
||||
for="{{ $attribute->code }}[delete]"
|
||||
>
|
||||
@lang('admin::app.components.attributes.edit.delete')
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="flex items-center gap-2">
|
||||
@if ($value)
|
||||
<a
|
||||
href="{{ route('admin.settings.attributes.download', ['path' => $value]) }}"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="{{ Storage::url($value) }}"
|
||||
alt="{{ $attribute->code }}"
|
||||
class="top-15 rounded-3 border-3 relative h-[33px] w-[33px] border-gray-500"
|
||||
/>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="file"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
class="!w-full"
|
||||
:rules="$validations"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if ($value)
|
||||
<div class="flex cursor-pointer items-center gap-2.5">
|
||||
<x-admin::form.control-group.control
|
||||
type="checkbox"
|
||||
name="{{ $attribute->code }}[delete]"
|
||||
id="{{ $attribute->code }}[delete]"
|
||||
for="{{ $attribute->code }}[delete]"
|
||||
value="1"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="cursor-pointer !text-gray-600 dark:!text-gray-300"
|
||||
for="{{ $attribute->code }}[delete]"
|
||||
>
|
||||
@lang('admin::app.components.attributes.edit.delete')
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,143 @@
|
||||
@props([
|
||||
'attribute' => '',
|
||||
'value' => '',
|
||||
'validations' => '',
|
||||
])
|
||||
|
||||
@switch($attribute->type)
|
||||
@case('text')
|
||||
<x-admin::attributes.edit.text
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('email')
|
||||
<x-admin::attributes.edit.email
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('phone')
|
||||
<x-admin::attributes.edit.phone
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('lookup')
|
||||
<x-admin::attributes.edit.lookup
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
can-add-new="true"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('select')
|
||||
<x-admin::attributes.edit.select
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('multiselect')
|
||||
<x-admin::attributes.edit.multiselect
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('price')
|
||||
<x-admin::attributes.edit.price
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('image')
|
||||
<x-admin::attributes.edit.image
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('file')
|
||||
<x-admin::attributes.edit.file
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('textarea')
|
||||
<x-admin::attributes.edit.textarea
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('address')
|
||||
<x-admin::attributes.edit.address
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('date')
|
||||
<x-admin::attributes.edit.date
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('datetime')
|
||||
<x-admin::attributes.edit.datetime
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('boolean')
|
||||
<x-admin::attributes.edit.boolean
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
<x-admin::attributes.edit.checkbox
|
||||
:attribute="$attribute"
|
||||
:value="$value"
|
||||
:validations="$validations"
|
||||
/>
|
||||
|
||||
@break
|
||||
@endswitch
|
||||
@@ -0,0 +1,290 @@
|
||||
@if (isset($attribute))
|
||||
@php
|
||||
$lookUpEntityData = app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpEntity($attribute->lookup_type, old($attribute->code) ?: $value);
|
||||
@endphp
|
||||
|
||||
<v-lookup-component
|
||||
:attribute="{{ json_encode($attribute) }}"
|
||||
:validations="'{{ $validations }}'"
|
||||
:value="{{ json_encode($lookUpEntityData)}}"
|
||||
can-add-new="{{ $canAddNew ?? false }}"
|
||||
@lookup-added="handleLookupAdded"
|
||||
@lookup-removed="handleLookupRemoved"
|
||||
>
|
||||
<div class="relative inline-block w-full">
|
||||
<!-- Input Container -->
|
||||
<div class="relative flex items-center justify-between rounded border border-gray-200 p-2 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:text-gray-300">
|
||||
@lang('admin::app.components.attributes.lookup.click-to-add')
|
||||
|
||||
<!-- Icons Container -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Arrow Icon -->
|
||||
<i class="icon-down-arrow text-2xl text-gray-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-lookup-component>
|
||||
@endif
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-lookup-component-template"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
ref="lookup"
|
||||
>
|
||||
<div
|
||||
class="relative inline-block w-full"
|
||||
@click="toggle"
|
||||
>
|
||||
<!-- Input Container -->
|
||||
<div
|
||||
class="relative flex items-center justify-between rounded border border-gray-200 p-2 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:text-gray-300"
|
||||
:class="{
|
||||
'bg-gray-50': isDisabled,
|
||||
}"
|
||||
>
|
||||
<!-- Selected Item or Placeholder Text -->
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis"
|
||||
:title="selectedItem?.name"
|
||||
>
|
||||
@{{ selectedItem?.name !== "" ? selectedItem?.name : "@lang('admin::app.components.attributes.lookup.click-to-add')" }}
|
||||
</span>
|
||||
|
||||
<!-- Icons Container -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Close Icon -->
|
||||
<i
|
||||
v-if="
|
||||
! isDisabled
|
||||
&& (
|
||||
selectedItem?.name
|
||||
&& ! isSearching
|
||||
)"
|
||||
class="icon-cross-large cursor-pointer text-2xl text-gray-600"
|
||||
@click="remove"
|
||||
></i>
|
||||
|
||||
<!-- Arrow Icon -->
|
||||
<i
|
||||
class="text-2xl text-gray-600"
|
||||
:class="showPopup ? 'icon-up-arrow' : 'icon-down-arrow'"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Input Entity Value -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="attribute['code']"
|
||||
v-model="selectedItem.id"
|
||||
::rules="validations"
|
||||
::label="attribute['name']"
|
||||
/>
|
||||
|
||||
<!-- Popup Box -->
|
||||
<div
|
||||
v-if="showPopup"
|
||||
class="absolute top-full z-10 mt-1 flex w-full origin-top transform flex-col gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg transition-transform dark:border-gray-900 dark:bg-gray-800"
|
||||
>
|
||||
<!-- Search Bar -->
|
||||
<div class="relative flex items-center">
|
||||
<!-- Input Box -->
|
||||
<input
|
||||
type="text"
|
||||
v-model.lazy="searchTerm"
|
||||
v-debounce="500"
|
||||
class="w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
placeholder="@lang('admin::app.components.attributes.lookup.search')"
|
||||
ref="searchInput"
|
||||
@keyup="search"
|
||||
/>
|
||||
|
||||
<!-- Search Icon (absolute positioned) -->
|
||||
<span class="absolute flex items-center ltr:right-2 rtl:left-2">
|
||||
<!-- Loader (optional, based on condition) -->
|
||||
<div
|
||||
class="relative"
|
||||
v-if="isSearching"
|
||||
>
|
||||
<x-admin::spinner />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<ul class="max-h-40 divide-y divide-gray-100 overflow-y-auto">
|
||||
<li
|
||||
v-for="item in filteredResults"
|
||||
:key="item.id"
|
||||
class="flex cursor-pointer gap-2 p-2 transition-colors hover:bg-blue-100 dark:text-gray-300 dark:hover:bg-gray-900"
|
||||
@click="handleResult(item)"
|
||||
>
|
||||
<!-- Entity Name -->
|
||||
<span>@{{ item.name }}</span>
|
||||
</li>
|
||||
|
||||
<template v-if="filteredResults.length === 0">
|
||||
<li class="px-4 py-2 text-center text-gray-500">
|
||||
@lang('admin::app.components.attributes.lookup.no-result-found')
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="searchTerm.length > 2 && canAddNew"
|
||||
@click="handleResult({ id: '', name: searchTerm })"
|
||||
class="cursor-pointer border-t border-gray-800 px-4 py-2 text-gray-500 hover:bg-brandColor hover:text-white dark:border-gray-300 dark:text-gray-400 dark:hover:bg-gray-900 dark:hover:text-white"
|
||||
>
|
||||
<i class="icon-add text-md"></i>
|
||||
|
||||
@lang('admin::app.components.lookup.add-as-new')
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-lookup-component', {
|
||||
template: '#v-lookup-component-template',
|
||||
|
||||
props: ['validations', 'isDisabled', 'attribute', 'value', 'canAddNew'],
|
||||
|
||||
emits: ['lookup-added', 'lookup-removed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
searchedResults: [],
|
||||
|
||||
selectedItem: {
|
||||
id: '',
|
||||
name: ''
|
||||
},
|
||||
|
||||
searchRoute: `{{ route('admin.settings.attributes.lookup') }}/${this.attribute.lookup_type}`,
|
||||
|
||||
lookupEntityRoute: `{{ route('admin.settings.attributes.lookup_entity') }}/${this.attribute.lookup_type}`,
|
||||
|
||||
isSearching: false,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.value) {
|
||||
this.getLookUpEntity();
|
||||
}
|
||||
|
||||
window.addEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm(newVal, oldVal) {
|
||||
this.search();
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Filter the searchedResults based on the search query.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
filteredResults() {
|
||||
return this.searchedResults.filter(item =>
|
||||
item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
if (this.isDisabled) {
|
||||
this.showPopup = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPopup = ! this.showPopup;
|
||||
|
||||
if (this.showPopup) {
|
||||
this.$nextTick(() => this.$refs.searchInput.focus());
|
||||
}
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.searchTerm.length <= 2) {
|
||||
this.searchedResults = [];
|
||||
|
||||
this.isSearching = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
|
||||
this.$axios.get(this.searchRoute, {
|
||||
params: { query: this.searchTerm }
|
||||
})
|
||||
.then (response => {
|
||||
this.searchedResults = response.data;
|
||||
})
|
||||
.catch (error => {})
|
||||
.finally(() => this.isSearching = false);
|
||||
},
|
||||
|
||||
getLookUpEntity() {
|
||||
this.$axios.get(this.lookupEntityRoute, {
|
||||
params: { query: this.value?.id ?? ""}
|
||||
})
|
||||
.then (response => {
|
||||
this.selectedItem = Object.keys(response.data).length
|
||||
? response.data
|
||||
: {
|
||||
id: '',
|
||||
name: ''
|
||||
};
|
||||
})
|
||||
.catch (error => {});
|
||||
},
|
||||
|
||||
handleResult(result) {
|
||||
this.showPopup = false;
|
||||
|
||||
this.selectedItem = result;
|
||||
|
||||
this.searchTerm = '';
|
||||
|
||||
this.$emit('lookup-added', this.selectedItem);
|
||||
},
|
||||
|
||||
handleFocusOut(e) {
|
||||
const lookup = this.$refs.lookup;
|
||||
|
||||
if (
|
||||
lookup &&
|
||||
! lookup.contains(event.target)
|
||||
) {
|
||||
this.showPopup = false;
|
||||
}
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.selectedItem = {
|
||||
id: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
this.$emit('lookup-removed', this.selectedItem);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,32 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
|
||||
$selectedOption = old($attribute->code) ?: $value;
|
||||
@endphp
|
||||
|
||||
<v-field
|
||||
type="select"
|
||||
id="{{ $attribute->code }}"
|
||||
name="{{ $attribute->code }}[]"
|
||||
rules="{{ $validations }}"
|
||||
label="{{ $attribute->name }}"
|
||||
placeholder="{{ $attribute->name }}"
|
||||
multiple
|
||||
>
|
||||
<select
|
||||
name="{{ $attribute->code }}[]"
|
||||
class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
multiple
|
||||
>
|
||||
@foreach ($options as $option)
|
||||
<option
|
||||
value="{{ $option->id }}"
|
||||
{{ in_array($option->id, is_array($selectedOption) ? $selectedOption : explode(',', $selectedOption)) ? 'selected' : ''}}
|
||||
>
|
||||
{{ $option->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</v-field>
|
||||
@@ -0,0 +1,186 @@
|
||||
@if (isset($attribute))
|
||||
<v-phone-component
|
||||
:attribute="{{ json_encode($attribute) }}"
|
||||
:validations="'{{ $validations }}'"
|
||||
:value="{{ json_encode(old($attribute->code) ?? $value) }}"
|
||||
>
|
||||
<div class="mb-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded rounded-r-none border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
>
|
||||
|
||||
<div class="relative">
|
||||
<select class="custom-select w-full rounded rounded-l-none border bg-white px-2.5 py-2 text-sm font-normal text-gray-800 hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 ltr:mr-6 ltr:pr-8 rtl:ml-6 rtl:pl-8">
|
||||
<option value="work" selected>@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex cursor-pointer items-center gap-2 text-brandColor">
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</span>
|
||||
</v-phone-component>
|
||||
@endif
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-phone-component-template"
|
||||
>
|
||||
<template v-for="(contactNumber, index) in contactNumbers">
|
||||
<div class="mb-2 flex items-center">
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="attribute.code"
|
||||
::name="`${attribute['code']}[${index}][value]`"
|
||||
class="rounded-r-none"
|
||||
::rules="getValidation"
|
||||
::label="attribute.name"
|
||||
v-model="contactNumber['value']"
|
||||
::disabled="isDisabled"
|
||||
/>
|
||||
|
||||
<div class="relative">
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="attribute.code"
|
||||
::name="`${attribute['code']}[${index}][label]`"
|
||||
class="rounded-l-none ltr:mr-6 ltr:pr-8 rtl:ml-6 rtl:pl-8"
|
||||
rules="required"
|
||||
::label="attribute.name"
|
||||
v-model="contactNumber['label']"
|
||||
::disabled="isDisabled"
|
||||
>
|
||||
<option value="work">@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</x-admin::form.control-group.control>
|
||||
</div>
|
||||
|
||||
<i
|
||||
v-if="contactNumbers.length > 1"
|
||||
class="icon-delete ml-1 cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="remove(contactNumber)"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${attribute['code']}[${index}][value]`"/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${attribute['code']}[${index}].value`"/>
|
||||
</template>
|
||||
|
||||
<span
|
||||
class="flex w-fit cursor-pointer items-center gap-2 text-brandColor"
|
||||
@click="add"
|
||||
v-if="! isDisabled"
|
||||
>
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-phone-component', {
|
||||
template: '#v-phone-component-template',
|
||||
|
||||
props: ['validations', 'isDisabled', 'attribute', 'value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
contactNumbers: this.value || [{'value': '', 'label': 'work'}],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
|
||||
this.contactNumbers = newValue || [{'value': '', 'label': 'work'}];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
getValidation() {
|
||||
return {
|
||||
phone: true,
|
||||
unique_contact_number: this.contactNumbers ?? [],
|
||||
...(this.validations === 'required' ? { required: true } : {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.extendValidations();
|
||||
|
||||
if (! this.contactNumbers || ! this.contactNumbers.length) {
|
||||
this.contactNumbers = [{
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.contactNumbers.push({
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
});
|
||||
},
|
||||
|
||||
remove(contactNumber) {
|
||||
this.contactNumbers = this.contactNumbers.filter(number => number !== contactNumber);
|
||||
},
|
||||
|
||||
extendValidations() {
|
||||
defineRule('unique_contact_number', async (value, contactNumbers) => {
|
||||
if (
|
||||
! value
|
||||
|| ! value.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const phoneOccurrences = contactNumbers.filter(contactNumber => contactNumber.value === value).length;
|
||||
|
||||
if (phoneOccurrences > 1) {
|
||||
return 'This phone number is already in use.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the phone number is unique. This support is only for person phone numbers only.
|
||||
*/
|
||||
if (this.attribute.code === 'person[contact_numbers]') {
|
||||
try {
|
||||
const { data } = await this.$axios.get('{{ route('admin.settings.attributes.check_unique_validation') }}', {
|
||||
params: {
|
||||
entity_id: this.attribute.id,
|
||||
entity_type: 'persons',
|
||||
attribute_code: 'contact_numbers',
|
||||
attribute_value: value
|
||||
}
|
||||
});
|
||||
|
||||
if (! data.validated) {
|
||||
return 'This phone number is already in use.';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking email: ', error);
|
||||
|
||||
return 'Error validating email. Please try again.';
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,32 @@
|
||||
@if (isset($attribute))
|
||||
<v-price-component
|
||||
:attribute="{{ json_encode($attribute) }}"
|
||||
:validations="'{{ $validations }}'"
|
||||
:value="{{ json_encode(old($attribute->code) ?? $value) }}"
|
||||
>
|
||||
</v-price-component>
|
||||
@endif
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-price-component-template"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="attribute.code"
|
||||
::value="value"
|
||||
::name="attribute.code"
|
||||
::rules="validations"
|
||||
::label="attribute.name"
|
||||
/>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-price-component', {
|
||||
template: '#v-price-component-template',
|
||||
|
||||
props: ['validations', 'attribute', 'value'],
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,21 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
id="{{ $attribute->code }}"
|
||||
name="{{ $attribute->code }}"
|
||||
rules="{{ $validations }}"
|
||||
:label="$attribute->name"
|
||||
:placeholder="$attribute->name"
|
||||
:value="old($attribute->code) ?? $value"
|
||||
>
|
||||
@foreach ($options as $option)
|
||||
<option value="{{ $option->id }}">
|
||||
{{ $option->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</x-admin::form.control-group.control>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
:value="old($attribute->code) ?? $value"
|
||||
:rules="$validations"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
:id="$attribute->code"
|
||||
:name="$attribute->code"
|
||||
:value="old($attribute->code) ?? $value"
|
||||
:rules="$validations"
|
||||
:label="$attribute->name"
|
||||
/>
|
||||
@@ -0,0 +1,40 @@
|
||||
@foreach ($customAttributes as $attribute)
|
||||
@php
|
||||
$validations = [];
|
||||
|
||||
if ($attribute->is_required) {
|
||||
$validations[] = 'required';
|
||||
}
|
||||
|
||||
if ($attribute->type == 'price') {
|
||||
$validations[] = 'decimal';
|
||||
}
|
||||
|
||||
$validations[] = $attribute->validation;
|
||||
|
||||
$validations = implode('|', array_filter($validations));
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group class="mb-2.5 w-full">
|
||||
<x-admin::form.control-group.label
|
||||
for="{{ $attribute->code }}"
|
||||
:class="$attribute->is_required ? 'required' : ''"
|
||||
>
|
||||
{{ $attribute->name }}
|
||||
|
||||
@if ($attribute->type == 'price')
|
||||
<span class="currency-code">({{ core()->currencySymbol(config('app.currency')) }})</span>
|
||||
@endif
|
||||
</x-admin::form.control-group.label>
|
||||
|
||||
@if (isset($attribute))
|
||||
<x-admin::attributes.edit.index
|
||||
:attribute="$attribute"
|
||||
:validations="$validations"
|
||||
:value="isset($entity) ? $entity[$attribute->code] : null"
|
||||
/>
|
||||
@endif
|
||||
|
||||
<x-admin::form.control-group.error :control-name="$attribute->code" />
|
||||
</x-admin::form.control-group>
|
||||
@endforeach
|
||||
@@ -0,0 +1,25 @@
|
||||
@props([
|
||||
'customAttributes' => [],
|
||||
'entity' => null,
|
||||
'allowEdit' => false,
|
||||
'url' => null,
|
||||
])
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@foreach ($customAttributes as $attribute)
|
||||
@if (view()->exists($typeView = 'admin::components.attributes.view.' . $attribute->type))
|
||||
<div class="grid grid-cols-[1fr_2fr] items-center gap-1">
|
||||
<div class="label dark:text-white">{{ $attribute->name }}</div>
|
||||
|
||||
<div class="font-medium dark:text-white">
|
||||
@include ($typeView, [
|
||||
'attribute' => $attribute,
|
||||
'value' => isset($entity) ? $entity[$attribute->code] : null,
|
||||
'allowEdit' => $allowEdit,
|
||||
'url' => $url,
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.address
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.boolean
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="json_encode($value)"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
|
||||
$selectedOption = old($attribute->code) ?: $value;
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.controls.inline.multiselect
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $selectedOption }}'"
|
||||
:data="$options"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,21 @@
|
||||
@php
|
||||
if (! empty($value)) {
|
||||
if ($value instanceof \Carbon\Carbon) {
|
||||
$value = $value->format('Y-m-d');
|
||||
} elseif (is_string($value)) {
|
||||
$value = \Carbon\Carbon::parse($value)->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.controls.inline.date
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $value }}'"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.datetime
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $value }}'"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.email
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value"
|
||||
rules="required|decimal:4"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.file
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $value ? route('admin.settings.attributes.download', ['path' => $value]) : '' }}'"
|
||||
rules="required|mimes:jpeg,jpg,png,gif"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.image
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ route('admin.settings.attributes.download', ['path' => $value]) }}'"
|
||||
rules="required|mimes:jpeg,jpg,png,gif"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,16 @@
|
||||
@php
|
||||
$lookUpEntity = app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpEntity($attribute->lookup_type, $value);
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.controls.inline.lookup
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $lookUpEntity?->name }}'"
|
||||
:attribute="$attribute"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
:value-label="$lookUpEntity?->name ?? '--'"
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
|
||||
$selectedOption = old($attribute->code) ?: $value;
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.controls.inline.multiselect
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $selectedOption }}'"
|
||||
:data="$options"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-admin::form.control-group.controls.inline.phone
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value"
|
||||
rules="required|decimal:4"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,12 @@
|
||||
<x-admin::form.control-group.controls.inline.text
|
||||
type="inline"
|
||||
::name="'{{ $attribute->code }}'"
|
||||
::value="'{{ $value }}'"
|
||||
position="left"
|
||||
rules="required"
|
||||
:label="$attribute->name"
|
||||
:placeholder="$attribute->name"
|
||||
::errors="errors"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,18 @@
|
||||
@php
|
||||
$options = $attribute->lookup_type
|
||||
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($attribute->lookup_type)
|
||||
: $attribute->options()->orderBy('sort_order')->get();
|
||||
@endphp
|
||||
|
||||
<x-admin::form.control-group.controls.inline.select
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value"
|
||||
:options="$options"
|
||||
rules="required"
|
||||
position="left"
|
||||
:label="$attribute->name"
|
||||
::errors="errors"
|
||||
:placeholder="$attribute->name"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,13 @@
|
||||
<x-admin::form.control-group.controls.inline.text
|
||||
type="inline"
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value ?? ''"
|
||||
:value-label="$value == '' ? '--' : $value"
|
||||
position="left"
|
||||
rules="required|{{ $attribute->validation }}"
|
||||
:label="$attribute->name"
|
||||
:placeholder="$attribute->name"
|
||||
::errors="errors"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,12 @@
|
||||
<x-admin::form.control-group.controls.inline.text
|
||||
type="inline"
|
||||
::name="'{{ $attribute->code }}'"
|
||||
:value="$value"
|
||||
position="left"
|
||||
rules="required"
|
||||
:label="$attribute->name"
|
||||
:placeholder="$attribute->name"
|
||||
::errors="errors"
|
||||
:url="$url"
|
||||
:allow-edit="$allowEdit"
|
||||
/>
|
||||
@@ -0,0 +1,69 @@
|
||||
<v-avatar {{ $attributes }}>
|
||||
<div class="shimmer h-9 w-9 rounded-full"></div>
|
||||
</v-avatar>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-avatar-template"
|
||||
>
|
||||
<div
|
||||
class="flex h-9 min-w-9 items-center justify-center rounded-full text-xs font-medium"
|
||||
:class="colorClasses()"
|
||||
>
|
||||
@{{ name.split(' ').slice(0, 2).map(word => word[0].toUpperCase()).join('') }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-avatar', {
|
||||
template: '#v-avatar-template',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
colors: {
|
||||
background: [
|
||||
'bg-yellow-200',
|
||||
'bg-red-200',
|
||||
'bg-lime-200',
|
||||
'bg-blue-200',
|
||||
'bg-orange-200',
|
||||
'bg-green-200',
|
||||
'bg-pink-200',
|
||||
'bg-yellow-400'
|
||||
],
|
||||
|
||||
text: [
|
||||
'text-yellow-900',
|
||||
'text-red-900',
|
||||
'text-lime-900',
|
||||
'text-blue-900',
|
||||
'text-orange-900',
|
||||
'text-green-900',
|
||||
'text-pink-900',
|
||||
'text-yellow-900',
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
colorClasses() {
|
||||
let index = Math.floor(Math.random() * this.colors.background.length);
|
||||
|
||||
return [
|
||||
this.colors.background[index],
|
||||
this.colors.text[index],
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,15 @@
|
||||
@props([
|
||||
'name' => '',
|
||||
'entity' => null,
|
||||
'route' => null,
|
||||
])
|
||||
|
||||
<div class="flex justify-start max-lg:hidden">
|
||||
<div class="flex items-center gap-x-3.5">
|
||||
@if($route)
|
||||
{{ Breadcrumbs::view('admin::partials.breadcrumbs', $name, $route, $entity) }}
|
||||
@else
|
||||
{{ Breadcrumbs::view('admin::partials.breadcrumbs', $name, $entity) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<v-button {{ $attributes }}></v-button>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-button-template"
|
||||
>
|
||||
<button
|
||||
v-if="! loading"
|
||||
:class="[buttonClass, '']"
|
||||
>
|
||||
@{{ title }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else
|
||||
:class="[buttonClass, '']"
|
||||
>
|
||||
<!-- Spinner -->
|
||||
<x-admin::spinner class="absolute" />
|
||||
|
||||
<span class="relative h-full w-full opacity-0">
|
||||
@{{ title }}
|
||||
</span>
|
||||
</button>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-button', {
|
||||
template: '#v-button-template',
|
||||
|
||||
props: {
|
||||
loading: Boolean,
|
||||
buttonType: String,
|
||||
title: String,
|
||||
buttonClass: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,100 @@
|
||||
<v-charts-bar {{ $attributes }}></v-charts-bar>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<!-- SEO Vue Component Template -->
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-charts-bar-template"
|
||||
>
|
||||
<canvas
|
||||
:id="$.uid + '_chart'"
|
||||
class="flex w-full max-w-full items-end"
|
||||
:style="'aspect-ratio:' + aspectRatio + '/1'"
|
||||
style=""
|
||||
></canvas>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-charts-bar', {
|
||||
template: '#v-charts-bar-template',
|
||||
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
|
||||
datasets: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
|
||||
aspectRatio: {
|
||||
type: Number,
|
||||
default: 3.23,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chart: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.prepare();
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepare() {
|
||||
const barCount = this.datasets.length;
|
||||
|
||||
this.datasets.forEach((dataset) => {
|
||||
dataset.barThickness = Math.max(4, 36 / barCount);
|
||||
});
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
this.chart = new Chart(document.getElementById(this.$.uid + '_chart'), {
|
||||
type: 'bar',
|
||||
|
||||
data: {
|
||||
labels: this.labels,
|
||||
|
||||
datasets: this.datasets,
|
||||
},
|
||||
|
||||
options: {
|
||||
aspectRatio: this.aspectRatio,
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
|
||||
border: {
|
||||
dash: [8, 4],
|
||||
}
|
||||
},
|
||||
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: {
|
||||
dash: [8, 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,68 @@
|
||||
<v-charts-doughnut {{ $attributes }}></v-charts-doughnut>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<!-- SEO Vue Component Template -->
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-charts-doughnut-template"
|
||||
>
|
||||
<canvas
|
||||
:id="$.uid + '_chart'"
|
||||
class="flex w-full max-w-full items-end"
|
||||
></canvas>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-charts-doughnut', {
|
||||
template: '#v-charts-doughnut-template',
|
||||
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
|
||||
datasets: {
|
||||
type: Array,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chart: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.prepare();
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepare() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
this.chart = new Chart(document.getElementById(this.$.uid + '_chart'), {
|
||||
type: 'doughnut',
|
||||
|
||||
data: {
|
||||
labels: this.labels,
|
||||
|
||||
datasets: this.datasets,
|
||||
},
|
||||
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,97 @@
|
||||
<v-charts-line {{ $attributes }}></v-charts-line>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<!-- SEO Vue Component Template -->
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-charts-line-template"
|
||||
>
|
||||
<canvas
|
||||
:id="$.uid + '_chart'"
|
||||
class="flex w-full items-end"
|
||||
:style="'aspect-ratio:' + aspectRatio + '/1'"
|
||||
></canvas>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-charts-line', {
|
||||
template: '#v-charts-line-template',
|
||||
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
|
||||
datasets: {
|
||||
type: Array,
|
||||
default: true,
|
||||
},
|
||||
|
||||
aspectRatio: {
|
||||
type: Number,
|
||||
default: 3.23,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chart: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.prepare();
|
||||
},
|
||||
|
||||
methods: {
|
||||
prepare() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
this.chart = new Chart(document.getElementById(this.$.uid + '_chart'), {
|
||||
type: 'line',
|
||||
|
||||
data: {
|
||||
labels: this.labels,
|
||||
|
||||
datasets: this.datasets,
|
||||
},
|
||||
|
||||
options: {
|
||||
aspectRatio: this.aspectRatio,
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
|
||||
{{-- tooltip: {
|
||||
enabled: false,
|
||||
} --}}
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
|
||||
border: {
|
||||
dash: [8, 4],
|
||||
}
|
||||
},
|
||||
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: {
|
||||
dash: [8, 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,170 @@
|
||||
<v-datagrid-export {{ $attributes }}>
|
||||
<div class="transparent-button hover:bg-gray-200 dark:text-white dark:hover:bg-gray-800">
|
||||
<span class="icon-export text-xl text-gray-600"></span>
|
||||
|
||||
@lang('admin::app.export.export')
|
||||
</div>
|
||||
</v-datagrid-export>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-export-template"
|
||||
>
|
||||
<div>
|
||||
<x-admin::modal ref="exportModal">
|
||||
<x-slot:toggle>
|
||||
<button class="transparent-button hover:bg-gray-200 dark:text-white dark:hover:bg-gray-800">
|
||||
<span class="icon-export text-xl text-gray-600"></span>
|
||||
|
||||
@lang('admin::app.export.export')
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:header>
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white">
|
||||
@lang('admin::app.export.download')
|
||||
</p>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content>
|
||||
<x-admin::form action="">
|
||||
<x-admin::form.control-group class="!mb-0">
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
name="format"
|
||||
v-model="format"
|
||||
>
|
||||
<option value="csv">
|
||||
@lang('admin::app.export.csv')
|
||||
</option>
|
||||
|
||||
<option value="xls">
|
||||
@lang('admin::app.export.xls')
|
||||
</option>
|
||||
|
||||
<option value="xlsx">
|
||||
@lang('admin::app.export.xlsx')
|
||||
</option>
|
||||
</x-admin::form.control-group.control>
|
||||
</x-admin::form.control-group>
|
||||
</x-admin::form>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:footer>
|
||||
<button
|
||||
type="button"
|
||||
class="primary-button"
|
||||
@click="download"
|
||||
>
|
||||
@lang('admin::app.export.export')
|
||||
</button>
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid-export', {
|
||||
template: '#v-datagrid-export-template',
|
||||
|
||||
props: ['src'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
format: 'xls',
|
||||
|
||||
available: null,
|
||||
|
||||
applied: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.registerEvents();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Registers events to update properties and trigger the download process.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
registerEvents() {
|
||||
this.$emitter.on('change-datagrid', this.updateProperties);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the available and applied properties with new values.
|
||||
*
|
||||
* @param {object} data - Object containing available and applied properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
updateProperties({ available, applied }) {
|
||||
this.available = available;
|
||||
|
||||
this.applied = applied;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates the download process for exporting data.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
download() {
|
||||
if (! this.available?.records?.length) {
|
||||
this.$emitter.emit('add-flash', { type: 'warning', message: '@lang('admin::app.export.no-records')' });
|
||||
|
||||
this.$refs.exportModal.toggle();
|
||||
} else {
|
||||
let params = {
|
||||
export: 1,
|
||||
|
||||
format: this.format,
|
||||
|
||||
sort: {},
|
||||
|
||||
filters: {},
|
||||
};
|
||||
|
||||
if (
|
||||
this.applied.sort.column &&
|
||||
this.applied.sort.order
|
||||
) {
|
||||
params.sort = this.applied.sort;
|
||||
}
|
||||
|
||||
this.applied.filters.columns.forEach(column => {
|
||||
params.filters[column.index] = column.value;
|
||||
});
|
||||
|
||||
this.$axios
|
||||
.get(this.src, {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then((response) => {
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
|
||||
/**
|
||||
* Link generation.
|
||||
*/
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${(Math.random() + 1).toString(36).substring(7)}.${this.format}`);
|
||||
|
||||
/**
|
||||
* Adding a link to a document, clicking on the link, and then removing the link.
|
||||
*/
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.$refs.exportModal.toggle();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,23 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($columns as $column)
|
||||
<th>{{ $column->getLabel() }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach ($records as $record)
|
||||
<tr>
|
||||
@foreach($columns as $column)
|
||||
@if ($closure = $column->getClosure())
|
||||
<td>{!! $closure($record) !!}</td>
|
||||
@else
|
||||
<td>{{ $record->{$column->getIndex()} }}</td>
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,572 @@
|
||||
@props([
|
||||
'isMultiRow' => false,
|
||||
'toolbarLeftBefore' => null,
|
||||
'toolbarLeftAfter' => null,
|
||||
'toolbarRightBefore' => null,
|
||||
'toolbarRightAfter' => null,
|
||||
])
|
||||
|
||||
<v-datagrid {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</v-datagrid>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-template"
|
||||
>
|
||||
<div>
|
||||
<!-- Toolbar -->
|
||||
<x-admin::datagrid.toolbar>
|
||||
<x-slot:toolbar-left-before>
|
||||
{{ $toolbarLeftBefore }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:toolbar-left-after>
|
||||
{{ $toolbarLeftAfter }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:toolbar-right-before>
|
||||
{{ $toolbarRightBefore }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot:toolbar-right-after>
|
||||
{{ $toolbarRightAfter }}
|
||||
</x-slot>
|
||||
</x-admin::datagrid.toolbar>
|
||||
|
||||
<div class="flex">
|
||||
<x-admin::datagrid.table :isMultiRow="$isMultiRow">
|
||||
<template #header="{
|
||||
isLoading,
|
||||
available,
|
||||
applied,
|
||||
selectAll,
|
||||
sort,
|
||||
performAction
|
||||
}">
|
||||
<slot
|
||||
name="header"
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:select-all="selectAll"
|
||||
:sort="sort"
|
||||
:perform-action="performAction"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #body="{
|
||||
isLoading,
|
||||
available,
|
||||
applied,
|
||||
selectAll,
|
||||
sort,
|
||||
performAction
|
||||
}">
|
||||
<slot
|
||||
name="body"
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:select-all="selectAll"
|
||||
:sort="sort"
|
||||
:perform-action="performAction"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
</x-admin::datagrid.table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid', {
|
||||
template: '#v-datagrid-template',
|
||||
|
||||
props: ['src'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
|
||||
available: {
|
||||
id: null,
|
||||
|
||||
columns: [],
|
||||
|
||||
actions: [],
|
||||
|
||||
massActions: [],
|
||||
|
||||
records: [],
|
||||
|
||||
meta: {},
|
||||
},
|
||||
|
||||
applied: {
|
||||
massActions: {
|
||||
meta: {
|
||||
mode: 'none',
|
||||
|
||||
action: null,
|
||||
},
|
||||
|
||||
indices: [],
|
||||
|
||||
value: null,
|
||||
},
|
||||
|
||||
pagination: {
|
||||
page: 1,
|
||||
|
||||
perPage: 10,
|
||||
},
|
||||
|
||||
sort: {
|
||||
column: null,
|
||||
|
||||
order: null,
|
||||
},
|
||||
|
||||
filters: {
|
||||
columns: [
|
||||
{
|
||||
index: 'all',
|
||||
value: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
savedFilterId: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
'available.records': function (newRecords, oldRecords) {
|
||||
this.setCurrentSelectionMode();
|
||||
|
||||
this.updateDatagrids();
|
||||
|
||||
this.updateExportComponent();
|
||||
},
|
||||
|
||||
'applied.savedFilterId': function (newSavedFilterId, oldSavedFilterId) {
|
||||
this.updateDatagrids();
|
||||
},
|
||||
|
||||
'applied.massActions.indices': function (newIndices, oldIndices) {
|
||||
this.setCurrentSelectionMode();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.boot();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialization: This function checks for any previously saved filters in local storage and applies them as needed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
boot() {
|
||||
let datagrids = this.getDatagrids();
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (urlParams.has('search')) {
|
||||
let searchAppliedColumn = this.applied.filters.columns.find(column => column.index === 'all');
|
||||
|
||||
searchAppliedColumn.value = [urlParams.get('search')];
|
||||
}
|
||||
|
||||
if (datagrids?.length) {
|
||||
const currentDatagrid = datagrids.find(({ src }) => src === this.src);
|
||||
|
||||
if (currentDatagrid) {
|
||||
this.applied.pagination = currentDatagrid.applied.pagination;
|
||||
|
||||
this.applied.sort = currentDatagrid.applied.sort;
|
||||
|
||||
this.applied.filters = currentDatagrid.applied.filters;
|
||||
|
||||
this.applied.savedFilterId = currentDatagrid.applied.savedFilterId;
|
||||
|
||||
if (urlParams.has('search')) {
|
||||
let searchAppliedColumn = this.applied.filters.columns.find(column => column.index === 'all');
|
||||
|
||||
searchAppliedColumn.value = [urlParams.get('search')];
|
||||
}
|
||||
|
||||
this.get();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get. This will prepare params from the `applied` props and fetch the data from the backend.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
get(extraParams = {}) {
|
||||
let params = {
|
||||
pagination: {
|
||||
page: this.applied.pagination.page,
|
||||
per_page: this.applied.pagination.perPage,
|
||||
},
|
||||
|
||||
sort: {},
|
||||
|
||||
filters: {},
|
||||
};
|
||||
|
||||
if (
|
||||
this.applied.sort.column &&
|
||||
this.applied.sort.order
|
||||
) {
|
||||
params.sort = this.applied.sort;
|
||||
}
|
||||
|
||||
this.applied.filters.columns.forEach(column => {
|
||||
params.filters[column.index] = column.value;
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
urlParams.forEach((param, key) => params[key] = param);
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.$axios
|
||||
.get(this.src, {
|
||||
params: { ...params, ...extraParams }
|
||||
})
|
||||
.then((response) => {
|
||||
/**
|
||||
* Precisely taking all the keys to the data prop to avoid adding any extra keys from the response.
|
||||
*/
|
||||
const {
|
||||
id,
|
||||
columns,
|
||||
actions,
|
||||
mass_actions,
|
||||
records,
|
||||
meta
|
||||
} = response.data;
|
||||
|
||||
this.available.id = id;
|
||||
|
||||
this.available.columns = columns;
|
||||
|
||||
this.available.actions = actions;
|
||||
|
||||
this.available.massActions = mass_actions;
|
||||
|
||||
this.available.records = records;
|
||||
|
||||
this.available.meta = meta;
|
||||
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Change Page. When the child component has handled all the cases, it will send the
|
||||
* valid new page; otherwise, it will block. Here, we are certain that we have
|
||||
* a new page, so the parent will simply call the AJAX based on the new page.
|
||||
*
|
||||
* @param {integer} newPage
|
||||
* @returns {void}
|
||||
*/
|
||||
changePage(newPage) {
|
||||
this.applied.pagination.page = newPage;
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* Change per page option.
|
||||
*
|
||||
* @param {integer} option
|
||||
* @returns {void}
|
||||
*/
|
||||
changePerPageOption(option) {
|
||||
this.applied.pagination.perPage = option;
|
||||
|
||||
/**
|
||||
* When the total records are less than the number of data per page, we need to reset the page.
|
||||
*/
|
||||
if (this.available.meta.last_page >= this.applied.pagination.page) {
|
||||
this.applied.pagination.page = 1;
|
||||
}
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sort results.
|
||||
*
|
||||
* @param {object} column
|
||||
* @returns {void}
|
||||
*/
|
||||
sort(column) {
|
||||
if (column.sortable) {
|
||||
this.applied.sort = {
|
||||
column: column.index,
|
||||
order: this.applied.sort.order === 'asc' ? 'desc' : 'asc',
|
||||
};
|
||||
|
||||
/**
|
||||
* When the sorting changes, we need to reset the page.
|
||||
*/
|
||||
this.applied.pagination.page = 1;
|
||||
|
||||
this.get();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Search results.
|
||||
*
|
||||
* @param {object} filters
|
||||
* @returns {void}
|
||||
*/
|
||||
search(filters) {
|
||||
this.applied.filters.columns = [
|
||||
...(this.applied.filters.columns.filter((column) => column.index !== 'all')),
|
||||
...filters.columns,
|
||||
];
|
||||
|
||||
/**
|
||||
* We need to reset the page on filtering.
|
||||
*/
|
||||
this.applied.pagination.page = 1;
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter results.
|
||||
*
|
||||
* @param {object} filters
|
||||
* @returns {void}
|
||||
*/
|
||||
filter(filters) {
|
||||
this.applied.filters.columns = [
|
||||
...(this.applied.filters.columns.filter((column) => column.index === 'all')),
|
||||
...filters.columns,
|
||||
];
|
||||
|
||||
/**
|
||||
* This will check for empty column values and reset the saved filter ID to ensure the saved filter is not highlighted.
|
||||
*/
|
||||
const isEmptyColumnValue = this.applied.filters.columns
|
||||
.filter((column) => column.index !== 'all')
|
||||
.every((column) => column.value.length === 0);
|
||||
|
||||
if (isEmptyColumnValue) {
|
||||
this.applied.savedFilterId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to reset the page on filtering.
|
||||
*/
|
||||
this.applied.pagination.page = 1;
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter results by the saved filter.
|
||||
*
|
||||
* @param {Object} filter
|
||||
* @returns {void}
|
||||
*/
|
||||
applySavedFilter(filter) {
|
||||
if (! filter) {
|
||||
this.applied.savedFilterId = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.applied = filter.applied;
|
||||
|
||||
this.applied.savedFilterId = filter.id;
|
||||
|
||||
this.get();
|
||||
},
|
||||
|
||||
/**
|
||||
* This will analyze the current selection mode based on the mass action indices.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
setCurrentSelectionMode() {
|
||||
this.applied.massActions.meta.mode = 'none';
|
||||
|
||||
if (! this.available.records.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectionCount = 0;
|
||||
|
||||
this.available.records.forEach(record => {
|
||||
const id = record[this.available.meta.primary_column];
|
||||
|
||||
if (this.applied.massActions.indices.includes(id)) {
|
||||
this.applied.massActions.meta.mode = 'partial';
|
||||
|
||||
++selectionCount;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.available.records.length === selectionCount) {
|
||||
this.applied.massActions.meta.mode = 'all';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This will select all records and update the mass action indices.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
selectAll() {
|
||||
if (['all', 'partial'].includes(this.applied.massActions.meta.mode)) {
|
||||
this.available.records.forEach(record => {
|
||||
const id = record[this.available.meta.primary_column];
|
||||
|
||||
this.applied.massActions.indices = this.applied.massActions.indices.filter(selectedId => selectedId !== id);
|
||||
});
|
||||
|
||||
this.applied.massActions.meta.mode = 'none';
|
||||
} else {
|
||||
this.available.records.forEach(record => {
|
||||
const id = record[this.available.meta.primary_column];
|
||||
|
||||
let found = this.applied.massActions.indices.find(selectedId => selectedId === id);
|
||||
|
||||
if (! found) {
|
||||
this.applied.massActions.indices = [
|
||||
...this.applied.massActions.indices,
|
||||
id,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
this.applied.massActions.meta.mode = 'all';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the export component properties whenever new results appear in the datagrid.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateExportComponent() {
|
||||
/**
|
||||
* This event should be fired whenever new results appear. This allows the export feature to
|
||||
* listen to it and update its properties accordingly.
|
||||
*/
|
||||
this.$emitter.emit('change-datagrid', {
|
||||
available: this.available,
|
||||
applied: this.applied
|
||||
});
|
||||
},
|
||||
|
||||
//=======================================================================================
|
||||
// Support for previous applied values in datagrids. All code is based on local storage.
|
||||
//=======================================================================================
|
||||
|
||||
/**
|
||||
* Updates the datagrids stored in local storage with the latest data.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateDatagrids() {
|
||||
let datagrids = this.getDatagrids();
|
||||
|
||||
if (datagrids?.length) {
|
||||
const currentDatagrid = datagrids.find(({ src }) => src === this.src);
|
||||
|
||||
if (currentDatagrid) {
|
||||
datagrids = datagrids.map(datagrid => {
|
||||
if (datagrid.src === this.src) {
|
||||
return {
|
||||
...datagrid,
|
||||
requestCount: ++datagrid.requestCount,
|
||||
available: this.available,
|
||||
applied: this.applied,
|
||||
};
|
||||
}
|
||||
|
||||
return datagrid;
|
||||
});
|
||||
} else {
|
||||
datagrids.push(this.getDatagridInitialProperties());
|
||||
}
|
||||
} else {
|
||||
datagrids = [this.getDatagridInitialProperties()];
|
||||
}
|
||||
|
||||
this.setDatagrids(datagrids);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the initial properties for a datagrid.
|
||||
*
|
||||
* @returns {object} Initial properties for a datagrid.
|
||||
*/
|
||||
getDatagridInitialProperties() {
|
||||
return {
|
||||
src: this.src,
|
||||
requestCount: 0,
|
||||
available: this.available,
|
||||
applied: this.applied,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the storage key for datagrids in local storage.
|
||||
*
|
||||
* @returns {string} Storage key for datagrids.
|
||||
*/
|
||||
getDatagridsStorageKey() {
|
||||
return 'datagrids';
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the datagrids stored in local storage.
|
||||
*
|
||||
* @returns {Array} Datagrids stored in local storage.
|
||||
*/
|
||||
getDatagrids() {
|
||||
let datagrids = localStorage.getItem(
|
||||
this.getDatagridsStorageKey()
|
||||
);
|
||||
|
||||
return JSON.parse(datagrids) ?? [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the datagrids in local storage.
|
||||
*
|
||||
* @param {Array} datagrids - Datagrids to be stored in local storage.
|
||||
* @returns {void}
|
||||
*/
|
||||
setDatagrids(datagrids) {
|
||||
localStorage.setItem(
|
||||
this.getDatagridsStorageKey(),
|
||||
JSON.stringify(datagrids)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,377 @@
|
||||
@props(['isMultiRow' => false])
|
||||
|
||||
<v-datagrid-table
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
@selectAll="selectAll"
|
||||
@sort="sort"
|
||||
@actionSuccess="get"
|
||||
>
|
||||
{{ $slot }}
|
||||
</v-datagrid-table>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-table-template"
|
||||
>
|
||||
<div class="w-full">
|
||||
<!-- Table view for larger screens, Card view for mobile -->
|
||||
<div class="table-responsive box-shadow rounded-t-0 grid w-full overflow-hidden border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<!-- Table Header - Always visible on all screens -->
|
||||
<slot
|
||||
name="header"
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:select-all="selectAll"
|
||||
:sort="sort"
|
||||
:perform-action="performAction"
|
||||
>
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.datagrid.table.head :isMultiRow="$isMultiRow" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
class="row grid min-h-[47px] items-center gap-2.5 border-b bg-gray-50 px-4 py-2.5 text-black dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 max-lg:hidden"
|
||||
:style="`grid-template-columns: repeat(${gridsCount}, minmax(0, 1fr))`"
|
||||
>
|
||||
<!-- Mass Actions -->
|
||||
<p v-if="available.massActions.length">
|
||||
<label for="mass_action_select_all_records">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="mass_action_select_all_records"
|
||||
id="mass_action_select_all_records"
|
||||
class="peer hidden"
|
||||
:checked="['all', 'partial'].includes(applied.massActions.meta.mode)"
|
||||
@change="selectAll"
|
||||
>
|
||||
|
||||
<span
|
||||
class="icon-checkbox-outline cursor-pointer rounded-md text-2xl text-gray-500 peer-checked:text-brandColor"
|
||||
:class="[
|
||||
applied.massActions.meta.mode === 'all' ? 'peer-checked:icon-checkbox-select peer-checked:text-brandColor ' : (
|
||||
applied.massActions.meta.mode === 'partial' ? 'peer-checked:icon-checkbox-multiple peer-checked:brandColor' : ''
|
||||
),
|
||||
]"
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<!-- Columns -->
|
||||
<template v-for="column in available.columns">
|
||||
<div
|
||||
class="flex items-center gap-1.5 break-words"
|
||||
:class="{'cursor-pointer select-none hover:text-gray-800 dark:hover:text-white': column.sortable}"
|
||||
@click="sort(column)"
|
||||
v-if="column.visibility"
|
||||
>
|
||||
<p v-html="column.label"></p>
|
||||
|
||||
<i
|
||||
class="align-text-bottom text-base text-gray-600 dark:text-gray-300"
|
||||
:class="[applied.sort.order === 'asc' ? 'icon-stats-down': 'icon-stats-up']"
|
||||
v-if="column.index == applied.sort.column"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<p
|
||||
class="text-end"
|
||||
v-if="available.actions.length"
|
||||
>
|
||||
@lang('admin::app.components.datagrid.table.actions')
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sort/Filter Header -->
|
||||
<div class="hidden border-b bg-gray-50 px-4 py-3 text-black dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 max-lg:block">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Mass Actions for Mobile -->
|
||||
<div v-if="available.massActions.length">
|
||||
<label for="mass_action_select_all_records">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="mass_action_select_all_records"
|
||||
id="mass_action_select_all_records"
|
||||
class="peer hidden"
|
||||
:checked="['all', 'partial'].includes(applied.massActions.meta.mode)"
|
||||
@change="selectAll"
|
||||
>
|
||||
|
||||
<span
|
||||
class="icon-checkbox-outline cursor-pointer rounded-md text-2xl text-gray-500 peer-checked:text-brandColor"
|
||||
:class="[
|
||||
applied.massActions.meta.mode === 'all' ? 'peer-checked:icon-checkbox-select peer-checked:text-brandColor ' : (
|
||||
applied.massActions.meta.mode === 'partial' ? 'peer-checked:icon-checkbox-multiple peer-checked:brandColor' : ''
|
||||
),
|
||||
]"
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sort Dropdown -->
|
||||
<div class="flex w-full justify-end" v-if="available.columns.some(column => column.sortable)">
|
||||
<x-admin::dropdown position="bottom-{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'left' : 'right' }}">
|
||||
<x-slot:toggle>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex w-full max-w-max cursor-pointer appearance-none items-center justify-between gap-x-2 rounded-md border bg-white px-2.5 py-1.5 text-center leading-6 text-gray-600 transition-all marker:shadow hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
>
|
||||
<span>
|
||||
Sort
|
||||
</span>
|
||||
|
||||
<span class="icon-down-arrow text-2xl"></span>
|
||||
</button>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:menu>
|
||||
<x-admin::dropdown.menu.item
|
||||
v-for="column in available.columns.filter(column => column.sortable && column.visibility)"
|
||||
@click="sort(column)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-html="column.label"></span>
|
||||
<i
|
||||
class="align-text-bottom text-base text-gray-600 dark:text-gray-300"
|
||||
:class="[applied.sort.order === 'asc' ? 'icon-stats-down': 'icon-stats-up']"
|
||||
v-if="column.index == applied.sort.column"
|
||||
></i>
|
||||
</div>
|
||||
</x-admin::dropdown.menu.item>
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</slot>
|
||||
|
||||
<slot
|
||||
name="body"
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:select-all="selectAll"
|
||||
:sort="sort"
|
||||
:perform-action="performAction"
|
||||
>
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.datagrid.table.body :isMultiRow="$isMultiRow" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="available.records.length">
|
||||
<!-- Desktop View -->
|
||||
<div
|
||||
class="row grid items-center gap-2.5 border-b px-4 py-4 text-black transition-all hover:bg-gray-50 dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-950 max-lg:hidden"
|
||||
v-for="record in available.records"
|
||||
:style="`grid-template-columns: repeat(${gridsCount}, minmax(0, 1fr))`"
|
||||
>
|
||||
<!-- Mass Actions -->
|
||||
<p v-if="available.massActions.length">
|
||||
<label :for="`mass_action_select_record_${record[available.meta.primary_column]}`">
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`mass_action_select_record_${record[available.meta.primary_column]}`"
|
||||
:value="record[available.meta.primary_column]"
|
||||
:id="`mass_action_select_record_${record[available.meta.primary_column]}`"
|
||||
class="peer hidden"
|
||||
v-model="applied.massActions.indices"
|
||||
>
|
||||
|
||||
<span class="icon-checkbox-outline peer-checked:icon-checkbox-select cursor-pointer rounded-md text-2xl text-gray-500 peer-checked:text-brandColor">
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<!-- Columns -->
|
||||
<template v-for="column in available.columns">
|
||||
<p
|
||||
class="break-words"
|
||||
v-html="record[column.index]"
|
||||
v-if="column.visibility"
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<p
|
||||
class="flex h-full items-center place-self-end"
|
||||
v-if="available.actions.length"
|
||||
>
|
||||
<span
|
||||
class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"
|
||||
:class="action.icon"
|
||||
v-text="! action.icon ? action.title : ''"
|
||||
v-for="action in record.actions"
|
||||
@click="performAction(action)"
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Card View -->
|
||||
<div
|
||||
class="hidden border-b px-4 py-4 text-black dark:border-gray-800 dark:text-gray-300 max-lg:block"
|
||||
v-for="record in available.records"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<!-- Mass Actions for Mobile Cards -->
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p v-if="available.massActions.length">
|
||||
<label :for="`mass_action_select_record_${record[available.meta.primary_column]}`">
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`mass_action_select_record_${record[available.meta.primary_column]}`"
|
||||
:value="record[available.meta.primary_column]"
|
||||
:id="`mass_action_select_record_${record[available.meta.primary_column]}`"
|
||||
class="peer hidden"
|
||||
v-model="applied.massActions.indices"
|
||||
>
|
||||
|
||||
<span class="icon-checkbox-outline peer-checked:icon-checkbox-select cursor-pointer rounded-md text-2xl text-gray-500 peer-checked:text-brandColor">
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<!-- Actions for Mobile -->
|
||||
<div
|
||||
class="flex w-full items-center justify-end"
|
||||
v-if="available.actions.length"
|
||||
>
|
||||
<span
|
||||
class="dark:hover:bg-gray-80 cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200"
|
||||
:class="action.icon"
|
||||
v-text="! action.icon ? action.title : ''"
|
||||
v-for="action in record.actions"
|
||||
@click="performAction(action)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
<div class="grid gap-2">
|
||||
<template v-for="column in available.columns">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2">
|
||||
<span class="text-slate-600 dark:text-gray-300" v-html="column.label + ':'"></span>
|
||||
<span class="break-words font-medium text-slate-900 dark:text-white" v-html="record[column.index]"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="row grid border-b px-4 py-4 text-center text-gray-600 dark:border-gray-800 dark:text-gray-300">
|
||||
<p>
|
||||
@lang('admin::app.components.datagrid.table.no-records-available')
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid-table', {
|
||||
template: '#v-datagrid-table-template',
|
||||
|
||||
props: ['isLoading', 'available', 'applied'],
|
||||
|
||||
computed: {
|
||||
gridsCount() {
|
||||
let count = this.available.columns.filter((column) => column.visibility).length;
|
||||
|
||||
if (this.available.actions.length) {
|
||||
++count;
|
||||
}
|
||||
|
||||
if (this.available.massActions.length) {
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Select all records in the datagrid.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
selectAll() {
|
||||
this.$emit('selectAll');
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a sorting operation on the specified column.
|
||||
*
|
||||
* @param {object} column
|
||||
* @returns {void}
|
||||
*/
|
||||
sort(column) {
|
||||
this.$emit('sort', column);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform the specified action.
|
||||
*
|
||||
* @param {object} action
|
||||
* @returns {void}
|
||||
*/
|
||||
performAction(action) {
|
||||
const method = action.method.toLowerCase();
|
||||
|
||||
switch (method) {
|
||||
case 'get':
|
||||
window.location.href = action.url;
|
||||
|
||||
break;
|
||||
|
||||
case 'post':
|
||||
case 'put':
|
||||
case 'patch':
|
||||
case 'delete':
|
||||
this.$emitter.emit('open-confirm-modal', {
|
||||
agree: () => {
|
||||
this.$axios[method](action.url)
|
||||
.then(response => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$emit('actionSuccess', response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$emit('actionError', error.response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error('Method not supported.');
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endpushOnce
|
||||
@@ -0,0 +1,94 @@
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.datagrid.toolbar />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between gap-4 rounded-t-lg border border-b-0 border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 max-md:flex-wrap">
|
||||
<!-- Left Toolbar -->
|
||||
<div class="toolbarLeft flex gap-x-1">
|
||||
{{ $toolbarLeftBefore }}
|
||||
|
||||
<!-- Mass Actions Panel -->
|
||||
<transition-group
|
||||
tag='div'
|
||||
name="flash-group"
|
||||
enter-from-class="ltr:translate-y-full rtl:-translate-y-full"
|
||||
enter-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
enter-to-class="ltr:translate-y-0 rtl:-translate-y-0"
|
||||
leave-from-class="ltr:translate-y-0 rtl:-translate-y-0"
|
||||
leave-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
leave-to-class="ltr:translate-y-full rtl:-translate-y-full"
|
||||
class='fixed bottom-10 left-1/2 z-[10003] grid -translate-x-1/2 justify-items-end gap-2.5'
|
||||
>
|
||||
<div v-if="applied.massActions.indices.length">
|
||||
<x-admin::datagrid.toolbar.mass-action>
|
||||
<template #mass-action="{
|
||||
available,
|
||||
applied,
|
||||
massActions,
|
||||
validateMassAction,
|
||||
performMassAction
|
||||
}">
|
||||
<slot
|
||||
name="mass-action"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:mass-actions="massActions"
|
||||
:validate-mass-action="validateMassAction"
|
||||
:perform-mass-action="performMassAction"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
</x-admin::datagrid.toolbar.mass-action>
|
||||
</div>
|
||||
</transition-group>
|
||||
|
||||
<!-- Search Panel -->
|
||||
<x-admin::datagrid.toolbar.search>
|
||||
<template #search="{
|
||||
available,
|
||||
applied,
|
||||
search,
|
||||
getSearchedValues,
|
||||
}">
|
||||
<slot
|
||||
name="search"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:search="search"
|
||||
:get-searched-values="getSearchedValues"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
</x-admin::datagrid.toolbar.search>
|
||||
|
||||
{{ $toolbarLeftAfter }}
|
||||
</div>
|
||||
|
||||
<!-- Right Toolbar -->
|
||||
<div class="toolbarRight flex gap-x-4">
|
||||
{{ $toolbarRightBefore }}
|
||||
|
||||
<!-- Pagination Panel -->
|
||||
<x-admin::datagrid.toolbar.pagination>
|
||||
<template #pagination="{
|
||||
available,
|
||||
applied,
|
||||
changePage,
|
||||
changePerPageOption
|
||||
}">
|
||||
<slot
|
||||
name="pagination"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:change-page="changePage"
|
||||
:change-per-page-option="changePerPageOption"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
</x-admin::datagrid.toolbar.pagination>
|
||||
|
||||
{{ $toolbarRightAfter }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
<v-datagrid-mass-action
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
>
|
||||
{{ $slot }}
|
||||
</v-datagrid-mass-action>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-mass-action-template"
|
||||
>
|
||||
<slot
|
||||
name="mass-action"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:mass-actions="massActions"
|
||||
:validate-mass-action="validateMassAction"
|
||||
:perform-mass-action="performMassAction"
|
||||
>
|
||||
<div class="flex max-w-max items-center justify-center gap-2 rounded-lg bg-white p-2 px-4 shadow-[0px_10px_20px_0px_rgba(0,0,0,0.12)] dark:border-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<p class="text-sm font-light text-gray-800 dark:text-white">
|
||||
@{{ "@lang('admin::app.components.datagrid.toolbar.selected')".replace(':total', applied.massActions.indices.length) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="available.massActions.some(action => action.icon !== 'icon-delete' && action.options.length)">
|
||||
<template v-for="massAction in available.massActions.filter(action => action.icon !== 'icon-delete')">
|
||||
<x-admin::dropdown class="rounded-lg dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400">
|
||||
<x-slot:toggle>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex w-full max-w-max cursor-pointer appearance-none items-center justify-between gap-x-2 rounded-md border bg-white px-2.5 py-1.5 text-center leading-6 text-gray-600 transition-all marker:shadow hover:border-gray-400 focus:border-gray-400 focus:ring-black dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
@click="showPopup = ! showPopup"
|
||||
>
|
||||
<span class="text-sm font-normal">
|
||||
@{{ massAction.title }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="text-2xl"
|
||||
:class="showPopup ? 'icon-up-arrow' : 'icon-down-arrow'"
|
||||
></span>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:menu class="!bottom-12 !top-auto !p-0 shadow-[0_5px_20px_rgba(0,0,0,0.15)] dark:border-gray-800">
|
||||
<li v-for="option in massAction?.options">
|
||||
<a
|
||||
class="whitespace-no-wrap block rounded-t px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-950"
|
||||
href="javascript:void(0);"
|
||||
@click="performMassAction(massAction, option)"
|
||||
>
|
||||
@{{ option.label }}
|
||||
</a>
|
||||
</li>
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="primary-button border-red-500 !bg-red-500"
|
||||
@click="performMassAction(available.massActions.find(action => action.icon === 'icon-delete'))"
|
||||
>
|
||||
@{{ available.massActions.find(action => action.icon === 'icon-delete')?.title }}
|
||||
</button>
|
||||
|
||||
<i
|
||||
class="icon-cross-large cursor-pointer rounded-md p-1 text-2xl text-gray-600 transition-all hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-950"
|
||||
@click="massActions.indices = []"
|
||||
></i>
|
||||
</div>
|
||||
</slot>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid-mass-action', {
|
||||
template: '#v-datagrid-mass-action-template',
|
||||
|
||||
props: ['available', 'applied'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
|
||||
massActions: {
|
||||
meta: {
|
||||
mode: 'none',
|
||||
|
||||
action: null,
|
||||
},
|
||||
|
||||
indices: [],
|
||||
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.massActions = this.applied.massActions;
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Validate mass action.
|
||||
*
|
||||
* @param {object} filters
|
||||
* @returns {void}
|
||||
*/
|
||||
validateMassAction() {
|
||||
if (! this.massActions.indices.length) {
|
||||
this.$emitter.emit('add-flash', { type: 'warning', message: "@lang('admin::app.components.datagrid.index.no-records-selected')" });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! this.massActions.meta.action) {
|
||||
this.$emitter.emit('add-flash', { type: 'warning', message: "@lang('admin::app.components.datagrid.index.must-select-a-mass-action')" });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.massActions.meta.action?.options?.length &&
|
||||
this.massActions.value === null
|
||||
) {
|
||||
this.$emitter.emit('add-flash', { type: 'warning', message: "@lang('admin::app.components.datagrid.index.must-select-a-mass-action-option')" });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform mass action.
|
||||
*
|
||||
* @param {object} currentAction
|
||||
* @param {object} currentOption
|
||||
* @returns {void}
|
||||
*/
|
||||
performMassAction(currentAction, currentOption = null) {
|
||||
this.massActions.meta.action = currentAction;
|
||||
|
||||
if (currentOption) {
|
||||
this.massActions.value = currentOption.value;
|
||||
}
|
||||
|
||||
if (! this.validateMassAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { action } = this.massActions.meta;
|
||||
|
||||
const method = action.method.toLowerCase();
|
||||
|
||||
this.$emitter.emit('open-confirm-modal', {
|
||||
agree: () => {
|
||||
switch (method) {
|
||||
case 'post':
|
||||
case 'put':
|
||||
case 'patch':
|
||||
this.$axios[method](action.url, {
|
||||
indices: this.massActions.indices,
|
||||
value: this.massActions.value,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
this.$parent.$parent.get();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
this.$parent.$parent.get();
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
this.$axios[method](action.url, {
|
||||
indices: this.massActions.indices
|
||||
})
|
||||
.then(response => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
|
||||
/**
|
||||
* Need to check reason why this.$emit('massActionSuccess') not emitting.
|
||||
*/
|
||||
this.$parent.$parent.get();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
|
||||
/**
|
||||
* Need to check reason why this.$emit('massActionSuccess') not emitting.
|
||||
*/
|
||||
this.$parent.$parent.get();
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error('Method not supported.');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.massActions.indices = [];
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,157 @@
|
||||
<v-datagrid-pagination
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
@changePage="changePage"
|
||||
@changePerPageOption="changePerPageOption"
|
||||
>
|
||||
{{ $slot }}
|
||||
</v-datagrid-pagination>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-pagination-template"
|
||||
>
|
||||
<slot
|
||||
name="pagination"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:change-page="changePage"
|
||||
:change-per-page-option="changePerPageOption"
|
||||
>
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.datagrid.toolbar.pagination />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="whitespace-nowrap text-gray-600 dark:text-gray-300 max-sm:hidden">
|
||||
@lang('admin::app.components.datagrid.toolbar.per-page')
|
||||
</p>
|
||||
|
||||
<x-admin::dropdown>
|
||||
<x-slot:toggle>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex w-full max-w-max cursor-pointer appearance-none items-center justify-between gap-x-2 rounded-md border bg-white px-2.5 py-1.5 text-center leading-6 text-gray-600 transition-all marker:shadow hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
>
|
||||
<span>
|
||||
@{{ applied.pagination.perPage }}
|
||||
</span>
|
||||
|
||||
<span class="icon-down-arrow text-2xl"></span>
|
||||
</button>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:menu>
|
||||
<x-admin::dropdown.menu.item
|
||||
v-for="perPageOption in available.meta.per_page_options"
|
||||
@click="changePerPageOption(perPageOption)"
|
||||
>
|
||||
@{{ perPageOption }}
|
||||
</x-admin::dropdown.menu.item>
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
|
||||
<div class="whitespace-nowrap text-gray-600 dark:text-gray-300">
|
||||
<span>
|
||||
@{{ available.meta.from ?? 0 }} - @{{ available.meta.to ?? 0 }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
@lang('admin::app.components.datagrid.toolbar.of')
|
||||
</span>
|
||||
|
||||
<span>
|
||||
@{{ available.meta.total }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="inline-flex w-full max-w-max cursor-pointer appearance-none items-center justify-between gap-x-1 rounded-md border border-transparent p-1.5 text-center text-gray-600 transition-all marker:shadow hover:bg-gray-200 active:border-gray-300 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
@click="changePage('previous')"
|
||||
>
|
||||
<span class="icon-left-arrow rtl:icon-right-arrow text-2xl"></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="inline-flex w-full max-w-max cursor-pointer appearance-none items-center justify-between gap-x-1 rounded-md border border-transparent p-1.5 text-center text-gray-600 transition-all marker:shadow hover:bg-gray-200 active:border-gray-300 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
@click="changePage('next')"
|
||||
>
|
||||
<span class="icon-right-arrow rtl:icon-left-arrow text-2xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</slot>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid-pagination', {
|
||||
template: '#v-datagrid-pagination-template',
|
||||
|
||||
props: ['isLoading', 'available', 'applied'],
|
||||
|
||||
emits: ['changePage', 'changePerPageOption'],
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Change Page.
|
||||
*
|
||||
* The reason for choosing the numeric approach over the URL approach is to prevent any conflicts with our existing
|
||||
* URLs. If we were to use the URL approach, it would introduce additional arguments in the `get` method, necessitating
|
||||
* the addition of a `url` prop. Instead, by using the numeric approach, we can let Axios handle all the query parameters
|
||||
* using the `applied` prop. This allows for a cleaner and more straightforward implementation.
|
||||
*
|
||||
* @param {string|integer} directionOrPageNumber
|
||||
* @returns {void}
|
||||
*/
|
||||
changePage(directionOrPageNumber) {
|
||||
let newPage;
|
||||
|
||||
if (typeof directionOrPageNumber === 'string') {
|
||||
if (directionOrPageNumber === 'previous') {
|
||||
newPage = this.available.meta.current_page - 1;
|
||||
} else if (directionOrPageNumber === 'next') {
|
||||
newPage = this.available.meta.current_page + 1;
|
||||
} else {
|
||||
console.warn('Invalid Direction Provided : ' + directionOrPageNumber);
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (typeof directionOrPageNumber === 'number') {
|
||||
newPage = directionOrPageNumber;
|
||||
} else {
|
||||
console.warn('Invalid Input Provided: ' + directionOrPageNumber);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the `newPage` is within the valid range.
|
||||
*/
|
||||
if (newPage >= 1 && newPage <= this.available.meta.last_page) {
|
||||
this.$emit('changePage', newPage);
|
||||
} else {
|
||||
console.warn('Invalid Page Provided: ' + newPage);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Change per page option.
|
||||
*
|
||||
* @param {integer} option
|
||||
* @returns {void}
|
||||
*/
|
||||
changePerPageOption(option) {
|
||||
this.$emit('changePerPageOption', option);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,157 @@
|
||||
<v-datagrid-search
|
||||
:is-loading="isLoading"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:src="src"
|
||||
@search="search"
|
||||
@filter="filter"
|
||||
@applySavedFilter="applySavedFilter"
|
||||
>
|
||||
{{ $slot }}
|
||||
</v-datagrid-search>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datagrid-search-template"
|
||||
>
|
||||
<slot
|
||||
name="search"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:search="search"
|
||||
:get-searched-values="getSearchedValues"
|
||||
>
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.datagrid.toolbar.search />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex w-full items-center gap-x-1.5">
|
||||
<!-- Search Panel -->
|
||||
<div class="flex max-w-[445px] items-center max-sm:w-full max-sm:max-w-full">
|
||||
<div class="relative w-full">
|
||||
<div class="icon-search absolute top-1.5 flex items-center text-2xl ltr:left-3 rtl:right-3"></div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
:value="getSearchedValues('all')"
|
||||
class="block w-full rounded-lg border bg-white py-1.5 leading-6 text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400 ltr:pl-10 ltr:pr-3 rtl:pl-3 rtl:pr-10"
|
||||
placeholder="@lang('admin::app.components.datagrid.toolbar.search.title')"
|
||||
autocomplete="off"
|
||||
@keyup.enter="search"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Panel -->
|
||||
<x-admin::datagrid.toolbar.filter>
|
||||
<template #filter="{
|
||||
available,
|
||||
applied,
|
||||
filters,
|
||||
applyFilter,
|
||||
applyColumnValues,
|
||||
findAppliedColumn,
|
||||
hasAnyAppliedColumnValues,
|
||||
getAppliedColumnValues,
|
||||
removeAppliedColumnValue,
|
||||
removeAppliedColumnAllValues
|
||||
}">
|
||||
<slot
|
||||
name="filter"
|
||||
:available="available"
|
||||
:applied="applied"
|
||||
:filters="filters"
|
||||
:apply-filter="applyFilter"
|
||||
:apply-column-values="applyColumnValues"
|
||||
:find-applied-column="findAppliedColumn"
|
||||
:has-any-applied-column-values="hasAnyAppliedColumnValues"
|
||||
:get-applied-column-values="getAppliedColumnValues"
|
||||
:remove-applied-column-value="removeAppliedColumnValue"
|
||||
:remove-applied-column-all-values="removeAppliedColumnAllValues"
|
||||
>
|
||||
</slot>
|
||||
</template>
|
||||
</x-admin::datagrid.toolbar.filter>
|
||||
</div>
|
||||
</template>
|
||||
</slot>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datagrid-search', {
|
||||
template: '#v-datagrid-search-template',
|
||||
|
||||
props: ['isLoading', 'available', 'applied', 'src'],
|
||||
|
||||
emits: ['search', 'filter', 'applySavedFilter'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
filters: {
|
||||
columns: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.filters.columns = this.applied.filters.columns.filter((column) => column.index === 'all');
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Perform a search operation based on the input value.
|
||||
*
|
||||
* @param {Event} $event
|
||||
* @returns {void}
|
||||
*/
|
||||
search($event) {
|
||||
let requestedValue = $event.target.value;
|
||||
|
||||
let appliedColumn = this.filters.columns.find(column => column.index === 'all');
|
||||
|
||||
if (! requestedValue) {
|
||||
appliedColumn.value = [];
|
||||
|
||||
this.$emit('search', this.filters);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (appliedColumn) {
|
||||
appliedColumn.value = [requestedValue];
|
||||
} else {
|
||||
this.filters.columns.push({
|
||||
index: 'all',
|
||||
value: [requestedValue]
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('search', this.filters);
|
||||
},
|
||||
|
||||
filter(filter) {
|
||||
this.$emit('filter', filter);
|
||||
},
|
||||
|
||||
applySavedFilter(filter) {
|
||||
this.$emit('applySavedFilter', filter);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the searched values for a specific column.
|
||||
*
|
||||
* @param {string} columnIndex
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSearchedValues(columnIndex) {
|
||||
let appliedColumn = this.filters.columns.find(column => column.index === 'all');
|
||||
|
||||
return appliedColumn?.value ?? [];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,201 @@
|
||||
@props([
|
||||
'isActive' => false,
|
||||
'position' => 'right',
|
||||
'width' => '500px',
|
||||
])
|
||||
|
||||
<v-drawer
|
||||
{{ $attributes }}
|
||||
is-active="{{ $isActive }}"
|
||||
position="{{ $position }}"
|
||||
width="{{ $width }}"
|
||||
>
|
||||
@isset($toggle)
|
||||
<template v-slot:toggle>
|
||||
{{ $toggle }}
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($header)
|
||||
<template v-slot:header="{ close }">
|
||||
<div {{ $header->attributes->merge(['class' => 'flex justify-between items-center gap-y-2.5 border-b p-3 dark:border-gray-800 max-sm:px-4']) }}>
|
||||
{{ $header }}
|
||||
|
||||
<div class="w-full flex-1 ltr:right-3 ltr:text-right rtl:left-3 rtl:text-left">
|
||||
<span
|
||||
class="icon-cross-large cursor-pointer text-3xl hover:rounded-md hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="close"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($content)
|
||||
<template v-slot:content>
|
||||
<div {{ $content->attributes->merge(['class' => 'flex-1 overflow-auto p-3 max-sm:px-4']) }}>
|
||||
{{ $content }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($footer)
|
||||
<template v-slot:footer>
|
||||
<div {{ $footer->attributes->merge(['class' => 'pb-8']) }}>
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
</v-drawer>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-drawer-template"
|
||||
>
|
||||
<div>
|
||||
<!-- Toggler -->
|
||||
<div @click="open">
|
||||
<slot name="toggle">
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<transition
|
||||
tag="div"
|
||||
name="drawer-overlay"
|
||||
enter-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-[10002] bg-gray-500 bg-opacity-50 transition-opacity"
|
||||
v-show="isOpen"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<!-- Content -->
|
||||
<transition
|
||||
tag="div"
|
||||
name="drawer"
|
||||
:enter-from-class="enterFromLeaveToClasses"
|
||||
enter-active-class="transform transition duration-200 ease-in-out"
|
||||
enter-to-class="translate-x-0"
|
||||
leave-from-class="translate-x-0"
|
||||
leave-active-class="transform transition duration-200 ease-in-out"
|
||||
:leave-to-class="enterFromLeaveToClasses"
|
||||
>
|
||||
<div
|
||||
class="fixed z-[10003] m-3 rounded-lg bg-white dark:bg-gray-900 max-sm:!w-[calc(100%-24px)]"
|
||||
:class="{
|
||||
'inset-x-0 top-0': position == 'top',
|
||||
'inset-x-0 bottom-0': position == 'bottom',
|
||||
'inset-y-0 ltr:right-0 rtl:left-0': position == 'right',
|
||||
'inset-y-0 ltr:left-0 rtl:right-0': position == 'left'
|
||||
}"
|
||||
:style="'width:' + width"
|
||||
v-if="isOpen"
|
||||
>
|
||||
<div class="pointer-events-auto h-full w-full overflow-auto rounded-lg bg-white dark:bg-gray-900">
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div class="min-h-0 min-w-0 flex-1 overflow-auto">
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Header Slot-->
|
||||
<slot
|
||||
name="header"
|
||||
:close="close"
|
||||
>
|
||||
</slot>
|
||||
|
||||
<!-- Content Slot -->
|
||||
<slot name="content"></slot>
|
||||
|
||||
<!-- Footer Slot -->
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-drawer', {
|
||||
template: '#v-drawer-template',
|
||||
|
||||
props: [
|
||||
'isActive',
|
||||
'position',
|
||||
'width'
|
||||
],
|
||||
|
||||
emits: [
|
||||
'toggle',
|
||||
'open',
|
||||
'close',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: this.isActive,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
isActive: function(newVal, oldVal) {
|
||||
this.isOpen = newVal;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
enterFromLeaveToClasses() {
|
||||
if (this.position == 'top') {
|
||||
return '-translate-y-full';
|
||||
} else if (this.position == 'bottom') {
|
||||
return 'translate-y-full';
|
||||
} else if (this.position == 'left') {
|
||||
return 'ltr:-translate-x-full rtl:translate-x-full';
|
||||
} else if (this.position == 'right') {
|
||||
return 'ltr:translate-x-full rtl:-translate-x-full';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isOpen = ! this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow ='auto';
|
||||
}
|
||||
|
||||
this.$emit('toggle', { isActive: this.isOpen });
|
||||
},
|
||||
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
this.$emit('open', { isActive: this.isOpen });
|
||||
},
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
|
||||
document.body.style.overflow = 'auto';
|
||||
|
||||
this.$emit('close', { isActive: this.isOpen });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,170 @@
|
||||
@props(['position' => 'bottom-left'])
|
||||
|
||||
<v-dropdown position="{{ $position }}" {{ $attributes->merge(['class' => 'relative']) }}>
|
||||
@isset($toggle)
|
||||
{{ $toggle }}
|
||||
|
||||
<template v-slot:toggle>
|
||||
{{ $toggle }}
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($content)
|
||||
<template #content="{ isActive, positionStyles }">
|
||||
<div
|
||||
{{ $content->attributes->merge(['class' => 'absolute z-10 w-max rounded bg-white py-5 shadow-[0px_10px_20px_0px_#0000001F] dark:bg-gray-900 border border-gray-300 dark:border-gray-800']) }}
|
||||
:style="positionStyles"
|
||||
v-show="isActive"
|
||||
>
|
||||
{{ $content }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($menu)
|
||||
<template #menu="{ isActive, positionStyles }">
|
||||
<ul
|
||||
{{ $menu->attributes->merge(['class' => 'absolute z-10 w-max rounded bg-white py-4 shadow-[0px_10px_20px_0px_#0000001F] dark:bg-gray-900']) }}
|
||||
:style="positionStyles"
|
||||
v-show="isActive"
|
||||
>
|
||||
{{ $menu }}
|
||||
</ul>
|
||||
</template>
|
||||
@endisset
|
||||
</v-dropdown>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-dropdown-template"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="flex select-none items-center"
|
||||
ref="toggleBlock"
|
||||
@click="toggle()"
|
||||
>
|
||||
<slot name="toggle"></slot>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
tag="div"
|
||||
name="dropdown"
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="scale-95 transform opacity-0"
|
||||
enter-to-class="scale-100 transform opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="scale-100 transform opacity-100"
|
||||
leave-to-class="scale-95 transform opacity-0"
|
||||
>
|
||||
<div>
|
||||
<slot
|
||||
name="content"
|
||||
:position-styles="positionStyles"
|
||||
:is-active="isActive"
|
||||
></slot>
|
||||
|
||||
<slot
|
||||
name="menu"
|
||||
:position-styles="positionStyles"
|
||||
:is-active="isActive"
|
||||
></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-dropdown', {
|
||||
template: '#v-dropdown-template',
|
||||
|
||||
props: {
|
||||
position: String,
|
||||
|
||||
closeOnClick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
toggleBlockWidth: 0,
|
||||
|
||||
toggleBlockHeight: 0,
|
||||
|
||||
isActive: false,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.toggleBlockWidth = this.$refs.toggleBlock.clientWidth;
|
||||
|
||||
this.toggleBlockHeight = this.$refs.toggleBlock.clientHeight;
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
computed: {
|
||||
positionStyles() {
|
||||
switch (this.position) {
|
||||
case 'bottom-left':
|
||||
return [
|
||||
`min-width: ${this.toggleBlockWidth}px`,
|
||||
`top: ${this.toggleBlockHeight}px`,
|
||||
'left: 0',
|
||||
];
|
||||
|
||||
case 'bottom-right':
|
||||
return [
|
||||
`min-width: ${this.toggleBlockWidth}px`,
|
||||
`top: ${this.toggleBlockHeight}px`,
|
||||
'right: 0',
|
||||
];
|
||||
|
||||
case 'top-left':
|
||||
return [
|
||||
`min-width: ${this.toggleBlockWidth}px`,
|
||||
`bottom: ${this.toggleBlockHeight*2}px`,
|
||||
'left: 0',
|
||||
];
|
||||
|
||||
case 'top-right':
|
||||
return [
|
||||
`min-width: ${this.toggleBlockWidth}px`,
|
||||
`bottom: ${this.toggleBlockHeight*2}px`,
|
||||
'right: 0',
|
||||
];
|
||||
|
||||
default:
|
||||
return [
|
||||
`min-width: ${this.toggleBlockWidth}px`,
|
||||
`top: ${this.toggleBlockHeight}px`,
|
||||
'left: 0',
|
||||
];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isActive = ! this.isActive;
|
||||
},
|
||||
|
||||
handleFocusOut(e) {
|
||||
if (! this.$el.contains(e.target) || (this.closeOnClick && this.$el.children[1].contains(e.target))) {
|
||||
this.isActive = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,3 @@
|
||||
<li {{ $attributes->merge(['class' => 'cursor-pointer px-5 py-2 text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-950 ']) }}>
|
||||
{{ $slot }}
|
||||
</li>
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
@php
|
||||
$bgColors = ['bg-orange-100', 'bg-red-100', 'bg-green-100', 'bg-blue-100', 'bg-purple-100'];
|
||||
$textColors = ['text-orange-800', 'text-red-800', 'text-green-800', 'text-blue-800', 'text-purple-800'];
|
||||
@endphp
|
||||
@foreach ($bgColors as $bgColor)
|
||||
<div class="{{ $bgColor }}"></div>
|
||||
@endforeach
|
||||
|
||||
@foreach ($textColors as $textColor)
|
||||
<div class="{{ $textColor }}"></div>
|
||||
@endforeach
|
||||
@@ -0,0 +1,64 @@
|
||||
<v-flash-group ref='flashes'></v-flash-group>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-flash-group-template"
|
||||
>
|
||||
<transition-group
|
||||
tag='div'
|
||||
name="flash-group"
|
||||
enter-from-class="ltr:translate-y-full rtl:-translate-y-full"
|
||||
enter-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
enter-to-class="ltr:translate-y-0 rtl:-translate-y-0"
|
||||
leave-from-class="ltr:translate-y-0 rtl:-translate-y-0"
|
||||
leave-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
leave-to-class="ltr:translate-y-full rtl:-translate-y-full"
|
||||
class='fixed bottom-5 left-1/2 z-[10003] grid -translate-x-1/2 justify-items-end gap-2.5'
|
||||
>
|
||||
<x-admin::flash-group.item />
|
||||
</transition-group>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-flash-group', {
|
||||
template: '#v-flash-group-template',
|
||||
|
||||
data() {
|
||||
return {
|
||||
uid: 0,
|
||||
|
||||
flashes: []
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
@foreach (['success', 'warning', 'error', 'info'] as $key)
|
||||
@if (session()->has($key))
|
||||
this.flashes.push({'type': '{{ $key }}', 'message': "{{ session($key) }}", 'uid': this.uid++});
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
this.registerGlobalEvents();
|
||||
},
|
||||
|
||||
methods: {
|
||||
add(flash) {
|
||||
flash.uid = this.uid++;
|
||||
|
||||
this.flashes.push(flash);
|
||||
},
|
||||
|
||||
remove(flash) {
|
||||
let index = this.flashes.indexOf(flash);
|
||||
|
||||
this.flashes.splice(index, 1);
|
||||
},
|
||||
|
||||
registerGlobalEvents() {
|
||||
this.$emitter.on('add-flash', this.add);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpushOnce
|
||||
@@ -0,0 +1,205 @@
|
||||
<v-flash-item
|
||||
v-for='flash in flashes'
|
||||
:key='flash.uid'
|
||||
:flash="flash"
|
||||
@onRemove="remove($event)"
|
||||
>
|
||||
</v-flash-item>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-flash-item-template"
|
||||
>
|
||||
<div
|
||||
class="flex w-max items-start justify-between gap-2 rounded-lg bg-white p-3 shadow-[0px_10px_20px_0px_rgba(0,0,0,0.12)] dark:bg-gray-950"
|
||||
:style="typeStyles[flash.type]['container']"
|
||||
@mouseenter="pauseTimer"
|
||||
@mouseleave="resumeTimer"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<span
|
||||
class="icon-toast-done rounded-full bg-white text-2xl dark:bg-gray-900"
|
||||
:class="iconClasses[flash.type]"
|
||||
:style="typeStyles[flash.type]['icon']"
|
||||
></span>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<!-- Heading -->
|
||||
<p class="text-base font-semibold dark:text-white">
|
||||
@{{ typeHeadings[flash.type] }}
|
||||
</p>
|
||||
|
||||
<!-- Message -->
|
||||
<p
|
||||
class="flex items-center break-all text-sm dark:text-white"
|
||||
:style="typeStyles[flash.type]['message']"
|
||||
>
|
||||
|
||||
@{{ flash.message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="relative ml-4 inline-flex rounded-full bg-white p-1.5 text-gray-400 dark:bg-gray-950"
|
||||
@click="remove"
|
||||
>
|
||||
<span class="icon-cross-large text-2xl text-slate-800"></span>
|
||||
|
||||
<svg class="absolute inset-0 h-full w-full -rotate-90" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="text-gray-200"
|
||||
stroke-width="1.5"
|
||||
stroke="#D0D4DA"
|
||||
fill="transparent"
|
||||
r="10"
|
||||
cx="12"
|
||||
cy="12"
|
||||
/>
|
||||
|
||||
<circle
|
||||
class="text-blue-600 transition-all duration-100 ease-out"
|
||||
stroke-width="1.5"
|
||||
:stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="strokeDashoffset"
|
||||
stroke-linecap="round"
|
||||
:stroke="typeStyles[flash.type]['stroke']"
|
||||
fill="transparent"
|
||||
r="10"
|
||||
cx="12"
|
||||
cy="12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-flash-item', {
|
||||
template: '#v-flash-item-template',
|
||||
|
||||
props: ['flash'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
iconClasses: {
|
||||
success: 'icon-success',
|
||||
|
||||
error: 'icon-error',
|
||||
|
||||
warning: 'icon-warning',
|
||||
|
||||
info: 'icon-info',
|
||||
},
|
||||
|
||||
typeHeadings: {
|
||||
success: "@lang('admin::app.components.flash-group.success')",
|
||||
|
||||
error: "@lang('admin::app.components.flash-group.error')",
|
||||
|
||||
warning: "@lang('admin::app.components.flash-group.warning')",
|
||||
|
||||
info: "@lang('admin::app.components.flash-group.info')",
|
||||
},
|
||||
|
||||
typeStyles: {
|
||||
success: {
|
||||
container: 'border-left: 8px solid #16A34A',
|
||||
|
||||
icon: 'color: #16A34A',
|
||||
|
||||
stroke: '#16A34A',
|
||||
},
|
||||
|
||||
error: {
|
||||
container: 'border-left: 8px solid #FF4D50',
|
||||
|
||||
icon: 'color: #FF4D50',
|
||||
|
||||
stroke: '#FF4D50',
|
||||
},
|
||||
|
||||
warning: {
|
||||
container: 'border-left: 8px solid #FBAD15',
|
||||
|
||||
icon: 'color: #FBAD15',
|
||||
|
||||
stroke: '#FBAD15',
|
||||
},
|
||||
|
||||
info: {
|
||||
container: 'border-left: 8px solid #0E90D9',
|
||||
|
||||
icon: 'color: #0E90D9',
|
||||
|
||||
stroke: '#0E90D9',
|
||||
},
|
||||
},
|
||||
|
||||
duration: 5000,
|
||||
|
||||
progress: 0,
|
||||
|
||||
circumference: 2 * Math.PI * 10,
|
||||
|
||||
timer: null,
|
||||
|
||||
isPaused: false,
|
||||
|
||||
remainingTime: 5000,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
strokeDashoffset() {
|
||||
return this.circumference - (this.progress / 100) * this.circumference;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.startTimer();
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.stopTimer();
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove() {
|
||||
this.$emit('onRemove', this.flash)
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
const interval = 100;
|
||||
|
||||
const step = (100 / (this.duration / interval));
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (! this.isPaused) {
|
||||
this.progress += step;
|
||||
|
||||
this.remainingTime -= interval;
|
||||
|
||||
if (this.progress >= 100) {
|
||||
this.stopTimer();
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
}, interval);
|
||||
},
|
||||
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
|
||||
pauseTimer() {
|
||||
this.isPaused = true;
|
||||
},
|
||||
|
||||
resumeTimer() {
|
||||
this.isPaused = false;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpushOnce
|
||||
@@ -0,0 +1,81 @@
|
||||
<v-date-picker {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</v-date-picker>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-date-picker-template"
|
||||
>
|
||||
<span class="relative inline-block w-full">
|
||||
<slot></slot>
|
||||
|
||||
<i class="icon-calendar pointer-events-none absolute top-1/2 -translate-y-1/2 text-2xl text-gray-400 ltr:right-2 rtl:left-2"></i>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-date-picker', {
|
||||
template: '#v-date-picker-template',
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
|
||||
value: String,
|
||||
|
||||
allowInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
disable: Array,
|
||||
|
||||
minDate: String,
|
||||
|
||||
maxDate: String,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
datepicker: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
let options = this.setOptions();
|
||||
|
||||
this.activate(options);
|
||||
},
|
||||
|
||||
methods: {
|
||||
setOptions: function() {
|
||||
let self = this;
|
||||
|
||||
return {
|
||||
allowInput: this.allowInput ?? true,
|
||||
disable: this.disable ?? [],
|
||||
minDate: this.minDate ?? '',
|
||||
maxDate: this.maxDate ?? '',
|
||||
altFormat: "Y-m-d",
|
||||
dateFormat: "Y-m-d",
|
||||
weekNumbers: true,
|
||||
|
||||
onChange: function(selectedDates, dateStr, instance) {
|
||||
self.$emit("onChange", dateStr);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
activate: function(options) {
|
||||
let element = this.$el.getElementsByTagName("input")[0];
|
||||
|
||||
this.datepicker = new Flatpickr(element, options);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.datepicker.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,83 @@
|
||||
<v-datetime-picker {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</v-datetime-picker>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-datetime-picker-template"
|
||||
>
|
||||
<span class="relative inline-block w-full">
|
||||
<slot></slot>
|
||||
|
||||
<i class="icon-calendar pointer-events-none absolute top-1/2 -translate-y-1/2 text-2xl text-gray-400 ltr:right-2 rtl:left-2"></i>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-datetime-picker', {
|
||||
template: '#v-datetime-picker-template',
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
|
||||
value: String,
|
||||
|
||||
allowInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
disable: Array,
|
||||
|
||||
minDate: String,
|
||||
|
||||
maxDate: String,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
datepicker: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
let options = this.setOptions();
|
||||
|
||||
this.activate(options);
|
||||
},
|
||||
|
||||
methods: {
|
||||
setOptions: function() {
|
||||
let self = this;
|
||||
|
||||
return {
|
||||
allowInput: this.allowInput ?? true,
|
||||
disable: this.disable ?? [],
|
||||
minDate: this.minDate ?? '',
|
||||
maxDate: this.maxDate ?? '',
|
||||
altFormat: "Y-m-d H:i:S",
|
||||
dateFormat: "Y-m-d H:i:S",
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
weekNumbers: true,
|
||||
|
||||
onChange: function(selectedDates, dateStr, instance) {
|
||||
self.$emit("onChange", dateStr);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
activate: function(options) {
|
||||
let element = this.$el.getElementsByTagName("input")[0];
|
||||
|
||||
this.datepicker = new Flatpickr(element, options);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.datepicker.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,353 @@
|
||||
@props([
|
||||
'type' => 'text',
|
||||
'name' => '',
|
||||
])
|
||||
|
||||
@switch($type)
|
||||
@case('hidden')
|
||||
@case('text')
|
||||
@case('email')
|
||||
@case('password')
|
||||
@case('number')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<input
|
||||
type="{{ $type }}"
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
|
||||
/>
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('price')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center overflow-hidden rounded-md border text-sm text-gray-600 transition-all focus-within:border-gray-400 hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
>
|
||||
@if (isset($currency))
|
||||
<span {{ $currency->attributes->merge(['class' => 'py-2.5 text-gray-500 ltr:pl-4 rtl:pr-4']) }}>
|
||||
{{ $currency }}
|
||||
</span>
|
||||
@else
|
||||
<span class="py-2.5 text-gray-500 ltr:pl-4 rtl:pr-4">
|
||||
{{ config('app.currency') }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full p-2.5 text-sm text-gray-600 dark:bg-gray-900 dark:text-gray-300']) }}
|
||||
/>
|
||||
</div>
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('file')
|
||||
<v-field
|
||||
v-slot="{ field, errors, handleChange, handleBlur }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<input
|
||||
type="{{ $type }}"
|
||||
v-bind="{ name: field.name }"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full dark:file:bg-gray-800 dark:file:dark:text-white rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
|
||||
@change="handleChange"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('color')
|
||||
<v-field
|
||||
name="{{ $name }}"
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->except('class') }}
|
||||
>
|
||||
<input
|
||||
type="{{ $type }}"
|
||||
:class="[errors.length ? 'border border-red-500' : '']"
|
||||
v-bind="field"
|
||||
{{ $attributes->except(['value'])->merge(['class' => 'w-full appearance-none rounded-md border text-sm text-gray-600 transition-all hover:border-gray-400 dark:text-gray-300 dark:hover:border-gray-400']) }}
|
||||
>
|
||||
</v-field>
|
||||
@break
|
||||
|
||||
@case('textarea')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
@php
|
||||
$defaultAttributes = [
|
||||
'class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400'
|
||||
];
|
||||
|
||||
if ($attributes->get('tinymce', false) || $attributes->get(':tinymce', false)) {
|
||||
$defaultAttributes['id'] = $attributes->get(':id', 'id');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<textarea
|
||||
type="{{ $type }}"
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{
|
||||
$attributes
|
||||
->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])
|
||||
->merge($defaultAttributes)
|
||||
}}
|
||||
>
|
||||
</textarea>
|
||||
|
||||
@if ($attributes->get('tinymce', false) || $attributes->get(':tinymce', false))
|
||||
<x-admin::tinymce
|
||||
:selector="'textarea#' . ($attributes->get('id') ?? $attributes->get(':id'))"
|
||||
::field="field"
|
||||
/>
|
||||
@endif
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('date')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2}$']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<x-admin::flat-picker.date>
|
||||
<input
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</x-admin::flat-picker.date>
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('datetime')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<x-admin::flat-picker.datetime>
|
||||
<input
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
|
||||
autocomplete="off"
|
||||
>
|
||||
</x-admin::flat-picker.datetime>
|
||||
</v-field>
|
||||
@break
|
||||
|
||||
@case('select')
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<select
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
:class="[errors.length ? 'border border-red-500' : '']"
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'custom-select w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400']) }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('multiselect')
|
||||
<v-field
|
||||
as="select"
|
||||
v-slot="{ value }"
|
||||
:class="[errors && errors['{{ $name }}'] ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes->except([])->merge(['class' => 'flex w-full flex-col rounded-md border bg-white px-3 py-2.5 text-sm font-normal text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400']) }}
|
||||
name="{{ $name }}"
|
||||
multiple
|
||||
>
|
||||
{{ $slot }}
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
<v-field
|
||||
v-slot="{ field }"
|
||||
type="checkbox"
|
||||
class="hidden"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
class="peer sr-only"
|
||||
{{ $attributes->except(['rules', 'label', ':label', 'key', ':key']) }}
|
||||
/>
|
||||
|
||||
<v-checked-handler
|
||||
:field="field"
|
||||
checked="{{ $attributes->get('checked') }}"
|
||||
>
|
||||
</v-checked-handler>
|
||||
</v-field>
|
||||
|
||||
<label
|
||||
{{
|
||||
$attributes
|
||||
->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key'])
|
||||
->merge(['class' => 'text-gray-500 icon-checkbox-outline peer-checked:icon-checkbox-select text-2xl peer-checked:text-brandColor'])
|
||||
->merge(['class' => $attributes->get('disabled') ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'])
|
||||
}}
|
||||
>
|
||||
</label>
|
||||
|
||||
@break
|
||||
|
||||
@case('radio')
|
||||
<v-field
|
||||
type="radio"
|
||||
class="hidden"
|
||||
v-slot="{ field }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="{{ $name }}"
|
||||
v-bind="field"
|
||||
{{ $attributes->except(['rules', 'label', ':label', 'key', ':key'])->merge(['class' => 'peer sr-only']) }}
|
||||
/>
|
||||
|
||||
<v-checked-handler
|
||||
class="hidden"
|
||||
:field="field"
|
||||
checked="{{ $attributes->get('checked') }}"
|
||||
>
|
||||
</v-checked-handler>
|
||||
</v-field>
|
||||
|
||||
<label
|
||||
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key'])->merge(['class' => 'icon-radio-normal peer-checked:icon-radio-selected cursor-pointer text-2xl peer-checked:text-brandColor']) }}
|
||||
>
|
||||
</label>
|
||||
|
||||
@break
|
||||
|
||||
@case('switch')
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<v-field
|
||||
type="checkbox"
|
||||
class="hidden"
|
||||
v-slot="{ field }"
|
||||
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
|
||||
name="{{ $name }}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="{{ $name }}"
|
||||
id="{{ $name }}"
|
||||
class="peer sr-only"
|
||||
v-bind="field"
|
||||
{{ $attributes->except(['v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
|
||||
/>
|
||||
|
||||
<v-checked-handler
|
||||
class="hidden"
|
||||
:field="field"
|
||||
checked="{{ $attributes->get('checked') }}"
|
||||
>
|
||||
</v-checked-handler>
|
||||
</v-field>
|
||||
|
||||
<label
|
||||
class="peer h-5 w-9 cursor-pointer rounded-full bg-gray-200 after:absolute after:top-0.5 after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-brandColor peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-blue-300 dark:bg-gray-800 dark:after:border-white dark:after:bg-white dark:peer-checked:bg-gray-950 after:ltr:left-0.5 peer-checked:after:ltr:translate-x-full after:rtl:right-0.5 peer-checked:after:rtl:-translate-x-full"
|
||||
for="{{ $name }}"
|
||||
></label>
|
||||
</label>
|
||||
|
||||
@break
|
||||
|
||||
@case('image')
|
||||
<x-admin::media.images
|
||||
name="{{ $name }}"
|
||||
::class="[errors && errors['{{ $name }}'] ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
{{ $attributes }}
|
||||
/>
|
||||
|
||||
@break
|
||||
|
||||
@case('inline')
|
||||
<x-admin::form.control-group.controls.inline.text {{ $attributes }} />
|
||||
|
||||
@break
|
||||
|
||||
@case('custom')
|
||||
<v-field {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</v-field>
|
||||
|
||||
@break
|
||||
|
||||
@case('tags')
|
||||
<x-admin::form.control-group.controls.tags
|
||||
:name="$name"
|
||||
:data="$attributes->get(':data') ?? $attributes->get('data')"
|
||||
{{ $attributes}}
|
||||
/>
|
||||
@break
|
||||
@endswitch
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-checked-handler-template"
|
||||
>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-checked-handler', {
|
||||
template: '#v-checked-handler-template',
|
||||
|
||||
props: ['field', 'checked'],
|
||||
|
||||
mounted() {
|
||||
if (this.checked == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.field.checked = true;
|
||||
|
||||
this.field.onChange();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endpushOnce
|
||||
@@ -0,0 +1,370 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-address-edit
|
||||
{{ $attributes->except('value') }}
|
||||
:value='@json($attributes->get('value'))'
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center ltr:pl-2.5 ltr:text-left rtl:pr-2.5 rtl:text-right">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-address-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-address-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : `${inputValue?.address} ${inputValue?.city} ${inputValue?.state} ${inputValue?.postcode} ${inputValue?.country}`.length > 20 ? `${inputValue?.address} ${inputValue?.city} ${inputValue?.state} ${inputValue?.postcode} ${inputValue?.country}`.substring(0, 20) + '...' : `${inputValue?.address} ${inputValue?.city} ${inputValue?.state} ${inputValue?.postcode} ${inputValue?.country}` }}
|
||||
</span>
|
||||
|
||||
<div class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex">
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue?.address }}<br>
|
||||
@{{ `${inputValue?.city}, ${inputValue?.state}, ${inputValue?.postcode}` }}<br>
|
||||
@{{ `${inputValue?.country}` }}<br>
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, updateOrCreate)">
|
||||
<!-- Editing view -->
|
||||
<x-admin::modal ref="emailModal">
|
||||
<!-- Modal Header -->
|
||||
<x-slot:header>
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white">
|
||||
Update Address
|
||||
</p>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<x-slot:content>
|
||||
<div class="flex gap-4">
|
||||
<div class="w-full">
|
||||
<!-- Address (Textarea field) -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="textarea"
|
||||
::name="`${name}.address`"
|
||||
rows="10"
|
||||
::value="inputValue?.address"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name" />
|
||||
</x-admin::form.control-group>
|
||||
</div>
|
||||
|
||||
<div class="grid w-full">
|
||||
<!-- Country Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::name="`${name}.country`"
|
||||
v-model="inputValue.country"
|
||||
>
|
||||
<option value="">@lang('admin::app.common.custom-attributes.select-country')</option>
|
||||
|
||||
@foreach (core()->countries() as $country)
|
||||
<option value="{{ $country->code }}">{{ $country->name }}</option>
|
||||
@endforeach
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<x-admin::form.control-group.error name="country" />
|
||||
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- State Field -->
|
||||
<template v-if="haveStates()">
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::name="`${name}.state`"
|
||||
v-model="inputValue.state"
|
||||
>
|
||||
<option value="">@lang('admin::app.common.custom-attributes.select-state')</option>
|
||||
|
||||
<option v-for='(state, index) in countryStates[inputValue?.country]' :value="state.code">
|
||||
@{{ state.name }}
|
||||
</option>
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<x-admin::form.control-group.error name="country" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="`${name}.state`"
|
||||
v-model="inputValue.state"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error name="state" />
|
||||
</x-admin::form.control-group>
|
||||
</template>
|
||||
|
||||
<!-- City Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="`${name}.city`"
|
||||
::value="inputValue?.city"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error name="city" />
|
||||
</x-admin::form.control-group>
|
||||
|
||||
<!-- Postcode Field -->
|
||||
<x-admin::form.control-group>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="`${name}.postcode`"
|
||||
::value="inputValue?.postcode"
|
||||
:placeholder="trans('admin::app.common.custom-attributes.postcode')"
|
||||
/>
|
||||
|
||||
<x-admin::form.control-group.error name="postcode" />
|
||||
</x-admin::form.control-group>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<x-slot:footer>
|
||||
<!-- Save Button -->
|
||||
<x-admin::button
|
||||
button-type="submit"
|
||||
class="primary-button justify-center"
|
||||
:title="trans('Save')"
|
||||
::loading="isProcessing"
|
||||
::disabled="isProcessing"
|
||||
/>
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
</form>
|
||||
</x-admin::form>
|
||||
</Teleport>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-address-edit', {
|
||||
template: '#v-inline-address-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
emails: JSON.parse(JSON.stringify(this.value || [{'value': '', 'label': 'work'}])),
|
||||
|
||||
isProcessing: false,
|
||||
|
||||
countryStates: @json(core()->groupedStatesByCountries()),
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue, oldValue) {
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
|
||||
this.emails = newValue || [{'value': '', 'label': 'work'}];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.extendValidations();
|
||||
|
||||
if (! this.emails || ! this.emails.length) {
|
||||
this.emails = [{
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the validation rules.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getValidation() {
|
||||
return {
|
||||
email: true,
|
||||
unique_contact_email: this.emails ?? [],
|
||||
required: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.$refs.emailModal.toggle();
|
||||
},
|
||||
|
||||
add() {
|
||||
this.emails.push({
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
});
|
||||
},
|
||||
|
||||
remove(email) {
|
||||
this.emails = this.emails.filter(email => email !== email);
|
||||
},
|
||||
|
||||
extendValidations() {
|
||||
defineRule('unique_contact_email', (value, emails) => {
|
||||
if (
|
||||
! value
|
||||
|| ! value.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const emailOccurrences = emails.filter(email => email.value === value).length;
|
||||
|
||||
if (emailOccurrences > 1) {
|
||||
return 'This email email is already used';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
updateOrCreate(params) {
|
||||
this.inputValue = params[this.name];
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
|
||||
this.$refs.emailModal.toggle();
|
||||
},
|
||||
|
||||
haveStates() {
|
||||
/*
|
||||
* The double negation operator is used to convert the value to a boolean.
|
||||
* It ensures that the final result is a boolean value,
|
||||
* true if the array has a length greater than 0, and otherwise false.
|
||||
*/
|
||||
return !!this.countryStates[this.inputValue.country]?.length;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,250 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-boolean-edit
|
||||
{{ $attributes->except('options') }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-boolean-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-boolean-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative h-[18px] !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : options[inputValue].name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="name"
|
||||
::name="name"
|
||||
class="text-normal py-1 ltr:pr-16 rtl:pl-16"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
::placeholder="placeholder"
|
||||
::style="inputPositionStyle"
|
||||
v-model="inputValue"
|
||||
>
|
||||
<option
|
||||
v-for="(option, index) in options"
|
||||
:key="option.id"
|
||||
:value="option.id"
|
||||
>
|
||||
@{{ option.name }}
|
||||
</option>
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-boolean-edit', {
|
||||
template: '#v-inline-boolean-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
|
||||
options: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'No',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Yes',
|
||||
},
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,245 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-date-edit
|
||||
{{ $attributes }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-date-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-date-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : inputValue.length > 20 ? inputValue.substring(0, 20) + '...' : inputValue }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col ltr:[&>span>i]:right-14 rtl:[&>span>i]:left-14"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="date"
|
||||
::id="name"
|
||||
::name="name"
|
||||
class="text-normal py-1 ltr:pr-20 rtl:pl-20"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
::placeholder="placeholder"
|
||||
::style="inputPositionStyle"
|
||||
v-model="inputValue"
|
||||
ref="input"
|
||||
readonly
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-date-edit', {
|
||||
template: '#v-inline-date-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,245 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-datetime-edit
|
||||
{{ $attributes }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center ltr:pl-2.5 ltr:text-left rtl:pr-2.5 rtl:text-right">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-datetime-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-datetime-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : inputValue.length > 20 ? inputValue.substring(0, 20) + '...' : inputValue }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col ltr:[&>span>i]:right-14 rtl:[&>span>i]:left-14"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="datetime"
|
||||
::id="name"
|
||||
::name="name"
|
||||
class="text-normal py-1 ltr:pr-16 rtl:pl-16"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
::placeholder="placeholder"
|
||||
::style="inputPositionStyle"
|
||||
v-model="inputValue"
|
||||
ref="input"
|
||||
readonly
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 bg-white ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-datetime-edit', {
|
||||
template: '#v-inline-datetime-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,320 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-email-edit
|
||||
{{ $attributes->except('value') }}
|
||||
:value={{ json_encode($attributes->get('value')) }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center ltr:pl-2.5 ltr:text-left rtl:pr-2.5 rtl:text-right">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-email-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-email-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : inputValue?.map(item => `${item.value}(${item.label})`).join(', ').length > 20 ? inputValue?.map(item => `${item.value}(${item.label})`).join(', ').substring(0, 20) + '...' : inputValue?.map(item => `${item.value}(${item.label})`).join(', ') }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue?.map(item => `${item.value}(${item.label})`).join(', ').length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue?.map(item => `${item.value}(${item.label})`).join(', \n') }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, updateOrCreate)">
|
||||
<!-- Editing view -->
|
||||
<x-admin::modal ref="emailModal">
|
||||
<!-- Modal Header -->
|
||||
<x-slot:header>
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white">
|
||||
@lang("admin::app.common.custom-attributes.update-emails-title")
|
||||
</p>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<x-slot:content>
|
||||
<template v-for="(email, index) in emails">
|
||||
<div class="mb-2 flex items-center">
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="`${name}[${index}].value`"
|
||||
::name="`${name}[${index}].value`"
|
||||
class="!rounded-r-none"
|
||||
::rules="getValidation"
|
||||
v-model="email.value"
|
||||
:label="trans('admin::app.common.custom-attributes.email')"
|
||||
/>
|
||||
|
||||
<div class="relative">
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="`${name}[${index}].label`"
|
||||
::name="`${name}[${index}].label`"
|
||||
class="!w-24 !rounded-l-none"
|
||||
::value="email.label"
|
||||
>
|
||||
<option value="work">@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</x-admin::form.control-group.control>
|
||||
</div>
|
||||
|
||||
<i
|
||||
v-if="emails.length > 1"
|
||||
class="icon-delete ml-1 cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="remove(email)"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${name}[${index}].value`"/>
|
||||
</template>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex max-w-max items-center gap-2 text-brandColor"
|
||||
@click="add"
|
||||
>
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<x-slot:footer>
|
||||
<!-- Save Button -->
|
||||
<x-admin::button
|
||||
button-type="submit"
|
||||
class="primary-button justify-center"
|
||||
:title="trans('admin::app.common.custom-attributes.save')"
|
||||
::loading="isProcessing"
|
||||
::disabled="isProcessing"
|
||||
/>
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
</form>
|
||||
</x-admin::form>
|
||||
</Teleport>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-email-edit', {
|
||||
template: '#v-inline-email-edit-template',
|
||||
|
||||
emits: ['on-save'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
emails: JSON.parse(JSON.stringify(this.value || [{'value': '', 'label': 'work'}])),
|
||||
|
||||
isProcessing: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue, oldValue) {
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
|
||||
this.emails = newValue || [{'value': '', 'label': 'work'}];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.extendValidations();
|
||||
|
||||
if (! this.emails || ! this.emails.length) {
|
||||
this.emails = [{
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the validation rules.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getValidation() {
|
||||
return {
|
||||
email: true,
|
||||
unique_contact_email: this.emails ?? [],
|
||||
required: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.$refs.emailModal.toggle();
|
||||
},
|
||||
|
||||
add() {
|
||||
this.emails.push({
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
});
|
||||
},
|
||||
|
||||
remove(contactEmail) {
|
||||
this.emails = this.emails.filter(email => email !== contactEmail);
|
||||
},
|
||||
|
||||
extendValidations() {
|
||||
defineRule('unique_contact_email', (value, emails) => {
|
||||
if (
|
||||
! value
|
||||
|| ! value.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const emailOccurrences = emails.filter(email => email.value === value).length;
|
||||
|
||||
if (emailOccurrences > 1) {
|
||||
return 'This email is already used';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
updateOrCreate(params) {
|
||||
this.inputValue = params.contact_emails ?? params.emails;
|
||||
|
||||
if (this.url) {
|
||||
this.isProcessing = true;
|
||||
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.emails = response.data.data.emails || this.emails;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false;
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-save', params);
|
||||
|
||||
this.$refs.emailModal.toggle();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,245 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-file-edit
|
||||
{{ $attributes }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-file-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-file-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center justify-between rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<a
|
||||
:href="inputValue"
|
||||
v-if="inputValue"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="icon-download pl-[2px] text-2xl font-normal"></span>
|
||||
</a>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="icon-download cursor-pointer pl-[2px] text-2xl font-normal"
|
||||
>
|
||||
</span>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col"
|
||||
v-else
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
:name="name"
|
||||
:id="name"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
class="w-full rounded-md border px-3 py-2.5 text-sm text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:file:bg-gray-800 dark:file:dark:text-white dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
@change="handleChange"
|
||||
ref="input"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-file-edit', {
|
||||
template: '#v-inline-file-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
file: null,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(this.name, this.file);
|
||||
|
||||
formData.append('_method', 'PUT');
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.post(this.url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
handleChange(event) {
|
||||
this.file = event.target.files[0];
|
||||
|
||||
this.inputValue = URL.createObjectURL(this.file);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,242 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-image-edit
|
||||
{{ $attributes }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-image-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-image-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<img
|
||||
:src="inputValue"
|
||||
class="h-5 w-5"
|
||||
:alt="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative w-full"
|
||||
v-else
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
:name="name"
|
||||
:id="name"
|
||||
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
class="!h-[34px] w-full cursor-pointer rounded-md border !py-0 px-3 text-sm text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-white dark:file:bg-gray-800 dark:file:dark:text-white dark:hover:border-gray-400 dark:focus:border-gray-400 ltr:pr-20 rtl:pl-20"
|
||||
@change="handleChange"
|
||||
ref="input"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-image-edit', {
|
||||
template: '#v-inline-image-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
file: null,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(this.name, this.file);
|
||||
|
||||
formData.append('_method', 'PUT');
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.post(this.url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
handleChange(event) {
|
||||
this.file = event.target.files[0];
|
||||
|
||||
this.inputValue = URL.createObjectURL(this.file);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,448 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
'attribute' => [],
|
||||
])
|
||||
|
||||
<v-inline-look-edit
|
||||
{{ $attributes }}
|
||||
:attribute="{{ json_encode($attribute) }}"
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-look-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-look-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative h-[18px] !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
<template v-if="isDirty">
|
||||
@{{ inputValue.length > 20 ? inputValue.substring(0, 20) + '...' : inputValue }}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@{{ valueLabel ? valueLabel : inputValue.length > 20 ? inputValue.substring(0, 20) + '...' : inputValue }}
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col"
|
||||
ref="dropdownContainer"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::name="name"
|
||||
class="!h-[34px] w-full cursor-pointer !py-0 text-gray-800 dark:text-white ltr:pr-20 rtl:pl-20"
|
||||
::placeholder="placeholder"
|
||||
v-model="selectedItem.name"
|
||||
@click="toggleEditor"
|
||||
readonly
|
||||
/>
|
||||
|
||||
<span class="pointer-events-none absolute inset-y-0 flex items-center ltr:right-0 ltr:pr-14 rtl:left-0 rtl:pl-14">
|
||||
<div class="flex items-center justify-center space-x-1">
|
||||
<div
|
||||
class="relative"
|
||||
v-if="isSearching"
|
||||
>
|
||||
<x-admin::spinner />
|
||||
</div>
|
||||
|
||||
<i
|
||||
class="text-2xl"
|
||||
:class="showPopup ? 'icon-up-arrow': 'icon-down-arrow'"
|
||||
></i>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<!-- Popup Box -->
|
||||
<div
|
||||
v-if="showPopup"
|
||||
class="absolute z-10 mt-1 w-full origin-top transform rounded-lg border border-gray-200 bg-white p-2 shadow-lg transition-transform dark:border-gray-800 dark:bg-gray-800"
|
||||
:class="dropdownPosition === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1'"
|
||||
>
|
||||
<!-- Search Bar -->
|
||||
<input
|
||||
type="text"
|
||||
v-model.lazy="searchTerm"
|
||||
v-debounce="200"
|
||||
class="!mb-2 w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
placeholder="@lang('admin::app.components.lookup.search')"
|
||||
ref="searchInput"
|
||||
/>
|
||||
|
||||
<!-- Results List -->
|
||||
<ul class="max-h-40 divide-y divide-gray-100 overflow-y-auto">
|
||||
<li
|
||||
v-for="item in filteredResults"
|
||||
:key="item.id"
|
||||
class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-950"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
@{{ item.name }}
|
||||
</li>
|
||||
|
||||
<li v-if="filteredResults.length === 0" class="px-4 py-2 text-center text-gray-500 dark:text-gray-300">
|
||||
@lang('admin::app.components.lookup.no-results')
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 bg-white dark:bg-gray-900 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="item-center flex justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-look-edit', {
|
||||
template: '#v-inline-look-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
attribute: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...',
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value ?? '',
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isDirty: false,
|
||||
|
||||
showPopup: false,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
selectedItem: {},
|
||||
|
||||
searchedResults: [],
|
||||
|
||||
isSearching: false,
|
||||
|
||||
cancelToken: null,
|
||||
|
||||
isDropdownOpen: false,
|
||||
|
||||
dropdownPosition: "bottom",
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
|
||||
searchTerm(newVal, oldVal) {
|
||||
this.search();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.setDropdownPosition);
|
||||
|
||||
this.$emitter.on('show-pop', this.handleShowPop);
|
||||
},
|
||||
|
||||
computed: {
|
||||
src() {
|
||||
return `{{ route('admin.settings.attributes.lookup') }}/${this.attribute.lookup_type}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the searchedResults based on the search query.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
filteredResults() {
|
||||
return this.searchedResults.filter(item =>
|
||||
item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.searchTerm = '';
|
||||
|
||||
this.selectedItem.name = this.inputValue;
|
||||
|
||||
this.isDropdownOpen = ! this.isDropdownOpen;
|
||||
|
||||
if (this.isDropdownOpen) {
|
||||
this.setDropdownPosition();
|
||||
}
|
||||
},
|
||||
|
||||
toggleEditor() {
|
||||
this.$emitter.emit('show-pop', this.$.uid);
|
||||
},
|
||||
|
||||
handleShowPop(uid) {
|
||||
this.showPopup = (uid === this.$.uid);
|
||||
|
||||
if (this.showPopup) {
|
||||
this.$nextTick(() => this.$refs.searchInput?.focus());
|
||||
} else {
|
||||
this.isEditing = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.selectedItem.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDirty = true;
|
||||
|
||||
this.inputValue = this.selectedItem.name;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.selectedItem.id,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isDirty = false;
|
||||
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.selectedItem.id,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Select an item from the list.
|
||||
*
|
||||
* @param {Object} item
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
selectItem(item) {
|
||||
this.showPopup = false;
|
||||
|
||||
this.searchTerm = '';
|
||||
|
||||
this.selectedItem = item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
if (this.selectItem) {
|
||||
this.inputValue = this.selectedItem.name;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', this.inputValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the items.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
search() {
|
||||
if (this.searchTerm.length <= 2) {
|
||||
this.searchedResults = [];
|
||||
|
||||
this.isSearching = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
|
||||
if (this.cancelToken) {
|
||||
this.cancelToken.cancel();
|
||||
}
|
||||
|
||||
this.cancelToken = this.$axios.CancelToken.source();
|
||||
|
||||
this.$axios.get(this.src, {
|
||||
params: {
|
||||
...this.params,
|
||||
query: this.searchTerm
|
||||
},
|
||||
cancelToken: this.cancelToken.token,
|
||||
})
|
||||
.then(response => {
|
||||
this.searchedResults = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (! this.$axios.isCancel(error)) {
|
||||
console.error("Search request failed:", error);
|
||||
}
|
||||
|
||||
this.isSearching = false;
|
||||
})
|
||||
.finally(() => this.isSearching = false);
|
||||
},
|
||||
|
||||
setDropdownPosition() {
|
||||
this.$nextTick(() => {
|
||||
const dropdownContainer = this.$refs.dropdownContainer;
|
||||
|
||||
if (! dropdownContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdownRect = dropdownContainer.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (dropdownRect.bottom + 250 > viewportHeight) {
|
||||
this.dropdownPosition = "top";
|
||||
} else {
|
||||
this.dropdownPosition = "bottom";
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,367 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
'data' => [],
|
||||
])
|
||||
|
||||
<v-inline-multi-select-edit
|
||||
{{ $attributes->except('data') }}
|
||||
:data="{{ json_encode($data) }}"
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-multi-select-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-multi-select-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative h-[18px] !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : selectedValue?.length > 20 ? selectedValue.substring(0, 20) + '...' : selectedValue }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="selectedValue?.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ selectedValue }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit phttp://192.168.15.43/test/-0.5 cursor-pointer rounded text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative flex w-full flex-col"
|
||||
ref="dropdownContainer"
|
||||
v-if="isEditing"
|
||||
>
|
||||
<div class="flex min-h-[38px] w-full items-center rounded border border-gray-200 px-2.5 py-1.5 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400 ltr:pr-16 rtl:pl-16">
|
||||
<ul class="flex flex-wrap items-center gap-1">
|
||||
<li
|
||||
v-for="option in tempOptions"
|
||||
:key="option.id"
|
||||
class="flex items-center gap-1 rounded-md bg-slate-100 pl-2 dark:bg-gray-800 dark:text-white"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name"
|
||||
:value="option"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="relative h-[18px] pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<!-- Wrap the text and tooltip in a group class for hover tracking -->
|
||||
<div class="group">
|
||||
<!-- Truncated text -->
|
||||
<p class="max-w-[110px] cursor-pointer truncate">@{{ option.name }}</p>
|
||||
|
||||
<!-- Tooltip that shows on hover over the truncated text -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="option.name?.length > 20"
|
||||
>
|
||||
<span
|
||||
class="whitespace-no-wrap relative z-20 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900"
|
||||
>
|
||||
@{{ option.name }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cross icon for removing option -->
|
||||
<span
|
||||
class="icon-cross-large cursor-pointer p-0.5 text-xl"
|
||||
@click="removeOption(option)"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown (position dynamic based on space) -->
|
||||
<div
|
||||
class="absolute z-10 w-full origin-top transform rounded-lg border bg-white shadow-lg dark:border-gray-800 dark:bg-gray-800"
|
||||
:class="dropdownPosition === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1'"
|
||||
v-if="options.length > 0"
|
||||
>
|
||||
<!-- Results List -->
|
||||
<ul class="max-h-40 divide-gray-100 overflow-y-auto p-0.5">
|
||||
<li
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
class="cursor-pointer rounded px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-950 ltr:pr-16 rtl:pl-16"
|
||||
@click="addOption(option)"
|
||||
>
|
||||
@{{ option.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-multi-select-edit', {
|
||||
template: '#v-inline-multi-select-edit-template',
|
||||
|
||||
emits: ['options-updated'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
options: this.data ?? [],
|
||||
|
||||
tempOptions: [],
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
|
||||
isDropdownOpen: false,
|
||||
|
||||
dropdownPosition: "bottom",
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.tempOptions = this.options.filter((data) => this.value.includes(data.id));
|
||||
|
||||
this.options = this.options.filter((data) => !this.value.includes(data.id));
|
||||
|
||||
window.addEventListener("resize", this.setDropdownPosition);
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the selected value.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
selectedValue() {
|
||||
if (this.tempOptions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.tempOptions.map((data) => data.name).join(', ');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.isDropdownOpen = ! this.isDropdownOpen;
|
||||
|
||||
if (this.isDropdownOpen) {
|
||||
this.setDropdownPosition();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.tempOptions.map((data) => data.id),
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('options-updated', {
|
||||
name: this.name,
|
||||
value: this.tempOptions.map((data) => data.id),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('options-updated', {
|
||||
name: this.name,
|
||||
value: this.tempOptions.map((data) => data.id),
|
||||
});
|
||||
},
|
||||
|
||||
addOption(option) {
|
||||
if (!this.tempOptions.some((data) => data.id === option.id)) {
|
||||
this.tempOptions.push(option);
|
||||
|
||||
this.options = this.options.filter((data) => data.id !== option.id);
|
||||
|
||||
this.input = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeOption(option) {
|
||||
if (!this.options.some((data) => data.id === option.id)) {
|
||||
this.options.push(option);
|
||||
|
||||
this.tempOptions = this.tempOptions.filter((data) => data.id !== option.id);
|
||||
}
|
||||
},
|
||||
|
||||
setDropdownPosition() {
|
||||
this.$nextTick(() => {
|
||||
const dropdownContainer = this.$refs.dropdownContainer;
|
||||
|
||||
if (! dropdownContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdownRect = dropdownContainer.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (dropdownRect.bottom + 250 > viewportHeight) {
|
||||
this.dropdownPosition = "top";
|
||||
} else {
|
||||
this.dropdownPosition = "bottom";
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,321 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-phone-edit
|
||||
{{ $attributes->except('value') }}
|
||||
:value={{ json_encode($attributes->get('value')) }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-phone-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-phone-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : inputValue.map(item => `${item.value}(${item.label})`).join(', ').length > 20 ? inputValue.map(item => `${item.value}(${item.label})`).join(', ').substring(0, 20) + '...' : inputValue.map(item => `${item.value}(${item.label})`).join(', ') }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue.map(item => `${item.value}(${item.label})`).join(', ').length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue.map(item => `${item.value}(${item.label})`).join(', \n') }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<x-admin::form
|
||||
v-slot="{ meta, errors, handleSubmit }"
|
||||
as="div"
|
||||
ref="modalForm"
|
||||
>
|
||||
<form @submit="handleSubmit($event, updateOrCreate)">
|
||||
<!-- Editing view -->
|
||||
<x-admin::modal ref="phoneNumberModal">
|
||||
<!-- Modal Header -->
|
||||
<x-slot:header>
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white">
|
||||
@lang("admin::app.common.custom-attributes.update-contact-title")
|
||||
</p>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<x-slot:content>
|
||||
<template v-for="(contactNumber, index) in contactNumbers">
|
||||
<div class="mb-2 flex items-center">
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="`${name}[${index}].value`"
|
||||
::name="`${name}[${index}].value`"
|
||||
class="!rounded-r-none"
|
||||
::rules="getValidation"
|
||||
:label="trans('admin::app.common.custom-attributes.contact')"
|
||||
v-model="contactNumber.value"
|
||||
/>
|
||||
|
||||
<div class="relative">
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="`${name}[${index}].label`"
|
||||
::name="`${name}[${index}].label`"
|
||||
class="!w-24 !rounded-l-none"
|
||||
::value="contactNumber.label"
|
||||
>
|
||||
<option value="work">@lang('admin::app.common.custom-attributes.work')</option>
|
||||
<option value="home">@lang('admin::app.common.custom-attributes.home')</option>
|
||||
</x-admin::form.control-group.control>
|
||||
</div>
|
||||
|
||||
<i
|
||||
v-if="contactNumbers.length > 1"
|
||||
class="icon-delete ml-1 cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="remove(contactNumber)"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="`${name}[${index}].value`"/>
|
||||
</template>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex max-w-max items-center gap-2 text-brandColor"
|
||||
@click="add"
|
||||
>
|
||||
<i class="icon-add text-md !text-brandColor"></i>
|
||||
|
||||
@lang("admin::app.common.custom-attributes.add-more")
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<x-slot:footer>
|
||||
<!-- Save Button -->
|
||||
<x-admin::button
|
||||
button-type="submit"
|
||||
class="primary-button justify-center"
|
||||
:title="trans('admin::app.common.custom-attributes.save')"
|
||||
::loading="isProcessing"
|
||||
::disabled="isProcessing"
|
||||
/>
|
||||
</x-slot>
|
||||
</x-admin::modal>
|
||||
</form>
|
||||
</x-admin::form>
|
||||
</Teleport>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-phone-edit', {
|
||||
template: '#v-inline-phone-edit-template',
|
||||
|
||||
emits: ['on-save'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
contactNumbers: JSON.parse(JSON.stringify(this.value || [{'value': '', 'label': 'work'}])),
|
||||
|
||||
isProcessing: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue, oldValue) {
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
|
||||
this.contactNumbers = newValue || [{'value': '', 'label': 'work'}];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.extendValidations();
|
||||
|
||||
if (! this.contactNumbers || ! this.contactNumbers.length) {
|
||||
this.contactNumbers = [{
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the validation rules.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getValidation() {
|
||||
return {
|
||||
phone: true,
|
||||
unique_contact_number: this.contactNumbers ?? [],
|
||||
required: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.$refs.phoneNumberModal.toggle();
|
||||
},
|
||||
|
||||
add() {
|
||||
this.contactNumbers.push({
|
||||
'value': '',
|
||||
'label': 'work'
|
||||
});
|
||||
},
|
||||
|
||||
remove(contactNumber) {
|
||||
this.contactNumbers = this.contactNumbers.filter(number => number !== contactNumber);
|
||||
},
|
||||
|
||||
extendValidations() {
|
||||
defineRule('unique_contact_number', (value, contactNumbers) => {
|
||||
if (
|
||||
! value
|
||||
|| ! value.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const phoneOccurrences = contactNumbers.filter(contactNumber => contactNumber.value === value).length;
|
||||
|
||||
if (phoneOccurrences > 1) {
|
||||
return 'This phone number is already used';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
updateOrCreate(params) {
|
||||
this.inputValue = params.contact_numbers || this.inputValue;
|
||||
|
||||
if (this.url) {
|
||||
this.isProcessing = true;
|
||||
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.contactNumbers = response.data.data.contact_numbers || this.contactNumbers;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false;
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-save', params);
|
||||
|
||||
this.$refs.phoneNumberModal.toggle();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,270 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
'options' => [],
|
||||
])
|
||||
|
||||
<v-inline-select-edit
|
||||
{{ $attributes->except('options') }}
|
||||
:options="{{ json_encode($options) }}"
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-select-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-select-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
@{{ valueLabel ? valueLabel : selectedValue?.name.length > 20 ? selectedValue?.name.substring(0, 20) + '...' : selectedValue?.name }}
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="selectedValue?.name.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ selectedValue?.name }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative w-full"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="select"
|
||||
::id="name"
|
||||
::name="name"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
class="!h-[34px] !py-0 ltr:pr-20 rtl:pl-20"
|
||||
::placeholder="placeholder"
|
||||
v-model="inputValue"
|
||||
>
|
||||
<option
|
||||
v-for="(option, index) in options"
|
||||
:key="option.id"
|
||||
:value="option.id"
|
||||
>
|
||||
@{{ option.name }}
|
||||
</option>
|
||||
</x-admin::form.control-group.control>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1/2 flex -translate-y-1/2 transform items-center gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<i class="icon-down-arrow text-2xl" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-select-edit', {
|
||||
template: '#v-inline-select-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the selected value.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
selectedValue() {
|
||||
return this.options.find((option, index) => option.id == this.inputValue);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
this.$axios.put(this.url, {
|
||||
[this.name]: this.inputValue,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,277 @@
|
||||
@props([
|
||||
'allowEdit' => true,
|
||||
])
|
||||
|
||||
<v-inline-text-edit
|
||||
{{ $attributes }}
|
||||
:allow-edit="{{ $allowEdit ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<div class="rounded-xs flex h-[34px] items-center pl-2.5 text-left">
|
||||
<div class="shimmer h-5 w-48 rounded border border-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</v-inline-text-edit>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-inline-text-edit-template"
|
||||
>
|
||||
<div class="group w-full max-w-full hover:rounded-sm">
|
||||
<!-- Non-editing view -->
|
||||
<div
|
||||
v-if="! isEditing"
|
||||
class="flex h-[34px] items-center rounded border border-transparent transition-all"
|
||||
:class="allowEdit ? 'hover:bg-gray-100 dark:hover:bg-gray-800' : ''"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::id="name"
|
||||
::name="name"
|
||||
v-model="inputValue"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="group relative h-[18px] !w-full pl-2.5"
|
||||
:style="{ 'text-align': position }"
|
||||
>
|
||||
<span class="cursor-pointer truncate rounded">
|
||||
<template v-if="isDirty">
|
||||
@{{
|
||||
inputValue.length > 20
|
||||
? inputValue.substring(0, 20) + '...'
|
||||
: inputValue
|
||||
}}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@{{
|
||||
(valueLabel || inputValue || '').length > 20
|
||||
? (valueLabel || inputValue).substring(0, 20) + '...'
|
||||
: (valueLabel || inputValue)
|
||||
}}
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="absolute bottom-0 mb-5 hidden flex-col group-hover:flex"
|
||||
v-if="inputValue?.length > 20"
|
||||
>
|
||||
<span class="whitespace-no-wrap relative z-10 rounded-md bg-black px-4 py-2 text-xs leading-none text-white shadow-lg dark:bg-white dark:text-gray-900">
|
||||
@{{ inputValue }}
|
||||
</span>
|
||||
|
||||
<div class="-mt-2 ml-4 h-3 w-3 rotate-45 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="allowEdit">
|
||||
<i
|
||||
@click="toggle"
|
||||
class="icon-edit cursor-pointer rounded p-0.5 text-2xl opacity-0 hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-950 ltr:mr-1 rtl:ml-1"
|
||||
></i>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Editing view -->
|
||||
<div
|
||||
class="relative w-full"
|
||||
v-else
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="text"
|
||||
::id="name"
|
||||
::name="name"
|
||||
class="!h-[34px] !py-0 ltr:pr-16 rtl:pl-16"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
::placeholder="placeholder"
|
||||
v-model="inputValue"
|
||||
ref="input"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-[6px] flex gap-0.5 ltr:right-2 rtl:left-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-green-100 p-1 hover:bg-green-200 ltr:rounded-l-md rtl:rounded-r-md"
|
||||
@click="save"
|
||||
>
|
||||
<i class="icon-tick text-md cursor-pointer font-bold text-green-600 dark:!text-green-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center bg-red-100 p-1 hover:bg-red-200 ltr:rounded-r-md rtl:rounded-l-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<i class="icon-cross-large text-md cursor-pointer font-bold text-red-600 dark:!text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-admin::form.control-group.error ::name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-inline-text-edit', {
|
||||
template: '#v-inline-text-edit-template',
|
||||
|
||||
emits: ['on-change', 'on-cancelled'],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
|
||||
isEditing: false,
|
||||
|
||||
isDirty: false,
|
||||
|
||||
isRTL: document.documentElement.dir === 'rtl',
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the value prop.
|
||||
*
|
||||
* @param {String} newValue
|
||||
*/
|
||||
value(newValue) {
|
||||
this.inputValue = newValue;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the input.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.$nextTick(() => this.$refs.input.focus());
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
save() {
|
||||
if (this.errors[this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.url) {
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(this.name, this.inputValue);
|
||||
|
||||
formData.append('_method', 'PUT');
|
||||
|
||||
this.isDirty = true;
|
||||
|
||||
this.$axios.post(this.url, {
|
||||
...this.params,
|
||||
...Object.fromEntries(formData),
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isDirty = false;
|
||||
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('on-change', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the input value.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
cancel() {
|
||||
this.inputValue = this.value;
|
||||
|
||||
this.isEditing = false;
|
||||
|
||||
this.$emit('on-cancelled', {
|
||||
name: this.name,
|
||||
value: this.inputValue,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,183 @@
|
||||
<v-control-tags
|
||||
:errors="errors"
|
||||
{{ $attributes }}
|
||||
v-bind="$attrs"
|
||||
></v-control-tags>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-control-tags-template"
|
||||
>
|
||||
<div
|
||||
class="flex min-h-[38px] w-full items-center rounded border border-gray-200 px-2.5 py-1.5 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 dark:border-gray-800 dark:text-white dark:hover:border-gray-400"
|
||||
:class="[errors[`temp-${name}`] ? 'border !border-red-600 hover:border-red-600' : '']"
|
||||
>
|
||||
<ul
|
||||
class="flex flex-wrap items-center gap-1"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<li
|
||||
v-for="(tag, index) in tags"
|
||||
:key="index"
|
||||
class="flex items-center gap-1 rounded-md bg-gray-100 dark:bg-gray-950 ltr:pl-2 rtl:pr-2"
|
||||
>
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="name + '[' + index + ']'"
|
||||
::value="tag"
|
||||
/>
|
||||
|
||||
@{{ tag }}
|
||||
|
||||
<span
|
||||
class="icon-cross-large cursor-pointer p-0.5 text-xl"
|
||||
@click="removeTag(tag)"
|
||||
></span>
|
||||
</li>
|
||||
|
||||
<li :class="['w-full', tags.length && 'mt-1.5']">
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
:name="'temp-' + name"
|
||||
v-model="input"
|
||||
:rules="tags.length ? inputRules : [inputRules, rules].filter(Boolean).join('|')"
|
||||
:label="label"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
:name="'temp-' + name"
|
||||
v-bind="field"
|
||||
class="w-full dark:!bg-gray-900"
|
||||
:placeholder="placeholder"
|
||||
:label="label"
|
||||
@keydown.enter.prevent="addTag"
|
||||
autocomplete="new-email"
|
||||
@blur="addTag"
|
||||
/>
|
||||
</v-field>
|
||||
|
||||
<template v-if="! tags.length && input != ''">
|
||||
<v-field
|
||||
v-slot="{ field, errors }"
|
||||
:name="name + '[' + 0 +']'"
|
||||
:value="input"
|
||||
:rules="inputRules"
|
||||
:label="label"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name + '[0]'"
|
||||
v-bind="field"
|
||||
/>
|
||||
</v-field>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<v-error-message
|
||||
:name="'temp-' + name"
|
||||
v-slot="{ message }"
|
||||
>
|
||||
<p
|
||||
class="mt-1 text-xs italic text-red-600"
|
||||
v-text="message"
|
||||
>
|
||||
</p>
|
||||
</v-error-message>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-control-tags', {
|
||||
template: '#v-control-tags-template',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
inputRules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
allowDuplicates: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tags: this.data ? this.data : [],
|
||||
|
||||
input: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addTag() {
|
||||
if (this.errors['temp-' + this.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = this.input.trim();
|
||||
|
||||
if (! tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! this.allowDuplicates
|
||||
&& this.tags.includes(tag)
|
||||
) {
|
||||
this.input = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.tags.push(tag);
|
||||
|
||||
this.$emit('tags-updated', this.tags);
|
||||
|
||||
this.input = '';
|
||||
},
|
||||
|
||||
removeTag: function(tag) {
|
||||
this.tags = this.tags.filter(function (tempTag) {
|
||||
return tempTag != tag;
|
||||
});
|
||||
|
||||
this.$emit('tags-updated', this.tags);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpushOnce
|
||||
@@ -0,0 +1,16 @@
|
||||
@props([
|
||||
'name' => null,
|
||||
'controlName' => '',
|
||||
])
|
||||
|
||||
<v-error-message
|
||||
{{ $attributes }}
|
||||
name="{{ $name ?? $controlName }}"
|
||||
v-slot="{ message }"
|
||||
>
|
||||
<p
|
||||
{{ $attributes->merge(['class' => 'mt-1 text-xs italic text-red-600']) }}
|
||||
v-text="message"
|
||||
>
|
||||
</p>
|
||||
</v-error-message>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div {{ $attributes->merge(['class' => 'mb-4']) }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<label {{ $attributes->merge(['class' => 'mb-1.5 flex items-center gap-1 text-sm font-normal text-gray-800 dark:text-white']) }}>
|
||||
{{ $slot }}
|
||||
</label>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
If a component has the `as` attribute, it indicates that it uses
|
||||
the ajaxified form or some customized slot form.
|
||||
-->
|
||||
@if ($attributes->has('as'))
|
||||
<v-form {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</v-form>
|
||||
|
||||
<!--
|
||||
Otherwise, a traditional form will be provided with a minimal
|
||||
set of configurations.
|
||||
-->
|
||||
@else
|
||||
@props([
|
||||
'method' => 'POST',
|
||||
])
|
||||
|
||||
@php
|
||||
$method = strtoupper($method);
|
||||
@endphp
|
||||
|
||||
<v-form
|
||||
method="{{ $method === 'GET' ? 'GET' : 'POST' }}"
|
||||
:initial-errors="{{ json_encode($errors->getMessages()) }}"
|
||||
v-slot="{ meta, errors, setValues }"
|
||||
@invalid-submit="onInvalidSubmit"
|
||||
{{ $attributes }}
|
||||
>
|
||||
@unless(in_array($method, ['HEAD', 'GET', 'OPTIONS']))
|
||||
@csrf
|
||||
@endunless
|
||||
|
||||
@if (! in_array($method, ['GET', 'POST']))
|
||||
@method($method)
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
</v-form>
|
||||
@endif
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html
|
||||
lang="{{ app()->getLocale() }}"
|
||||
dir="{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'rtl' : 'ltr' }}"
|
||||
>
|
||||
|
||||
<head>
|
||||
<title>{{ $title ?? '' }}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta
|
||||
http-equiv="X-UA-Compatible"
|
||||
content="IE=edge"
|
||||
>
|
||||
<meta
|
||||
http-equiv="content-language"
|
||||
content="{{ app()->getLocale() }}"
|
||||
>
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
>
|
||||
<meta
|
||||
name="base-url"
|
||||
content="{{ url()->to('/') }}"
|
||||
>
|
||||
<meta
|
||||
name="currency-code"
|
||||
{{-- content="{{ core()->getCurrentCurrencyCode() }}" --}}
|
||||
>
|
||||
|
||||
@stack('meta')
|
||||
|
||||
{{
|
||||
vite()->set(['src/Resources/assets/css/app.css', 'src/Resources/assets/js/app.js'])
|
||||
}}
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
@if ($favicon = core()->getConfigData('general.design.admin_logo.favicon'))
|
||||
<link
|
||||
type="image/x-icon"
|
||||
href="{{ Storage::url($favicon) }}"
|
||||
rel="shortcut icon"
|
||||
sizes="16x16"
|
||||
>
|
||||
@else
|
||||
<link
|
||||
type="image/x-icon"
|
||||
href="{{ vite()->asset('images/favicon.ico') }}"
|
||||
rel="shortcut icon"
|
||||
sizes="16x16"
|
||||
/>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$brandColor = core()->getConfigData('general.settings.menu_color.brand_color') ?? '#0E90D9';
|
||||
@endphp
|
||||
|
||||
@stack('styles')
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--brand-color: {{ $brandColor }};
|
||||
}
|
||||
|
||||
{!! core()->getConfigData('general.content.custom_scripts.custom_css') !!}
|
||||
</style>
|
||||
|
||||
{!! view_render_event('admin.layout.head') !!}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{!! view_render_event('admin.layout.body.before') !!}
|
||||
|
||||
<div id="app">
|
||||
<!-- Flash Message Blade Component -->
|
||||
<x-admin::flash-group />
|
||||
|
||||
{!! view_render_event('admin.layout.content.before') !!}
|
||||
|
||||
<!-- Page Content Blade Component -->
|
||||
{{ $slot }}
|
||||
|
||||
{!! view_render_event('admin.layout.content.after') !!}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.layout.body.after') !!}
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
{!! view_render_event('admin.layout.vue-app-mount.before') !!}
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Load event, the purpose of using the event is to mount the application
|
||||
* after all of our `Vue` components which is present in blade file have
|
||||
* been registered in the app. No matter what `app.mount()` should be
|
||||
* called in the last.
|
||||
*/
|
||||
window.addEventListener("load", function(event) {
|
||||
app.mount("#app");
|
||||
});
|
||||
</script>
|
||||
|
||||
{!! view_render_event('admin.layout.vue-app-mount.after') !!}
|
||||
|
||||
<script type="text/javascript">
|
||||
{!! core()->getConfigData('general.content.custom_scripts.custom_javascript') !!}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,567 @@
|
||||
<v-mega-search>
|
||||
<div class="relative flex w-[550px] max-w-[550px] items-center max-lg:w-[400px] ltr:ml-2.5 rtl:mr-2.5">
|
||||
<i class="icon-search absolute top-2 flex items-center text-2xl ltr:left-3 rtl:right-3"></i>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded-3xl border bg-white px-10 py-1.5 leading-6 text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
placeholder="@lang('admin::app.components.layouts.header.mega-search.title')"
|
||||
>
|
||||
</div>
|
||||
</v-mega-search>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-mega-search-template"
|
||||
>
|
||||
<div class="relative flex w-[550px] max-w-[550px] items-center max-lg:w-[400px] ltr:ml-2.5 rtl:mr-2.5">
|
||||
<i class="icon-search absolute top-2 flex items-center text-2xl ltr:left-3 rtl:right-3"></i>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="peer block w-full rounded-3xl border bg-white px-10 py-1.5 leading-6 text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
:class="{'border-gray-400': isDropdownOpen}"
|
||||
placeholder="@lang('admin::app.components.layouts.header.mega-search.title')"
|
||||
v-model.lazy="searchTerm"
|
||||
@click="searchTerm.length >= 2 ? isDropdownOpen = true : {}"
|
||||
v-debounce="500"
|
||||
>
|
||||
|
||||
<div
|
||||
class="absolute top-10 z-10 w-full rounded-lg border bg-white shadow-[0px_0px_0px_0px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10),0px_5px_5px_0px_rgba(0,0,0,0.09),0px_12px_7px_0px_rgba(0,0,0,0.05),0px_22px_9px_0px_rgba(0,0,0,0.01),0px_34px_9px_0px_rgba(0,0,0,0.00)] dark:border-gray-800 dark:bg-gray-900"
|
||||
v-if="isDropdownOpen"
|
||||
>
|
||||
<!-- Search Tabs -->
|
||||
<div class="flex overflow-x-auto border-b text-sm text-gray-600 dark:border-gray-800 dark:text-gray-300">
|
||||
<div
|
||||
class="cursor-pointer p-4 hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
:class="{ 'border-b-2 border-brandColor': activeTab == tab.key }"
|
||||
v-for="tab in tabs"
|
||||
@click="activeTab = tab.key; search();"
|
||||
>
|
||||
@{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Searched Results -->
|
||||
<template v-if="activeTab == 'products'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.products />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="product in searchedResults.products">
|
||||
<a
|
||||
:href="'{{ route('admin.products.view', ':id') }}'.replace(':id', product.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ product.name }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ "@lang(':sku')".replace(':sku', product.sku) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Information -->
|
||||
<div class="grid place-content-center gap-1 text-right">
|
||||
<!-- Formatted Price -->
|
||||
<p class="font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ $admin.formatPrice(product.price) }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.products.length">
|
||||
<a
|
||||
:href="'{{ route('admin.products.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-products')`.replace(':query', searchTerm).replace(':count', searchedResults.products.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.products.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-products')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'leads'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.leads />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="lead in searchedResults.leads">
|
||||
<a
|
||||
:href="'{{ route('admin.leads.view', ':id') }}'.replace(':id', lead.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ lead.title }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ lead.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Information -->
|
||||
<div class="grid place-content-center gap-1 text-right">
|
||||
<!-- Formatted Price -->
|
||||
<p class="font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ $admin.formatPrice(lead.lead_value) }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.leads.length">
|
||||
<a
|
||||
:href="'{{ route('admin.leads.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-leads')`.replace(':query', searchTerm).replace(':count', searchedResults.leads.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.leads.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-leads')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'persons'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.persons />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="person in searchedResults.persons">
|
||||
<a
|
||||
:href="'{{ route('admin.contacts.persons.view', ':id') }}'.replace(':id', person.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ person.name }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ person.emails.map((item) => `${item.value}(${item.label})`).join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.persons.length">
|
||||
<a
|
||||
:href="'{{ route('admin.contacts.persons.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-contacts')`.replace(':query', searchTerm).replace(':count', searchedResults.persons.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.contacts.persons.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-contacts')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'quotes'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.quotes />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="quote in searchedResults.quotes">
|
||||
<a
|
||||
:href="'{{ route('admin.quotes.edit', ':id') }}'.replace(':id', quote.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ quote.subject }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ quote.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.quotes.length">
|
||||
<a
|
||||
:href="'{{ route('admin.quotes.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-quotes')`.replace(':query', searchTerm).replace(':count', searchedResults.quotes.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.quotes.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-quotes')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'settings'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.settings />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="setting in searchedResults.settings">
|
||||
<a
|
||||
:href="setting.url"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ setting.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="! searchedResults.settings.length">
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<a
|
||||
href="{{ route('admin.settings.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-settings')
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'configurations'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.configurations />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="configuration in searchedResults.configurations">
|
||||
<a
|
||||
:href="configuration.url"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ configuration.title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="! searchedResults.configurations.length">
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<a
|
||||
href="{{ route('admin.configuration.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-configurations')
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-mega-search', {
|
||||
template: '#v-mega-search-template',
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'leads',
|
||||
|
||||
isDropdownOpen: false,
|
||||
|
||||
tabs: {
|
||||
leads: {
|
||||
key: 'leads',
|
||||
title: '@lang('admin::app.components.layouts.header.mega-search.tabs.leads')',
|
||||
is_active: true,
|
||||
endpoint: '{{ route('admin.leads.search') }}',
|
||||
query_params: [
|
||||
{
|
||||
search: 'title',
|
||||
searchFields: 'title:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'person.name',
|
||||
searchFields: 'person.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
quotes: {
|
||||
key: 'quotes',
|
||||
title: '@lang('admin::app.components.layouts.header.mega-search.tabs.quotes')',
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.quotes.search') }}',
|
||||
query_params: [
|
||||
{
|
||||
search: 'subject',
|
||||
searchFields: 'subject:like',
|
||||
},
|
||||
{
|
||||
search: 'description',
|
||||
searchFields: 'description:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'person.name',
|
||||
searchFields: 'person.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
products: {
|
||||
key: 'products',
|
||||
title: '@lang('admin::app.components.layouts.header.mega-search.tabs.products')',
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.products.search') }}',
|
||||
query_params: [
|
||||
{
|
||||
search: 'name',
|
||||
searchFields: 'name:like',
|
||||
},
|
||||
{
|
||||
search: 'sku',
|
||||
searchFields: 'sku:like',
|
||||
},
|
||||
{
|
||||
search: 'description',
|
||||
searchFields: 'description:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
persons: {
|
||||
key: 'persons',
|
||||
title: '@lang('admin::app.components.layouts.header.mega-search.tabs.persons')',
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.contacts.persons.search') }}',
|
||||
query_params: [
|
||||
{
|
||||
search: 'name',
|
||||
searchFields: 'name:like',
|
||||
},
|
||||
{
|
||||
search: 'job_title',
|
||||
searchFields: 'job_title:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'organization.name',
|
||||
searchFields: 'organization.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
settings: {
|
||||
key: 'settings',
|
||||
title: "@lang('admin::app.layouts.settings')",
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.settings.search') }}',
|
||||
query: '',
|
||||
},
|
||||
|
||||
configurations: {
|
||||
key: 'configurations',
|
||||
title: "@lang('admin::app.layouts.configuration')",
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.configuration.search') }}',
|
||||
query: '',
|
||||
},
|
||||
},
|
||||
|
||||
isLoading: false,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
searchedResults: {
|
||||
leads: [],
|
||||
quotes: [],
|
||||
products: [],
|
||||
persons: [],
|
||||
settings: [],
|
||||
configurations: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
search: '',
|
||||
searchFields: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm: 'updateSearchParams',
|
||||
|
||||
activeTab: 'updateSearchParams',
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
methods: {
|
||||
search(endpoint = null) {
|
||||
if (! endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.searchTerm.length <= 1) {
|
||||
this.searchedResults[this.activeTab] = [];
|
||||
|
||||
this.isDropdownOpen = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDropdownOpen = true;
|
||||
|
||||
this.$axios.get(endpoint, {
|
||||
params: {
|
||||
...this.params,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
this.searchedResults[this.activeTab] = response.data.data;
|
||||
})
|
||||
.catch((error) => {})
|
||||
.finally(() => this.isLoading = false);
|
||||
},
|
||||
|
||||
handleFocusOut(e) {
|
||||
if (! this.$el.contains(e.target)) {
|
||||
this.isDropdownOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateSearchParams() {
|
||||
const newTerm = this.searchTerm;
|
||||
|
||||
this.params = {
|
||||
search: '',
|
||||
searchFields: '',
|
||||
};
|
||||
|
||||
const tab = this.tabs[this.activeTab];
|
||||
|
||||
if (
|
||||
tab.key === 'settings'
|
||||
|| tab.key === 'configurations'
|
||||
) {
|
||||
this.params = null;
|
||||
|
||||
this.search(`${tab.endpoint}?query=${newTerm}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.params.search += tab.query_params.map((param) => `${param.search}:${newTerm};`).join('');
|
||||
|
||||
this.params.searchFields += tab.query_params.map((param) => `${param.searchFields};`).join('');
|
||||
|
||||
this.search(tab.endpoint);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,191 @@
|
||||
<header class="sticky top-0 z-[10001] flex items-center justify-between gap-1 border-b border-gray-200 bg-white px-4 py-2.5 transition-all dark:border-gray-800 dark:bg-gray-900">
|
||||
<!-- logo -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- Sidebar Menu -->
|
||||
<x-admin::layouts.sidebar.mobile />
|
||||
|
||||
<a href="{{ route('admin.dashboard.index') }}">
|
||||
@if ($logo = core()->getConfigData('general.general.admin_logo.logo_image'))
|
||||
<img
|
||||
class="h-7"
|
||||
src="{{ Storage::url($logo) }}"
|
||||
alt="{{ config('app.name') }}"
|
||||
/>
|
||||
@else
|
||||
<img
|
||||
class="h-7 max-sm:hidden"
|
||||
src="{{ request()->cookie('dark_mode') ? vite()->asset('images/dark-logo.svg') : vite()->asset('images/logo.svg') }}"
|
||||
id="logo-image"
|
||||
alt="{{ config('app.name') }}"
|
||||
/>
|
||||
|
||||
<img
|
||||
class="h-7 sm:hidden"
|
||||
src="{{ request()->cookie('dark_mode') ? vite()->asset('images/mobile-dark-logo.svg') : vite()->asset('images/mobile-light-logo.svg') }}"
|
||||
id="logo-image"
|
||||
alt="{{ config('app.name') }}"
|
||||
/>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 max-md:hidden">
|
||||
<!-- Mega Search Bar -->
|
||||
@include('admin::components.layouts.header.desktop.mega-search')
|
||||
|
||||
<!-- Quick Creation Bar -->
|
||||
@include('admin::components.layouts.header.quick-creation')
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="md:hidden">
|
||||
<!-- Mega Search Bar -->
|
||||
@include('admin::components.layouts.header.mobile.mega-search')
|
||||
</div>
|
||||
|
||||
<!-- Dark mode -->
|
||||
<v-dark>
|
||||
<div class="flex">
|
||||
<span
|
||||
class="{{ request()->cookie('dark_mode') ? 'icon-light' : 'icon-dark' }} p-1.5 rounded-md text-2xl cursor-pointer transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
></span>
|
||||
</div>
|
||||
</v-dark>
|
||||
|
||||
<div class="md:hidden">
|
||||
<!-- Quick Creation Bar -->
|
||||
@include('admin::components.layouts.header.quick-creation')
|
||||
</div>
|
||||
|
||||
<!-- Admin profile -->
|
||||
<x-admin::dropdown position="bottom-{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'left' : 'right' }}">
|
||||
<x-slot:toggle>
|
||||
@php($user = auth()->guard('user')->user())
|
||||
|
||||
@if ($user->image)
|
||||
<button class="flex h-9 w-9 cursor-pointer overflow-hidden rounded-full hover:opacity-80 focus:opacity-80">
|
||||
<img
|
||||
src="{{ $user->image_url }}"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
@else
|
||||
<button class="flex h-9 w-9 cursor-pointer items-center justify-center rounded-full bg-pink-400 font-semibold leading-6 text-white">
|
||||
{{ substr($user->name, 0, 1) }}
|
||||
</button>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<!-- Admin Dropdown -->
|
||||
<x-slot:content class="mt-2 border-t-0 !p-0">
|
||||
<div class="flex items-center gap-1.5 border border-x-0 border-b-gray-300 px-5 py-2.5 dark:border-gray-800">
|
||||
<img
|
||||
src="{{ asset('favicon.ico') }}"
|
||||
width="24"
|
||||
height="24"
|
||||
class="object-contain"
|
||||
/>
|
||||
|
||||
<!-- Version -->
|
||||
<p class="text-gray-400">
|
||||
@lang('admin::app.layouts.app-version', ['version' => core()->version()])
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 pb-2.5">
|
||||
<a
|
||||
class="cursor-pointer px-5 py-2 text-base text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-950"
|
||||
href="{{ route('admin.user.account.edit') }}"
|
||||
>
|
||||
@lang('admin::app.layouts.my-account')
|
||||
</a>
|
||||
|
||||
<!--Admin logout-->
|
||||
<x-admin::form
|
||||
method="DELETE"
|
||||
action="{{ route('admin.session.destroy') }}"
|
||||
id="adminLogout"
|
||||
>
|
||||
</x-admin::form>
|
||||
|
||||
<a
|
||||
class="cursor-pointer px-5 py-2 text-base text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-950"
|
||||
href="{{ route('admin.session.destroy') }}"
|
||||
onclick="event.preventDefault(); document.getElementById('adminLogout').submit();"
|
||||
>
|
||||
@lang('admin::app.layouts.sign-out')
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-dark-template"
|
||||
>
|
||||
<div class="flex">
|
||||
<span
|
||||
class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
:class="[isDarkMode ? 'icon-light' : 'icon-dark']"
|
||||
@click="toggle"
|
||||
></span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-dark', {
|
||||
template: '#v-dark-template',
|
||||
|
||||
data() {
|
||||
return {
|
||||
isDarkMode: {{ request()->cookie('dark_mode') ?? 0 }},
|
||||
|
||||
logo: "{{ vite()->asset('images/logo.svg') }}",
|
||||
|
||||
dark_logo: "{{ vite()->asset('images/dark-logo.svg') }}",
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isDarkMode = parseInt(this.isDarkModeCookie()) ? 0 : 1;
|
||||
|
||||
var expiryDate = new Date();
|
||||
|
||||
expiryDate.setMonth(expiryDate.getMonth() + 1);
|
||||
|
||||
document.cookie = 'dark_mode=' + this.isDarkMode + '; path=/; expires=' + expiryDate.toGMTString();
|
||||
|
||||
document.documentElement.classList.toggle('dark', this.isDarkMode === 1);
|
||||
|
||||
if (this.isDarkMode) {
|
||||
this.$emitter.emit('change-theme', 'dark');
|
||||
|
||||
document.getElementById('logo-image').src = this.dark_logo;
|
||||
} else {
|
||||
this.$emitter.emit('change-theme', 'light');
|
||||
|
||||
document.getElementById('logo-image').src = this.logo;
|
||||
}
|
||||
},
|
||||
|
||||
isDarkModeCookie() {
|
||||
const cookies = document.cookie.split(';');
|
||||
|
||||
for (const cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
|
||||
if (name === 'dark_mode') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,593 @@
|
||||
<v-mobile-mega-search>
|
||||
<i class="icon-search flex items-center text-2xl"></i>
|
||||
</v-mobile-mega-search>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-mobile-mega-search-template"
|
||||
>
|
||||
<div>
|
||||
<i
|
||||
class="icon-search flex items-center text-2xl"
|
||||
@click="toggleSearchInput"
|
||||
v-show="!isSearchVisible"
|
||||
></i>
|
||||
|
||||
<div
|
||||
v-show="isSearchVisible"
|
||||
class="absolute left-1/2 top-3 z-[10002] flex w-full max-w-full -translate-x-1/2 items-center px-2"
|
||||
>
|
||||
<i class="icon-search absolute top-2 flex items-center text-2xl ltr:left-4 rtl:right-4"></i>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="peer block w-full rounded-3xl border bg-white px-10 py-1.5 leading-6 text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
:class="{'border-gray-400': isDropdownOpen}"
|
||||
placeholder="@lang('admin::app.components.layouts.header.mega-search.title')"
|
||||
v-model.lazy="searchTerm"
|
||||
@click="searchTerm.length >= 2 ? isDropdownOpen = true : {}"
|
||||
v-debounce="500"
|
||||
ref="searchInput"
|
||||
>
|
||||
|
||||
<i class="icon-cross-large absolute top-2 flex items-center text-2xl ltr:right-4 rtl:left-4"></i>
|
||||
|
||||
<div
|
||||
class="absolute left-[6px] right-[6px] top-10 z-10 max-h-[80vh] overflow-y-auto rounded-lg border bg-white shadow-[0px_0px_0px_0px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10),0px_5px_5px_0px_rgba(0,0,0,0.09),0px_12px_7px_0px_rgba(0,0,0,0.05),0px_22px_9px_0px_rgba(0,0,0,0.01),0px_34px_9px_0px_rgba(0,0,0,0.00)] dark:border-gray-800 dark:bg-gray-900"
|
||||
v-if="isDropdownOpen"
|
||||
>
|
||||
<!-- Search Tabs -->
|
||||
<div class="flex overflow-x-auto border-b text-sm text-gray-600 dark:border-gray-800 dark:text-gray-300 sm:text-base">
|
||||
<div
|
||||
class="flex-shrink-0 cursor-pointer whitespace-nowrap px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-950 sm:px-4 sm:py-3"
|
||||
:class="{ 'border-b-2 border-brandColor': activeTab == tab.key }"
|
||||
v-for="tab in tabs"
|
||||
@click="activeTab = tab.key; search();"
|
||||
>
|
||||
@{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Searched Results -->
|
||||
<template v-if="activeTab == 'products'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.products />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] divide-y divide-slate-200 overflow-y-auto dark:divide-gray-800">
|
||||
<template v-for="product in searchedResults.products">
|
||||
<a
|
||||
:href="'{{ route('admin.products.view', ':id') }}'.replace(':id', product.id)"
|
||||
class="flex flex-col justify-between gap-2.5 p-4 hover:bg-gray-100 dark:hover:bg-gray-950 sm:flex-row"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex flex-col gap-2.5 sm:flex-row sm:items-center">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-sm font-semibold text-gray-600 dark:text-gray-300 sm:text-base">
|
||||
@{{ product.name }}
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
@{{ "@lang(':sku')".replace(':sku', product.sku) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Information -->
|
||||
<div class="mt-2 text-right sm:mt-0 sm:text-right">
|
||||
<p class="text-gray-600 dark:text-gray-300 sm:text-base">
|
||||
@{{ $admin.formatPrice(product.price) }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.products.length">
|
||||
<a
|
||||
:href="'{{ route('admin.products.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-products')`.replace(':query', searchTerm).replace(':count', searchedResults.products.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.products.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-products')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'leads'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.leads />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="lead in searchedResults.leads">
|
||||
<a
|
||||
:href="'{{ route('admin.leads.view', ':id') }}'.replace(':id', lead.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
@{{ lead.title }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ lead.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Information -->
|
||||
<div class="grid place-content-center gap-1 text-right">
|
||||
<!-- Formatted Price -->
|
||||
<p class="font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ $admin.formatPrice(lead.lead_value) }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.leads.length">
|
||||
<a
|
||||
:href="'{{ route('admin.leads.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-leads')`.replace(':query', searchTerm).replace(':count', searchedResults.leads.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.leads.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-leads')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'persons'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.persons />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="person in searchedResults.persons">
|
||||
<a
|
||||
:href="'{{ route('admin.contacts.persons.view', ':id') }}'.replace(':id', person.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
@{{ person.name }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ person.emails.map((item) => `${item.value}(${item.label})`).join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.persons.length">
|
||||
<a
|
||||
:href="'{{ route('admin.contacts.persons.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-contacts')`.replace(':query', searchTerm).replace(':count', searchedResults.persons.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.contacts.persons.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-contacts')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'quotes'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.quotes />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="quote in searchedResults.quotes">
|
||||
<a
|
||||
:href="'{{ route('admin.quotes.edit', ':id') }}'.replace(':id', quote.id)"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-base font-semibold text-gray-600 dark:text-gray-300">
|
||||
@{{ quote.subject }}
|
||||
</p>
|
||||
|
||||
<p class="text-gray-500">
|
||||
@{{ quote.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<template v-if="searchedResults.quotes.length">
|
||||
<a
|
||||
:href="'{{ route('admin.quotes.index') }}?search=:query'.replace(':query', searchTerm)"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@{{ `@lang('admin::app.components.layouts.header.mega-search.explore-all-matching-quotes')`.replace(':query', searchTerm).replace(':count', searchedResults.quotes.length) }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a
|
||||
href="{{ route('admin.quotes.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-quotes')
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'settings'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.settings />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="setting in searchedResults.settings">
|
||||
<a
|
||||
:href="setting.url"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
@{{ setting.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="! searchedResults.settings.length">
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<a
|
||||
href="{{ route('admin.settings.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-settings')
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="activeTab == 'configurations'">
|
||||
<template v-if="isLoading">
|
||||
<x-admin::shimmer.header.mega-search.configurations />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid max-h-[400px] overflow-y-auto">
|
||||
<template v-for="configuration in searchedResults.configurations">
|
||||
<a
|
||||
:href="configuration.url"
|
||||
class="flex cursor-pointer justify-between gap-2.5 border-b border-slate-300 p-4 last:border-b-0 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-950"
|
||||
>
|
||||
<!-- Left Information -->
|
||||
<div class="flex gap-2.5">
|
||||
<!-- Details -->
|
||||
<div class="grid place-content-start gap-1.5">
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
@{{ configuration.title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="! searchedResults.configurations.length">
|
||||
<div class="flex border-t p-3 dark:border-gray-800">
|
||||
<a
|
||||
href="{{ route('admin.configuration.index') }}"
|
||||
class="cursor-pointer text-xs font-semibold text-brandColor transition-all hover:underline"
|
||||
>
|
||||
@lang('admin::app.components.layouts.header.mega-search.explore-all-configurations')
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-mobile-mega-search', {
|
||||
template: '#v-mobile-mega-search-template',
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'leads',
|
||||
|
||||
isSearchVisible: false,
|
||||
|
||||
isDropdownOpen: false,
|
||||
|
||||
tabs: {
|
||||
leads: {
|
||||
key: 'leads',
|
||||
title: "@lang('admin::app.components.layouts.header.mega-search.tabs.leads')",
|
||||
is_active: true,
|
||||
endpoint: "{{ route('admin.leads.search') }}",
|
||||
query_params: [
|
||||
{
|
||||
search: 'title',
|
||||
searchFields: 'title:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'person.name',
|
||||
searchFields: 'person.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
quotes: {
|
||||
key: 'quotes',
|
||||
title: "@lang('admin::app.components.layouts.header.mega-search.tabs.quotes')",
|
||||
is_active: false,
|
||||
endpoint: "{{ route('admin.quotes.search') }}",
|
||||
query_params: [
|
||||
{
|
||||
search: 'subject',
|
||||
searchFields: 'subject:like',
|
||||
},
|
||||
{
|
||||
search: 'description',
|
||||
searchFields: 'description:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'person.name',
|
||||
searchFields: 'person.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
products: {
|
||||
key: 'products',
|
||||
title: "@lang('admin::app.components.layouts.header.mega-search.tabs.products')",
|
||||
is_active: false,
|
||||
endpoint: "{{ route('admin.products.search') }}",
|
||||
query_params: [
|
||||
{
|
||||
search: 'name',
|
||||
searchFields: 'name:like',
|
||||
},
|
||||
{
|
||||
search: 'sku',
|
||||
searchFields: 'sku:like',
|
||||
},
|
||||
{
|
||||
search: 'description',
|
||||
searchFields: 'description:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
persons: {
|
||||
key: 'persons',
|
||||
title: "@lang('admin::app.components.layouts.header.mega-search.tabs.persons')",
|
||||
is_active: false,
|
||||
endpoint: "{{ route('admin.contacts.persons.search') }}",
|
||||
query_params: [
|
||||
{
|
||||
search: 'name',
|
||||
searchFields: 'name:like',
|
||||
},
|
||||
{
|
||||
search: 'job_title',
|
||||
searchFields: 'job_title:like',
|
||||
},
|
||||
{
|
||||
search: 'user.name',
|
||||
searchFields: 'user.name:like',
|
||||
},
|
||||
{
|
||||
search: 'organization.name',
|
||||
searchFields: 'organization.name:like',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
settings: {
|
||||
key: 'settings',
|
||||
title: "@lang('admin::app.layouts.settings')",
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.settings.search') }}',
|
||||
query: '',
|
||||
},
|
||||
|
||||
configurations: {
|
||||
key: 'configurations',
|
||||
title: "@lang('admin::app.layouts.configuration')",
|
||||
is_active: false,
|
||||
endpoint: '{{ route('admin.configuration.search') }}',
|
||||
query: '',
|
||||
},
|
||||
},
|
||||
|
||||
isLoading: false,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
searchedResults: {
|
||||
leads: [],
|
||||
quotes: [],
|
||||
products: [],
|
||||
persons: [],
|
||||
settings: [],
|
||||
configurations: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
search: '',
|
||||
searchFields: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm: 'updateSearchParams',
|
||||
|
||||
activeTab: 'updateSearchParams',
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleSearchInput() {
|
||||
this.isSearchVisible = ! this.isSearchVisible;
|
||||
this.isDropdownOpen = false;
|
||||
|
||||
if (this.isSearchVisible) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.searchTerm = '';
|
||||
}
|
||||
},
|
||||
|
||||
search(endpoint) {
|
||||
if (! endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.searchTerm.length <= 1) {
|
||||
this.searchedResults[this.activeTab] = [];
|
||||
|
||||
this.isDropdownOpen = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDropdownOpen = true;
|
||||
|
||||
this.$axios.get(endpoint, {
|
||||
params: {
|
||||
...this.params,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
this.searchedResults[this.activeTab] = response.data.data;
|
||||
})
|
||||
.catch((error) => {})
|
||||
.finally(() => this.isLoading = false);
|
||||
},
|
||||
|
||||
handleFocusOut(e) {
|
||||
if (! this.$el.contains(e.target) || e.target.classList.contains('icon-cross-large')) {
|
||||
this.isDropdownOpen = false;
|
||||
|
||||
if (! this.isDropdownOpen) {
|
||||
this.isSearchVisible = false;
|
||||
this.searchTerm = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateSearchParams() {
|
||||
const newTerm = this.searchTerm;
|
||||
|
||||
this.params = {
|
||||
search: '',
|
||||
searchFields: '',
|
||||
};
|
||||
|
||||
const tab = this.tabs[this.activeTab];
|
||||
|
||||
if (
|
||||
tab.key === 'settings'
|
||||
|| tab.key === 'configurations'
|
||||
) {
|
||||
this.params = null;
|
||||
|
||||
this.search(`${tab.endpoint}?query=${newTerm}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.params.search += tab.query_params.map((param) => `${param.search}:${newTerm};`).join('');
|
||||
|
||||
this.params.searchFields += tab.query_params.map((param) => `${param.searchFields};`).join('');
|
||||
|
||||
this.search(tab.endpoint);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,145 @@
|
||||
<div>
|
||||
@if (bouncer()->hasPermission('leads.create')
|
||||
|| bouncer()->hasPermission('quotes.create')
|
||||
|| bouncer()->hasPermission('mail.create')
|
||||
|| bouncer()->hasPermission('contacts.persons.create')
|
||||
|| bouncer()->hasPermission('contacts.organizations.create')
|
||||
|| bouncer()->hasPermission('products.create')
|
||||
|| bouncer()->hasPermission('settings.automation.attributes.create')
|
||||
|| bouncer()->hasPermission('settings.user.roles.create')
|
||||
|| bouncer()->hasPermission('settings.user.users.create')
|
||||
)
|
||||
<x-admin::dropdown position="bottom-right">
|
||||
<x-slot:toggle>
|
||||
<!-- Toggle Button -->
|
||||
<button class="flex h-9 w-9 cursor-pointer items-center justify-center rounded-full bg-brandColor text-white">
|
||||
<i class="icon-add text-2xl"></i>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<!-- Dropdown Content -->
|
||||
<x-slot:content class="mt-2 !p-0">
|
||||
<div class="relative px-2 py-4">
|
||||
<div class="grid grid-cols-3 gap-2 text-center max-sm:grid-cols-2">
|
||||
<!-- Link to create new Lead -->
|
||||
@if (bouncer()->hasPermission('leads.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.leads.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-leads text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.lead')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Quotes -->
|
||||
@if (bouncer()->hasPermission('quotes.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.quotes.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-quote text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.quote')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to send new Mail-->
|
||||
@if (bouncer()->hasPermission('mail.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.mail.index', ['route' => 'inbox', 'openModal' => 'true']) }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-mail text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.email')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Person-->
|
||||
@if (bouncer()->hasPermission('contacts.persons.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.contacts.persons.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-settings-user text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.person')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Organizations -->
|
||||
@if (bouncer()->hasPermission('contacts.organizations.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.contacts.organizations.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-organization text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.organization')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Products -->
|
||||
@if (bouncer()->hasPermission('products.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.products.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-product text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.product')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Attributes -->
|
||||
@if (bouncer()->hasPermission('settings.automation.attributes.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.settings.attributes.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-attribute text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.attribute')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new Role -->
|
||||
@if (bouncer()->hasPermission('settings.user.roles.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.settings.roles.create') }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-role text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.role')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Link to create new User-->
|
||||
@if (bouncer()->hasPermission('settings.user.users.create'))
|
||||
<div class="rounded-lg bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-950">
|
||||
<a href="{{ route('admin.settings.users.index', ['action' => 'create']) }}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<i class="icon-user text-2xl text-gray-600"></i>
|
||||
|
||||
<span class="font-medium dark:text-gray-300">@lang('admin::app.layouts.user')</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-admin::dropdown>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html
|
||||
class="{{ request()->cookie('dark_mode') ? 'dark' : '' }}"
|
||||
lang="{{ app()->getLocale() }}"
|
||||
dir="{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'rtl' : 'ltr' }}"
|
||||
>
|
||||
|
||||
<head>
|
||||
|
||||
{!! view_render_event('admin.layout.head.before') !!}
|
||||
|
||||
<title>{{ $title ?? '' }}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta
|
||||
http-equiv="X-UA-Compatible"
|
||||
content="IE=edge"
|
||||
>
|
||||
<meta
|
||||
http-equiv="content-language"
|
||||
content="{{ app()->getLocale() }}"
|
||||
>
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
>
|
||||
<meta
|
||||
name="base-url"
|
||||
content="{{ url()->to('/') }}"
|
||||
>
|
||||
<meta
|
||||
name="currency"
|
||||
content="{{
|
||||
json_encode([
|
||||
'code' => config('app.currency'),
|
||||
'symbol' => core()->currencySymbol(config('app.currency'))])
|
||||
}}
|
||||
"
|
||||
>
|
||||
|
||||
@stack('meta')
|
||||
|
||||
{{
|
||||
vite()->set(['src/Resources/assets/css/app.css', 'src/Resources/assets/js/app.js'])
|
||||
}}
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="{{ url('cache/logo/bagisto.png') }}"
|
||||
>
|
||||
|
||||
@if ($favicon = core()->getConfigData('general.design.admin_logo.favicon'))
|
||||
<link
|
||||
type="image/x-icon"
|
||||
href="{{ Storage::url($favicon) }}"
|
||||
rel="shortcut icon"
|
||||
sizes="16x16"
|
||||
>
|
||||
@else
|
||||
<link
|
||||
type="image/x-icon"
|
||||
href="{{ vite()->asset('images/favicon.ico') }}"
|
||||
rel="shortcut icon"
|
||||
sizes="16x16"
|
||||
/>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$brandColor = core()->getConfigData('general.settings.menu_color.brand_color') ?? '#0E90D9';
|
||||
@endphp
|
||||
|
||||
@stack('styles')
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--brand-color: {{ $brandColor }};
|
||||
}
|
||||
|
||||
{!! core()->getConfigData('general.content.custom_scripts.custom_css') !!}
|
||||
</style>
|
||||
|
||||
{!! view_render_event('admin.layout.head.after') !!}
|
||||
</head>
|
||||
|
||||
<body class="h-full font-inter dark:bg-gray-950">
|
||||
{!! view_render_event('admin.layout.body.before') !!}
|
||||
|
||||
<div
|
||||
id="app"
|
||||
class="h-full"
|
||||
>
|
||||
<!-- Flash Message Blade Component -->
|
||||
<x-admin::flash-group />
|
||||
|
||||
<!-- Confirm Modal Blade Component -->
|
||||
<x-admin::modal.confirm />
|
||||
|
||||
{!! view_render_event('admin.layout.content.before') !!}
|
||||
|
||||
<!-- Page Header Blade Component -->
|
||||
<x-admin::layouts.header />
|
||||
|
||||
<div
|
||||
class="group/container sidebar-collapsed flex gap-4"
|
||||
ref="appLayout"
|
||||
>
|
||||
<!-- Page Sidebar Blade Component -->
|
||||
<x-admin::layouts.sidebar.desktop />
|
||||
|
||||
<div class="flex min-h-[calc(100vh-62px)] max-w-full flex-1 flex-col bg-gray-100 pt-3 transition-all duration-300 dark:bg-gray-950">
|
||||
<!-- Page Content Blade Component -->
|
||||
<div class="px-4 pb-6 ltr:lg:pl-[85px] rtl:lg:pr-[85px]">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<!-- Powered By -->
|
||||
<div class="mt-auto pt-6">
|
||||
<div class="border-t bg-white py-5 text-center text-sm font-normal dark:border-gray-800 dark:bg-gray-900 dark:text-white max-md:py-3">
|
||||
<p>Desenvolvido por <a href="https://blyzer.com.br" style="color: #0E90D9;" class="hover:underline" target="_blank">Blyzer</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.layout.content.after') !!}
|
||||
</div>
|
||||
|
||||
{!! view_render_event('admin.layout.body.after') !!}
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
{!! view_render_event('admin.layout.vue-app-mount.before') !!}
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Load event, the purpose of using the event is to mount the application
|
||||
* after all of our `Vue` components which is present in blade file have
|
||||
* been registered in the app. No matter what `app.mount()` should be
|
||||
* called in the last.
|
||||
*/
|
||||
window.addEventListener("load", function(event) {
|
||||
app.mount("#app");
|
||||
});
|
||||
</script>
|
||||
|
||||
{!! view_render_event('admin.layout.vue-app-mount.after') !!}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div
|
||||
ref="sidebar"
|
||||
class="duration-80 fixed top-[60px] z-[10002] h-full w-[200px] border-gray-200 bg-white pt-4 transition-all group-[.sidebar-collapsed]/container:w-[70px] dark:border-gray-800 dark:bg-gray-900 max-lg:hidden ltr:border-r rtl:border-l"
|
||||
@mouseover="handleMouseOver"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="journal-scroll h-[calc(100vh-100px)] overflow-hidden group-[.sidebar-collapsed]/container:overflow-visible">
|
||||
<nav class="sidebar-rounded grid w-full gap-2">
|
||||
<!-- Navigation Menu -->
|
||||
@foreach (menu()->getItems('admin') as $menuItem)
|
||||
<div class="px-4 group/item {{ $menuItem->isActive() ? 'active' : 'inactive' }}">
|
||||
<a
|
||||
class="flex gap-2 p-1.5 items-center cursor-pointer hover:rounded-lg {{ $menuItem->isActive() == 'active' ? 'bg-brandColor rounded-lg' : ' hover:bg-gray-100 hover:dark:bg-gray-950' }} peer"
|
||||
href="{{ ! in_array($menuItem->getKey(), ['settings', 'configuration']) && $menuItem->haveChildren() ? 'javascript:void(0)' : $menuItem->getUrl() }}"
|
||||
@mouseleave="!isMenuActive ? hoveringMenu = '' : {}"
|
||||
@mouseover="hoveringMenu='{{$menuItem->getKey()}}'"
|
||||
@click="isMenuActive = !isMenuActive"
|
||||
>
|
||||
<span class="{{ $menuItem->getIcon() }} text-2xl {{ $menuItem->isActive() ? 'text-white' : ''}}"></span>
|
||||
|
||||
<div class="flex-1 flex justify-between items-center text-gray-600 dark:text-gray-300 font-medium whitespace-nowrap group-[.sidebar-collapsed]/container:hidden {{ $menuItem->isActive() ? 'text-white' : ''}} group">
|
||||
<p>{{ $menuItem->getName() }}</p>
|
||||
|
||||
@if ( ! in_array($menuItem->getKey(), ['settings', 'configuration']) && $menuItem->haveChildren())
|
||||
<i class="icon-right-arrow rtl:icon-left-arrow invisible text-2xl group-hover/item:visible {{ $menuItem->isActive() ? 'text-white' : ''}}"></i>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Submenu -->
|
||||
@if (
|
||||
! in_array($menuItem->getKey(), ['settings', 'configuration'])
|
||||
&& $menuItem->haveChildren()
|
||||
)
|
||||
<div
|
||||
class="absolute top-0 hidden flex-col bg-gray-100 ltr:left-[200px] rtl:right-[199px]"
|
||||
:class="[isMenuActive && (hoveringMenu == '{{$menuItem->getKey()}}') ? '!flex' : 'hidden']"
|
||||
>
|
||||
<div class="sidebar-rounded fixed z-[1000] h-full min-w-[140px] max-w-max bg-white pt-4 after:-right-[30px] dark:border-gray-800 dark:bg-gray-900 max-lg:hidden ltr:border-r rtl:border-x">
|
||||
<div class="journal-scroll h-[calc(100vh-100px)] overflow-hidden">
|
||||
<nav class="grid w-full gap-2">
|
||||
@foreach ($menuItem->getChildren() as $subMenuItem)
|
||||
<div class="px-4 group/item {{ $menuItem->isActive() ? 'active' : 'inactive' }}">
|
||||
<a
|
||||
href="{{ $subMenuItem->getUrl() }}"
|
||||
class="flex gap-2.5 p-2 items-center cursor-pointer hover:rounded-lg {{ $subMenuItem->isActive() == 'active' ? 'bg-brandColor rounded-lg' : ' hover:bg-gray-100 hover:dark:bg-gray-950' }} peer"
|
||||
>
|
||||
<p class="text-gray-600 dark:text-gray-300 font-medium whitespace-nowrap {{ $subMenuItem->isActive() ? 'text-white' : ''}}">
|
||||
{{ $subMenuItem->getName() }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
<v-sidebar-drawer>
|
||||
<i class="icon-menu lg:hidden cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-100 dark:hover:bg-gray-950 max-lg:block"></i>
|
||||
</v-sidebar-drawer>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-sidebar-drawer-template"
|
||||
>
|
||||
<x-admin::drawer
|
||||
position="left"
|
||||
width="280px"
|
||||
class="lg:hidden [&>:nth-child(3)]:!m-0 [&>:nth-child(3)]:!rounded-l-none [&>:nth-child(3)]:max-sm:!w-[80%]"
|
||||
>
|
||||
<x-slot:toggle>
|
||||
<i class="icon-menu lg:hidden cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-100 dark:hover:bg-gray-950 max-lg:block"></i>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:header>
|
||||
@if ($logo = core()->getConfigData('general.design.admin_logo.logo_image'))
|
||||
<img
|
||||
class="h-10"
|
||||
src="{{ Storage::url($logo) }}"
|
||||
alt="{{ config('app.name') }}"
|
||||
/>
|
||||
@else
|
||||
<img
|
||||
class="h-10"
|
||||
src="{{ request()->cookie('dark_mode') ? vite()->asset('images/dark-logo.svg') : vite()->asset('images/logo.svg') }}"
|
||||
id="logo-image"
|
||||
alt="{{ config('app.name') }}"
|
||||
/>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot:content class="p-4">
|
||||
<div class="journal-scroll h-[calc(100vh-100px)] overflow-auto">
|
||||
<nav class="grid w-full gap-2">
|
||||
@foreach (menu()->getItems('admin') as $menuItem)
|
||||
@php
|
||||
$hasActiveChild = $menuItem->haveChildren() && collect($menuItem->getChildren())->contains(fn($child) => $child->isActive());
|
||||
|
||||
$isMenuActive = $menuItem->isActive() == 'active' || $hasActiveChild;
|
||||
|
||||
$menuKey = $menuItem->getKey();
|
||||
@endphp
|
||||
|
||||
<div
|
||||
class="menu-item relative"
|
||||
data-menu-key="{{ $menuKey }}"
|
||||
>
|
||||
<a
|
||||
href="{{ ! in_array($menuItem->getKey(), ['settings', 'configuration']) && $menuItem->haveChildren() ? 'javascript:void(0)' : $menuItem->getUrl() }}"
|
||||
class="menu-link flex items-center justify-between rounded-lg p-2 transition-colors duration-200"
|
||||
@if ($menuItem->haveChildren() && !in_array($menuKey, ['settings', 'configuration']))
|
||||
@click.prevent="toggleMenu('{{ $menuKey }}')"
|
||||
@endif
|
||||
:class="{ 'bg-brandColor text-white': activeMenu === '{{ $menuKey }}' || {{ $isMenuActive ? 'true' : 'false' }}, 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-950': !(activeMenu === '{{ $menuKey }}' || {{ $isMenuActive ? 'true' : 'false' }}) }"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="{{ $menuItem->getIcon() }} text-2xl"></span>
|
||||
|
||||
<p class="whitespace-nowrap font-semibold">{{ $menuItem->getName() }}</p>
|
||||
</div>
|
||||
|
||||
@if ($menuItem->haveChildren())
|
||||
<span
|
||||
class="transform text-lg transition-transform duration-300"
|
||||
:class="{ 'icon-arrow-up': activeMenu === '{{ $menuKey }}', 'icon-arrow-down': activeMenu !== '{{ $menuKey }}' }"
|
||||
></span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
@if ($menuItem->haveChildren() && !in_array($menuKey, ['settings', 'configuration']))
|
||||
<div
|
||||
class="submenu ml-1 mt-1 overflow-hidden rounded-b-lg border-l-2 transition-all duration-300 dark:border-gray-700"
|
||||
:class="{ 'max-h-[500px] py-2 border-l-brandColor bg-gray-50 dark:bg-gray-900': activeMenu === '{{ $menuKey }}' || {{ $hasActiveChild ? 'true' : 'false' }}, 'max-h-0 py-0 border-transparent bg-transparent': activeMenu !== '{{ $menuKey }}' && !{{ $hasActiveChild ? 'true' : 'false' }} }"
|
||||
>
|
||||
@foreach ($menuItem->getChildren() as $subMenuItem)
|
||||
<a
|
||||
href="{{ $subMenuItem->getUrl() }}"
|
||||
class="submenu-link block whitespace-nowrap p-2 pl-10 text-sm transition-colors duration-200"
|
||||
:class="{ 'text-brandColor font-medium bg-gray-100 dark:bg-gray-800': '{{ $subMenuItem->isActive() }}' === '1', 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800': '{{ $subMenuItem->isActive() }}' !== '1' }">
|
||||
{{ $subMenuItem->getName() }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-admin::drawer>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-sidebar-drawer', {
|
||||
template: '#v-sidebar-drawer-template',
|
||||
|
||||
data() {
|
||||
return { activeMenu: null };
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const activeElement = document.querySelector('.menu-item .menu-link.bg-brandColor');
|
||||
|
||||
if (activeElement) {
|
||||
this.activeMenu = activeElement.closest('.menu-item').getAttribute('data-menu-key');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleMenu(menuKey) {
|
||||
this.activeMenu = this.activeMenu === menuKey ? null : menuKey;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
20
packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php
Executable file
20
packages/Webkul/Admin/src/Resources/views/components/layouts/tabs.blade.php
Executable file
@@ -0,0 +1,20 @@
|
||||
@php
|
||||
$tabs = menu()->getCurrentActiveMenu('admin')?->getChildren();
|
||||
@endphp
|
||||
|
||||
@if (
|
||||
$tabs
|
||||
&& $tabs->isNotEmpty()
|
||||
)
|
||||
<div class="tabs">
|
||||
<div class="mb-4 flex gap-4 border-b-2 pt-2 dark:border-gray-800 max-sm:hidden">
|
||||
@foreach ($tabs as $tab)
|
||||
<a href="{{ $tab->getUrl() }}">
|
||||
<div class="{{ $tab->isActive() ? "-mb-px border-blue-600 border-b-2 transition" : '' }} pb-3.5 px-2.5 text-base font-medium text-gray-600 dark:text-gray-300 cursor-pointer">
|
||||
{{ $tab->getName() }}
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,328 @@
|
||||
<v-lookup {{ $attributes }}></v-lookup>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-lookup-template"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
ref="lookup"
|
||||
>
|
||||
<!-- Input Box (Button) -->
|
||||
<div
|
||||
class="relative inline-block w-full"
|
||||
@click="toggle"
|
||||
>
|
||||
<!-- Input Container -->
|
||||
<div class="relative flex cursor-pointer items-center justify-between rounded border border-gray-200 p-2 hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:text-gray-300">
|
||||
<!-- Selected Item or Placeholder Text -->
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis"
|
||||
:title="selectedItem?.name"
|
||||
>
|
||||
@{{ selectedItem?.name !== "" ? selectedItem?.name : "@lang('admin::app.components.lookup.click-to-add')" }}
|
||||
</span>
|
||||
|
||||
<!-- Icons Container -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Close Icon -->
|
||||
<i
|
||||
v-if="(selectedItem?.name) && ! isSearching"
|
||||
class="icon-cross-large cursor-pointer text-xl text-gray-600"
|
||||
@click="remove"
|
||||
></i>
|
||||
|
||||
<!-- Arrow Icon -->
|
||||
<i
|
||||
class="text-2xl text-gray-600"
|
||||
:class="showPopup ? 'icon-up-arrow' : 'icon-down-arrow'"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Input Box -->
|
||||
<x-admin::form.control-group.control
|
||||
type="hidden"
|
||||
::name="name"
|
||||
::rules="rules"
|
||||
::label="label"
|
||||
v-model="selectedItem.id"
|
||||
/>
|
||||
|
||||
<!-- Popup Box -->
|
||||
<div
|
||||
v-if="showPopup"
|
||||
class="absolute top-full z-10 mt-1 flex w-full origin-top transform flex-col gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg transition-transform dark:border-gray-900 dark:bg-gray-800"
|
||||
>
|
||||
<!-- Search Bar -->
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
v-model.lazy="searchTerm"
|
||||
v-debounce="500"
|
||||
class="w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
|
||||
placeholder="@lang('admin::app.components.lookup.search')"
|
||||
ref="searchInput"
|
||||
@keyup="search"
|
||||
/>
|
||||
|
||||
<!-- Search Icon (absolute positioned) -->
|
||||
<span class="absolute flex items-center ltr:right-2 rtl:left-2">
|
||||
<!-- Loader (optional, based on condition) -->
|
||||
<div
|
||||
class="relative"
|
||||
v-if="isSearching"
|
||||
>
|
||||
<x-admin::spinner />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<ul class="max-h-40 divide-y divide-gray-100 overflow-y-auto">
|
||||
<li
|
||||
v-for="item in filteredResults"
|
||||
:key="item.id"
|
||||
class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-900"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
@{{ item.name }}
|
||||
</li>
|
||||
|
||||
<template v-if="filteredResults.length === 0">
|
||||
<li class="px-4 py-2 text-gray-500">
|
||||
@lang('admin::app.components.lookup.no-results')
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="searchTerm.length > 2 && canAddNew"
|
||||
@click="selectItem({ id: '', name: searchTerm })"
|
||||
class="cursor-pointer border-t border-gray-800 px-4 py-2 text-gray-500 hover:bg-brandColor hover:text-white dark:border-gray-300 dark:text-gray-400 dark:hover:bg-gray-900 dark:hover:text-white"
|
||||
>
|
||||
<i class="icon-add text-md"></i>
|
||||
|
||||
@lang('admin::app.components.lookup.add-as-new')
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-lookup', {
|
||||
template: '#v-lookup-template',
|
||||
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
rules: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
canAddNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
preload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['on-selected'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
selectedItem: {},
|
||||
|
||||
searchedResults: [],
|
||||
|
||||
isSearching: false,
|
||||
|
||||
cancelToken: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.value) {
|
||||
this.selectedItem = this.value;
|
||||
}
|
||||
|
||||
this.search(this.preload);
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('click', this.handleFocusOut);
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm(newVal, oldVal) {
|
||||
this.search(this.preload);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Filter the searchedResults based on the search query.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
filteredResults() {
|
||||
return this.searchedResults.filter(item =>
|
||||
item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the popup.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
toggle() {
|
||||
this.showPopup = ! this.showPopup;
|
||||
|
||||
if (this.showPopup) {
|
||||
this.$nextTick(() => this.$refs.searchInput.focus());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select an item from the list.
|
||||
*
|
||||
* @param {Object} item
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
selectItem(item) {
|
||||
this.showPopup = false;
|
||||
|
||||
this.searchTerm = '';
|
||||
|
||||
this.selectedItem = item;
|
||||
|
||||
this.$emit('on-selected', item);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the items.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
search(preload = false) {
|
||||
if (
|
||||
! preload
|
||||
&& this.searchTerm.length <= 2
|
||||
) {
|
||||
this.searchedResults = [];
|
||||
|
||||
this.isSearching = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
|
||||
if (this.cancelToken) {
|
||||
this.cancelToken.cancel();
|
||||
}
|
||||
|
||||
this.cancelToken = this.$axios.CancelToken.source();
|
||||
|
||||
this.$axios.get(this.src, {
|
||||
params: {
|
||||
...this.params,
|
||||
query: this.searchTerm
|
||||
},
|
||||
cancelToken: this.cancelToken.token,
|
||||
})
|
||||
.then(response => {
|
||||
this.searchedResults = response.data.data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (! this.$axios.isCancel(error)) {
|
||||
console.error("Search request failed:", error);
|
||||
}
|
||||
|
||||
this.isSearching = false;
|
||||
})
|
||||
.finally(() => this.isSearching = false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the focus out event.
|
||||
*
|
||||
* @param {Event} event
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
handleFocusOut(event) {
|
||||
const lookup = this.$refs.lookup;
|
||||
|
||||
if (
|
||||
lookup &&
|
||||
! lookup.contains(event.target)
|
||||
) {
|
||||
this.showPopup = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the selected item.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
remove() {
|
||||
this.selectedItem = {
|
||||
id: '',
|
||||
name: '',
|
||||
};
|
||||
|
||||
this.$emit('on-selected', {});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,334 @@
|
||||
@props([
|
||||
'name' => 'images',
|
||||
'allowMultiple' => false,
|
||||
'showPlaceholders' => false,
|
||||
'uploadedImages' => [],
|
||||
'width' => '120px',
|
||||
'height' => '120px'
|
||||
])
|
||||
|
||||
<v-media-images
|
||||
name="{{ $name }}"
|
||||
v-bind:allow-multiple="{{ $allowMultiple ? 'true' : 'false' }}"
|
||||
v-bind:show-placeholders="{{ $showPlaceholders ? 'true' : 'false' }}"
|
||||
:uploaded-images='{{ json_encode($uploadedImages) }}'
|
||||
width="{{ $width }}"
|
||||
height="{{ $height }}"
|
||||
:errors="errors"
|
||||
>
|
||||
<x-admin::shimmer.image class="h-[120px] w-[120px] rounded" />
|
||||
</v-media-images>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-media-images-template"
|
||||
>
|
||||
<!-- Panel Content -->
|
||||
<div class="grid">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<!-- Upload Image Button -->
|
||||
<template v-if="allowMultiple || images.length == 0">
|
||||
<!-- Upload Image Button -->
|
||||
<label
|
||||
class="grid h-[120px] max-h-[120px] min-h-[110px] w-full min-w-[110px] max-w-[120px] cursor-pointer items-center justify-items-center rounded border border-dashed border-gray-300 transition-all hover:border-gray-400 dark:border-gray-800 dark:mix-blend-exclusion dark:invert"
|
||||
:class="[(errors?.['images.files[0]'] ?? false) ? 'border border-red-500' : 'border-gray-300']"
|
||||
:style="{'max-width': this.width, 'max-height': this.height}"
|
||||
:for="$.uid + '_imageInput'"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="icon-image text-2xl"></span>
|
||||
|
||||
<p class="grid text-center text-sm font-semibold text-gray-600 dark:text-gray-300">
|
||||
@lang('admin::app.components.media.images.add-image-btn')
|
||||
|
||||
<span class="text-xs">
|
||||
@lang('admin::app.components.media.images.allowed-types')
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
:id="$.uid + '_imageInput'"
|
||||
accept="image/*"
|
||||
:multiple="allowMultiple"
|
||||
:ref="$.uid + '_imageInput'"
|
||||
@change="add"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<!-- Uploaded Images -->
|
||||
<draggable
|
||||
class="flex flex-wrap gap-1"
|
||||
ghost-class="draggable-ghost"
|
||||
v-bind="{animation: 200}"
|
||||
:list="images"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<v-media-image-item
|
||||
:name="name"
|
||||
:index="index"
|
||||
:image="element"
|
||||
:width="width"
|
||||
:height="height"
|
||||
@onRemove="remove($event)"
|
||||
>
|
||||
</v-media-image-item>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- Placeholders -->
|
||||
<template v-if="showPlaceholders && ! images.length">
|
||||
<!-- Front Placeholder -->
|
||||
<div
|
||||
class="relative h-[120px] max-h-[120px] w-full min-w-[120px] max-w-[120px] rounded border border-dashed border-gray-300 dark:border-gray-800 dark:mix-blend-exclusion dark:invert"
|
||||
v-for="placeholder in placeholders"
|
||||
>
|
||||
<img :src="placeholder.image">
|
||||
|
||||
<p class="absolute bottom-4 w-full text-center text-xs font-semibold text-gray-400">
|
||||
@{{ placeholder.label }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="v-media-image-item-template">
|
||||
<div class="group relative grid max-h-[120px] min-w-[120px] justify-items-center overflow-hidden rounded transition-all hover:border-gray-400">
|
||||
<!-- Image Preview -->
|
||||
<img
|
||||
:src="image.url"
|
||||
:style="{'width': this.width, 'height': this.height}"
|
||||
/>
|
||||
|
||||
<div class="invisible absolute bottom-0 top-0 flex w-full flex-col justify-between bg-white p-3 opacity-80 transition-all group-hover:visible dark:bg-gray-900">
|
||||
<!-- Image Name -->
|
||||
<p class="break-all text-xs font-semibold text-gray-600 dark:text-gray-300"></p>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between">
|
||||
<span
|
||||
class="icon-delete cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
@click="remove"
|
||||
></span>
|
||||
|
||||
<label
|
||||
class="icon-edit cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
:for="$.uid + '_imageInput_' + index"
|
||||
></label>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name + '[' + image.id + ']'"
|
||||
v-if="! image.is_new"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
:name="name + '[]'"
|
||||
class="hidden"
|
||||
accept="image/*"
|
||||
:id="$.uid + '_imageInput_' + index"
|
||||
:ref="$.uid + '_imageInput_' + index"
|
||||
@change="edit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-media-images', {
|
||||
template: '#v-media-images-template',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: 'images',
|
||||
},
|
||||
|
||||
allowMultiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
showPlaceholders: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
uploadedImages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
default: '120px'
|
||||
},
|
||||
|
||||
height: {
|
||||
type: String,
|
||||
default: '120px'
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
images: [],
|
||||
|
||||
placeholders: [
|
||||
{
|
||||
label: "@lang('admin::app.components.media.images.placeholders.front')",
|
||||
image: "{{ asset('images/product-placeholders/front.svg') }}"
|
||||
}, {
|
||||
label: "@lang('admin::app.components.media.images.placeholders.next')",
|
||||
image: "{{ asset('images/product-placeholders/next-1.svg') }}"
|
||||
}, {
|
||||
label: "@lang('admin::app.components.media.images.placeholders.next')",
|
||||
image: "{{ asset('images/product-placeholders/next-2.svg') }}"
|
||||
}, {
|
||||
label: "@lang('admin::app.components.media.images.placeholders.zoom')",
|
||||
image: "{{ asset('images/product-placeholders/zoom.svg') }}"
|
||||
}, {
|
||||
label: "@lang('admin::app.components.media.images.placeholders.use-cases')",
|
||||
image: "{{ asset('images/product-placeholders/use-cases.svg') }}"
|
||||
}, {
|
||||
label: "@lang('admin::app.components.media.images.placeholders.size')",
|
||||
image: "{{ asset('images/product-placeholders/size.svg') }}"
|
||||
}
|
||||
],
|
||||
|
||||
isLoading: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.images = this.uploadedImages;
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
let imageInput = this.$refs[this.$.uid + '_imageInput'];
|
||||
|
||||
if (imageInput.files == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles = Array.from(imageInput.files).every(file => file.type.includes('image/'));
|
||||
|
||||
if (! validFiles) {
|
||||
this.$emitter.emit('add-flash', {
|
||||
type: 'warning',
|
||||
message: "@lang('admin::app.components.media.images.not-allowed-error')"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
imageInput.files.forEach((file, index) => {
|
||||
this.images.push({
|
||||
id: 'image_' + this.images.length,
|
||||
url: '',
|
||||
file: file
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
remove(image) {
|
||||
let index = this.images.indexOf(image);
|
||||
|
||||
this.images.splice(index, 1);
|
||||
},
|
||||
|
||||
getBase64ToFile(base64, filename) {
|
||||
var arr = base64.split(','),
|
||||
mime = arr[0].match(/:(.*?);/)[1],
|
||||
bstr = atob(arr[arr.length - 1]),
|
||||
n = bstr.length,
|
||||
u8arr = new Uint8Array(n);
|
||||
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
|
||||
return new File([u8arr], filename, {type:mime});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
app.component('v-media-image-item', {
|
||||
template: '#v-media-image-item-template',
|
||||
|
||||
props: ['index', 'image', 'name', 'width', 'height'],
|
||||
|
||||
mounted() {
|
||||
if (this.image.file instanceof File) {
|
||||
this.setFile(this.image.file);
|
||||
|
||||
this.readFile(this.image.file);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
edit() {
|
||||
let imageInput = this.$refs[this.$.uid + '_imageInput_' + this.index];
|
||||
|
||||
if (imageInput.files == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles = Array.from(imageInput.files).every(file => file.type.includes('image/'));
|
||||
|
||||
if (! validFiles) {
|
||||
this.$emitter.emit('add-flash', {
|
||||
type: 'warning',
|
||||
message: "@lang('admin::app.components.media.images.not-allowed-error')"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFile(imageInput.files[0]);
|
||||
|
||||
this.readFile(imageInput.files[0]);
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$emit('onRemove', this.image)
|
||||
},
|
||||
|
||||
setFile(file) {
|
||||
this.image.is_new = 1;
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
this.$refs[this.$.uid + '_imageInput_' + this.index].files = dataTransfer.files;
|
||||
},
|
||||
|
||||
readFile(file) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.image.url = e.target.result;
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,307 @@
|
||||
@props([
|
||||
'name' => 'images',
|
||||
'allowMultiple' => false,
|
||||
'uploadedVideos' => [],
|
||||
'width' => '210px',
|
||||
'height' => '120px'
|
||||
])
|
||||
|
||||
<v-media-videos
|
||||
name="{{ $name }}"
|
||||
v-bind:allow-multiple="{{ $allowMultiple ? 'true' : 'false' }}"
|
||||
:uploaded-videos='{{ json_encode($uploadedVideos) }}'
|
||||
width="{{ $width }}"
|
||||
height="{{ $height }}"
|
||||
:errors="errors"
|
||||
{{ $attributes->get('class') }}
|
||||
>
|
||||
</v-media-videos>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-media-videos-template"
|
||||
>
|
||||
<!-- Panel Content -->
|
||||
<div class="grid">
|
||||
<div class="flex gap-1">
|
||||
<!-- Upload Video Button -->
|
||||
|
||||
<label
|
||||
class="grid h-[120px] max-h-[120px] w-full max-w-[210px] cursor-pointer items-center justify-items-center rounded border border-dashed transition-all hover:border-gray-400 dark:border-gray-800 dark:mix-blend-exclusion dark:invert"
|
||||
:class="[errors['videos.files[0]'] ? 'border border-red-500' : 'border-gray-300']"
|
||||
:for="$.uid + '_videoInput'"
|
||||
v-if="allowMultiple || videos.length == 0"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="icon-image text-2xl"></span>
|
||||
|
||||
<p class="grid text-center text-sm font-semibold text-gray-600 dark:text-gray-300">
|
||||
@lang('admin::app.components.media.videos.add-video-btn')
|
||||
|
||||
<span class="text-xs">
|
||||
@lang('admin::app.components.media.videos.allowed-types')
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
:id="$.uid + '_videoInput'"
|
||||
accept="video/*"
|
||||
:multiple="allowMultiple"
|
||||
:ref="$.uid + '_videoInput'"
|
||||
@change="add"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Uploaded Videos -->
|
||||
<draggable
|
||||
class="flex gap-1"
|
||||
ghost-class="draggable-ghost"
|
||||
v-bind="{animation: 200}"
|
||||
:list="videos"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<v-media-video-item
|
||||
:name="name"
|
||||
:index="index"
|
||||
:video="element"
|
||||
:width="width"
|
||||
:height="height"
|
||||
@onRemove="remove($event)"
|
||||
>
|
||||
</v-media-video-item>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-media-video-item-template"
|
||||
>
|
||||
<div class="group relative grid h-[120px] max-h-[120px] min-w-[210px] max-w-[210px] justify-items-center overflow-hidden rounded border border-dashed border-gray-300 transition-all hover:border-gray-400 dark:border-gray-800">
|
||||
<!-- Video Preview -->
|
||||
<video
|
||||
class="h-[120px] w-[210px] object-cover"
|
||||
ref="videoPreview"
|
||||
v-if="video.url.length > 0"
|
||||
>
|
||||
<source :src="video.url" type="video/mp4">
|
||||
</video>
|
||||
|
||||
<div class="invisible absolute bottom-0 top-0 flex w-full flex-col justify-between bg-white p-3 opacity-80 transition-all group-hover:visible dark:bg-gray-900">
|
||||
<!-- Video Name -->
|
||||
<p class="break-all text-xs font-semibold text-gray-600 dark:text-gray-300"></p>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between">
|
||||
<!-- Remove Button -->
|
||||
<span
|
||||
class="icon-delete cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
@click="remove"
|
||||
></span>
|
||||
|
||||
<!-- Play Pause Button -->
|
||||
<span
|
||||
class="cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
:class="[isPlaying ? 'icon-pause': 'icon-play']"
|
||||
@click="playPause"
|
||||
></span>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<label
|
||||
class="icon-edit cursor-pointer rounded-md p-1.5 text-2xl hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||
:for="$.uid + '_videoInput_' + index"
|
||||
></label>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name + '[' + video.id + ']'"
|
||||
v-if="! video.is_new"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
:name="name + '[]'"
|
||||
class="hidden"
|
||||
accept="video/*"
|
||||
:id="$.uid + '_videoInput_' + index"
|
||||
:ref="$.uid + '_videoInput_' + index"
|
||||
@change="edit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-media-videos', {
|
||||
template: '#v-media-videos-template',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: 'videos',
|
||||
},
|
||||
|
||||
allowMultiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
uploadedVideos: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
default: '210px'
|
||||
},
|
||||
|
||||
height: {
|
||||
type: String,
|
||||
default: '120px'
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
videos: [],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.videos = this.uploadedVideos;
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
let videoInput = this.$refs[this.$.uid + '_videoInput'];
|
||||
|
||||
if (videoInput.files == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles = Array.from(videoInput.files).every(file => file.type.includes('video/'));
|
||||
|
||||
if (! validFiles) {
|
||||
this.$emitter.emit('add-flash', {
|
||||
type: 'warning',
|
||||
message: "@lang('admin::app.components.media.videos.not-allowed-error')"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
videoInput.files.forEach((file, index) => {
|
||||
this.videos.push({
|
||||
id: 'video_' + this.videos.length,
|
||||
url: '',
|
||||
file: file
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
remove(video) {
|
||||
let index = this.videos.indexOf(video);
|
||||
|
||||
this.videos.splice(index, 1);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
app.component('v-media-video-item', {
|
||||
template: '#v-media-video-item-template',
|
||||
|
||||
props: ['index', 'video', 'name', 'width', 'height'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isPlaying: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.video.file instanceof File) {
|
||||
this.setFile(this.video.file);
|
||||
|
||||
this.readFile(this.video.file);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
edit() {
|
||||
let videoInput = this.$refs[this.$.uid + '_videoInput_' + this.index];
|
||||
|
||||
if (videoInput.files == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles = Array.from(videoInput.files).every(file => file.type.includes('video/'));
|
||||
|
||||
if (! validFiles) {
|
||||
this.$emitter.emit('add-flash', {
|
||||
type: 'warning',
|
||||
message: "@lang('admin::app.components.media.videos.not-allowed-error')"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFile(videoInput.files[0]);
|
||||
|
||||
this.readFile(videoInput.files[0]);
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$emit('onRemove', this.video)
|
||||
},
|
||||
|
||||
setFile(file) {
|
||||
this.video.is_new = 1;
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
this.$refs[this.$.uid + '_videoInput_' + this.index].files = dataTransfer.files;
|
||||
},
|
||||
|
||||
readFile(file) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.video.url = e.target.result;
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
|
||||
playPause() {
|
||||
let videoPreview = this.$refs.videoPreview;
|
||||
|
||||
if (videoPreview.paused == true) {
|
||||
this.isPlaying = true;
|
||||
|
||||
videoPreview.play();
|
||||
} else {
|
||||
this.isPlaying = false;
|
||||
|
||||
videoPreview.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,140 @@
|
||||
<v-modal-confirm ref="confirmModal"></v-modal-confirm>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-modal-confirm-template"
|
||||
>
|
||||
<div>
|
||||
<transition
|
||||
tag="div"
|
||||
name="modal-overlay"
|
||||
enter-class="duration-300 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="duration-200 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-[10003] bg-gray-500 bg-opacity-50 transition-opacity"
|
||||
v-show="isOpen"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<transition
|
||||
tag="div"
|
||||
name="modal-content"
|
||||
enter-class="duration-300 ease-out"
|
||||
enter-from-class="translate-y-4 opacity-0 md:translate-y-0 md:scale-95"
|
||||
enter-to-class="translate-y-0 opacity-100 md:scale-100"
|
||||
leave-class="duration-200 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100 md:scale-100"
|
||||
leave-to-class="translate-y-4 opacity-0 md:translate-y-0 md:scale-95"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-[10004] transform overflow-y-auto transition"
|
||||
v-if="isOpen"
|
||||
>
|
||||
<div class="flex min-h-full items-end justify-center p-5 sm:items-center sm:p-0">
|
||||
<div class="box-shadow absolute left-1/2 top-1/2 z-[999] w-full max-w-[400px] -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white dark:bg-gray-900 max-md:w-[90%]">
|
||||
<div class="flex items-center justify-between gap-2.5 border-b px-4 py-3 text-lg font-bold text-gray-800 dark:border-gray-800 dark:text-white">
|
||||
@{{ title }}
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 text-left text-gray-600 dark:text-gray-300">
|
||||
@{{ message }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2.5 px-4 py-2.5">
|
||||
<button type="button" class="transparent-button" @click="disagree">
|
||||
@{{ options.btnDisagree }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="primary-button" @click="agree">
|
||||
@{{ options.btnAgree }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-modal-confirm', {
|
||||
template: '#v-modal-confirm-template',
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
|
||||
title: '',
|
||||
|
||||
message: '',
|
||||
|
||||
options: {
|
||||
btnDisagree: '',
|
||||
btnAgree: '',
|
||||
},
|
||||
|
||||
agreeCallback: null,
|
||||
|
||||
disagreeCallback: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerGlobalEvents();
|
||||
},
|
||||
|
||||
methods: {
|
||||
open({
|
||||
title = "@lang('admin::app.components.modal.confirm.title')",
|
||||
message = "@lang('admin::app.components.modal.confirm.message')",
|
||||
options = {
|
||||
btnDisagree: "@lang('admin::app.components.modal.confirm.disagree-btn')",
|
||||
btnAgree: "@lang('admin::app.components.modal.confirm.agree-btn')",
|
||||
},
|
||||
agree = () => {},
|
||||
disagree = () => {},
|
||||
}) {
|
||||
this.isOpen = true;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
this.title = title;
|
||||
|
||||
this.message = message;
|
||||
|
||||
this.options = options;
|
||||
|
||||
this.agreeCallback = agree;
|
||||
|
||||
this.disagreeCallback = disagree;
|
||||
},
|
||||
|
||||
disagree() {
|
||||
this.isOpen = false;
|
||||
|
||||
document.body.style.overflow = 'auto';
|
||||
|
||||
this.disagreeCallback();
|
||||
},
|
||||
|
||||
agree() {
|
||||
this.isOpen = false;
|
||||
|
||||
document.body.style.overflow = 'auto';
|
||||
|
||||
this.agreeCallback();
|
||||
},
|
||||
|
||||
registerGlobalEvents() {
|
||||
this.$emitter.on('open-confirm-modal', this.open);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,225 @@
|
||||
@props([
|
||||
'isActive' => false,
|
||||
'position' => 'center',
|
||||
'size' => 'normal',
|
||||
])
|
||||
|
||||
<v-modal
|
||||
is-active="{{ $isActive }}"
|
||||
position="{{ $position }}"
|
||||
size="{{ $size }}"
|
||||
{{ $attributes }}
|
||||
>
|
||||
@isset($toggle)
|
||||
<template v-slot:toggle>
|
||||
{{ $toggle }}
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($header)
|
||||
<template v-slot:header="{ toggle, isOpen }">
|
||||
<div {{ $header->attributes->merge(['class' => 'flex items-center justify-between gap-2.5 border-b px-4 py-3 dark:border-gray-800']) }}>
|
||||
{{ $header }}
|
||||
|
||||
<span
|
||||
class="icon-cross-large cursor-pointer text-3xl hover:rounded-md hover:bg-gray-100 dark:hover:bg-gray-950"
|
||||
@click="toggle"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($content)
|
||||
<template v-slot:content>
|
||||
<div {{ $content->attributes->merge(['class' => 'border-b px-4 py-2.5 dark:border-gray-800']) }}>
|
||||
{{ $content }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
|
||||
@isset($footer)
|
||||
<template v-slot:footer>
|
||||
<div {{ $content->attributes->merge(['class' => 'flex justify-end px-4 py-2.5']) }}>
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</template>
|
||||
@endisset
|
||||
</v-modal>
|
||||
|
||||
@pushOnce('scripts')
|
||||
<script
|
||||
type="text/x-template"
|
||||
id="v-modal-template"
|
||||
>
|
||||
<div>
|
||||
<div @click="toggle">
|
||||
<slot name="toggle">
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
tag="div"
|
||||
name="modal-overlay"
|
||||
enter-class="duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="duration-200 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-[10002] bg-gray-500 bg-opacity-50 transition-opacity"
|
||||
v-show="isOpen"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<transition
|
||||
tag="div"
|
||||
name="modal-content"
|
||||
enter-class="duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
:enter-from-class="enterFromLeaveToClasses"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-class="duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
:leave-to-class="enterFromLeaveToClasses"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-[10003] transform overflow-y-auto transition"
|
||||
v-if="isOpen"
|
||||
>
|
||||
<div class="flex min-h-full items-center justify-center max-md:p-4">
|
||||
<div
|
||||
class="box-shadow z-[999] w-full overflow-hidden rounded-lg bg-white dark:bg-gray-900 sm:absolute"
|
||||
:class="[finalPositionClass, sizeClass]"
|
||||
>
|
||||
<!-- Header Slot -->
|
||||
<slot
|
||||
name="header"
|
||||
:toggle="toggle"
|
||||
:isOpen="isOpen"
|
||||
>
|
||||
</slot>
|
||||
|
||||
<!-- Content Slot -->
|
||||
<slot name="content"></slot>
|
||||
|
||||
<!-- Footer Slot -->
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
app.component('v-modal', {
|
||||
template: '#v-modal-template',
|
||||
|
||||
props: [
|
||||
'isActive',
|
||||
'position',
|
||||
'size'
|
||||
],
|
||||
|
||||
emits: [
|
||||
'toggle',
|
||||
'open',
|
||||
'close',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: this.isActive,
|
||||
|
||||
isMobile: window.innerWidth < 640,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
|
||||
computed: {
|
||||
positionClass() {
|
||||
return {
|
||||
'center': 'items-center justify-center',
|
||||
'top-center': 'top-4',
|
||||
'bottom-center': 'bottom-4',
|
||||
'bottom-right': 'bottom-4 right-4',
|
||||
'bottom-left': 'bottom-4 left-4',
|
||||
'top-right': 'top-4 right-4',
|
||||
'top-left': 'top-4 left-4',
|
||||
}[this.position];
|
||||
},
|
||||
|
||||
finalPositionClass() {
|
||||
return this.isMobile
|
||||
? 'items-center justify-center'
|
||||
: this.positionClass;
|
||||
},
|
||||
|
||||
sizeClass() {
|
||||
return {
|
||||
'normal': 'max-w-[525px]',
|
||||
'medium': 'max-w-[768px]',
|
||||
'large': 'max-w-[950px]',
|
||||
}[this.size] || 'max-w-[525px]';
|
||||
},
|
||||
|
||||
enterFromLeaveToClasses() {
|
||||
const effectivePosition = this.isMobile ? 'center' : this.position;
|
||||
|
||||
return {
|
||||
'center': '-translate-y-4 opacity-0',
|
||||
'top-center': '-translate-y-4 opacity-0',
|
||||
'bottom-center': 'translate-y-4 opacity-0',
|
||||
'bottom-right': 'translate-y-4 opacity-0',
|
||||
'bottom-left': 'translate-y-4 opacity-0',
|
||||
'top-right': '-translate-y-4 opacity-0',
|
||||
'top-left': '-translate-y-4 opacity-0',
|
||||
}[effectivePosition];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkScreenSize() {
|
||||
this.isMobile = window.innerWidth < 640;
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.isOpen = ! this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow ='auto';
|
||||
}
|
||||
|
||||
this.$emit('toggle', { isActive: this.isOpen });
|
||||
},
|
||||
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
this.$emit('open', { isActive: this.isOpen });
|
||||
},
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
|
||||
document.body.style.overflow = 'auto';
|
||||
|
||||
this.$emit('close', { isActive: this.isOpen });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endPushOnce
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="!rounded-lg bg-white dark:bg-gray-900">
|
||||
<div class="flex items-center justify-between gap-x-5 p-4">
|
||||
<p class="shimmer w-[200px] p-2.5"></p>
|
||||
<p class="shimmer w-5 p-2.5"></p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
<div class="grid gap-y-2.5">
|
||||
<p class="shimmer w-[200px] p-2.5"></p>
|
||||
<p class="shimmer w-[200px] p-2.5"></p>
|
||||
<p class="shimmer w-40 p-2.5"></p>
|
||||
<p class="shimmer w-40 p-2.5"></p>
|
||||
<p class="shimmer w-40 p-2.5"></p>
|
||||
<p class="shimmer w-40 p-2.5"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="w-full rounded-md border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 overflow-x-auto border-b border-gray-200 dark:border-gray-800">
|
||||
@for ($i = 0; $i < 5; $i++)
|
||||
<div class="px-3 py-[11px]">
|
||||
<div class="shimmer h-5 w-24"></div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-4">
|
||||
<!-- Activity List -->
|
||||
<div class="flex flex-col gap-4">
|
||||
@for ($i = 0; $i < 5; $i++)
|
||||
<!-- Activity Item -->
|
||||
<div class="flex gap-2">
|
||||
<!-- Icon -->
|
||||
<div class="shimmer mt-2 flex h-9 w-9 rounded-full"></div>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="flex w-full justify-between gap-4 p-4">
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="shimmer h-[17px] w-48"></div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="shimmer h-[17px] w-full"></div>
|
||||
<div class="shimmer h-[17px] w-1/2"></div>
|
||||
</div>
|
||||
|
||||
<div class="shimmer h-5 w-48"></div>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="shimmer h-7 w-7 rounded-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
@props(['count' => 30])
|
||||
|
||||
<div class="flex gap-1.5">
|
||||
<div class="grid">
|
||||
@foreach (range(1, 10) as $i)
|
||||
<div class="shimmer h-[15px] w-[39px]"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="grid w-full gap-1.5">
|
||||
<div class="flex aspect-[3.23/1] w-full items-end border-b border-l pl-2.5 dark:border-gray-800">
|
||||
<div class="flex aspect-[3.23/1] w-full items-end justify-between gap-5 max-lg:gap-4 max-sm:gap-2.5">
|
||||
@foreach (range(1, $count) as $i)
|
||||
<div
|
||||
class="shimmer flex w-full"
|
||||
style="height: {{ rand(10, 100) }}%"
|
||||
></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between gap-5 pl-2.5 max-lg:gap-4 max-sm:gap-2.5">
|
||||
@foreach (range(1, $count) as $i)
|
||||
<div class="shimmer flex h-[15px] w-full"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="flex gap-4">
|
||||
<div class="w-full">
|
||||
<div class="shimmer w-50% h-56"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid w-full gap-3">
|
||||
<div class="shimmer w-50% h-10"></div>
|
||||
|
||||
<div class="shimmer w-50% h-10"></div>
|
||||
|
||||
<div class="shimmer w-50% h-10"></div>
|
||||
|
||||
<div class="shimmer w-50% h-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user