Compare commits
2 Commits
07d72d799d
...
a2a399e531
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a2a399e531 | ||
![]() |
b28105c659 |
|
@ -1,5 +1,7 @@
|
|||
import django_filters as filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_filters as filters
|
||||
|
||||
from . import models
|
||||
|
||||
|
@ -10,6 +12,8 @@ __all__ = (
|
|||
"ImageFilterSet",
|
||||
"SoundFilterSet",
|
||||
"TrackFilterSet",
|
||||
"UserFilterSet",
|
||||
"UserGroupFilterSet",
|
||||
)
|
||||
|
||||
|
||||
|
@ -67,3 +71,26 @@ class TrackFilterSet(filters.FilterSet):
|
|||
artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
|
||||
album = filters.CharFilter(field_name="album", lookup_expr="icontains")
|
||||
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
|
||||
|
||||
class UserFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
in_group = filters.NumberFilter(field_name="in_group", method="in_group_filter")
|
||||
not_in_group = filters.NumberFilter(field_name="not_in_group", method="not_in_group_filter")
|
||||
|
||||
def in_group_filter(self, queryset, name, value):
|
||||
return queryset.filter(groups__in=[value])
|
||||
|
||||
def not_in_group_filter(self, queryset, name, value):
|
||||
return queryset.exclude(groups__in=[value])
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(username__icontains=value) | Q(first_name__icontains=value) | Q(last_name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class UserGroupFilterSet(filters.FilterSet):
|
||||
class Meta:
|
||||
model = User.groups.through
|
||||
fields = ["group", "user"]
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from . import widgets
|
||||
|
||||
from .auth import UserGroupFormSet
|
||||
from .episode import EpisodeForm, EpisodeSoundFormSet
|
||||
from .program import ProgramForm
|
||||
from .page import CommentForm, ImageForm, PageForm, ChildPageForm
|
||||
|
@ -19,5 +18,4 @@ __all__ = (
|
|||
ChildPageForm,
|
||||
SoundForm,
|
||||
SoundCreateForm,
|
||||
UserGroupFormSet,
|
||||
)
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
from django import forms
|
||||
from django.forms.models import modelformset_factory
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from aircox.forms import widgets
|
||||
|
||||
|
||||
__all__ = ("UserGroupFormSet",)
|
||||
|
||||
|
||||
UserGroupFormSet = modelformset_factory(
|
||||
User.groups.through,
|
||||
fields=("group", "user"),
|
||||
widgets={
|
||||
"group": forms.HiddenInput(),
|
||||
"user": widgets.VueAutoComplete("api:usergroup-autocomplete", lookup="username"),
|
||||
},
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Vue App</title>
|
||||
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/admin.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -19,12 +19,21 @@ Usefull context:
|
|||
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
|
||||
|
||||
{% block assets %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/public.css" %}"/>
|
||||
<script src="{% static "aircox/js/chunk-common.js" %}"></script>
|
||||
<script src="{% static "aircox/js/chunk-vendors.js" %}"></script>
|
||||
<script src="{% static "aircox/js/public.js" %}"></script>
|
||||
{% static "vue/vue.esm-browser.js" as vue_url %}
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"vue": "{{vue_url}}"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="{{vue_url}}"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static "fontawesome-free/css/all.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/index.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/public.css" %}"/>
|
||||
|
||||
<script type="module" src="{% if app_js_url %}{{ app_js_url }}{% else %}{% static "aircox/public.js" %}{% endif %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
<title>
|
||||
|
@ -47,6 +56,7 @@ Usefull context:
|
|||
})
|
||||
</script>
|
||||
<div id="app">
|
||||
{% block app %}
|
||||
<div class="navs">
|
||||
{% block nav %}
|
||||
<nav class="nav primary" role="navigation" aria-label="main navigation">
|
||||
|
@ -159,5 +169,6 @@ Usefull context:
|
|||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
{% load static i18n %}
|
||||
|
||||
{% block assets %}
|
||||
{% static "aircox/admin.js" as app_js_url %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "aircox/js/dashboard.js" %}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/admin.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block head-title %}
|
||||
|
|
18
aircox/templates/aircox/dashboard/widgets/group_users.html
Normal file
18
aircox/templates/aircox/dashboard/widgets/group_users.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
|
||||
<a-modal ref="group-users-modal">
|
||||
<template #title="{item}">[[ item?.name ]]</template>
|
||||
<template #default="{item}">
|
||||
<a-group-users v-if="item" ref="group-users"
|
||||
:url="'{% url 'api:usergroup-list' %}?group=' + item.id"
|
||||
commit-url="{% url 'api:usergroup-commit' %}"
|
||||
:search-url="'{% url 'api:user-autocomplete' %}?search=${query}&group=' + item.id"
|
||||
:initials="{group_id: item.id }"
|
||||
/>
|
||||
</template>
|
||||
<template #footer="{item, close}">
|
||||
<button type="button" class="button" @click="$refs['group-users'].save(); close()">
|
||||
Save
|
||||
</button>
|
||||
</template>
|
||||
</a-modal>
|
|
@ -18,11 +18,10 @@ Context:
|
|||
{{ formset.non_form_errors }}
|
||||
<!-- formset.management_form -->
|
||||
|
||||
<{{ tag|default:"a-form-set" }}
|
||||
<{{ tag|default:"a-form-set" }} ref="formset"
|
||||
{% block tag-attrs %}
|
||||
:form-data="{{ formset_data|json }}"
|
||||
:labels="window.aircox.labels"
|
||||
:init-data="{% formset_inline_data formset=formset %}"
|
||||
:columns="[{% for n, f in fields.items %}{% if not f.widget.is_hidden %}'{{ n }}',{% endif %}{% endfor %} ]"
|
||||
settings-url="{% url "api:user-settings" %}"
|
||||
data-prefix="{{ formset.prefix }}-"
|
||||
|
@ -40,7 +39,7 @@ Context:
|
|||
</template>
|
||||
{% for name, field in fields.items %}
|
||||
{% if not field.widget.is_hidden and not field.is_readonly %}
|
||||
<template v-slot:control-{{ name }}="{item,cell,value,attr,emit,inputName}">
|
||||
<template v-slot:control-{{ name }}="{context,item,cell,value,attr,emit,inputName}">
|
||||
{% block row-control %}
|
||||
{% include "./v_form_field.html" with value="item.data."|add:name name="inputName" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "./public.html" %}
|
||||
{% load i18n aircox %}
|
||||
|
||||
|
||||
{% block head_title %}{{ station.name }}{% endblock %}
|
||||
|
||||
{% block title %}{% if page %}{{ block.super }}{% endif %}{% endblock %}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{% extends "./dashboard/base.html" %}
|
||||
{% load static aircox_admin i18n %}
|
||||
|
||||
{% block assets %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "aircox/js/dashboard.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block init-scripts %}
|
||||
aircox.labels = {% inline_labels %}
|
||||
{{ block.super }}
|
||||
|
@ -86,6 +81,8 @@ aircox.labels = {% inline_labels %}
|
|||
{% endblock %}
|
||||
|
||||
<hr/>
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-1">{% block page-form-actions %}{% endblock %}</div>
|
||||
<div class="has-text-right">
|
||||
<button type="submit" class="button">{% translate "Update" %}</button>
|
||||
</div>
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page-form %}
|
||||
//////
|
||||
{{ block.super }}
|
||||
{% block page-form-actions %}
|
||||
{% if request.user.is_superuser %}
|
||||
<button type="button"
|
||||
class="button secondary"
|
||||
@click="$refs['group-users-modal'].open({id: {{ object.editors_group_id }}, name: '{{ object.editors_group.name }}' })">Editors</button>
|
||||
|
||||
{% if editors_formset %}
|
||||
<hr/>
|
||||
<h2 class="title is-2">{% translate "Editors" %}</h2>
|
||||
{% include "./widgets/usergroup_formset.html" with formset=editors_formset formset_data=editors_formset_data tag_id="usergroup_formset" %}
|
||||
{% include "./dashboard/widgets/group_users.html" %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends base_template|default:"./base.html" %}
|
||||
{% extends "./base.html" %}
|
||||
|
||||
|
||||
{% comment %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<a-autocomplete
|
||||
url="{{url}}"
|
||||
name="{{ widget.name }}"{% if widget.value != None %} model-value="{{ widget.value|stringformat:'s' }}"{% endif %}
|
||||
{% include "django/forms/widgets/attrs.html" %} />
|
||||
{% if ":name" not in widget.attrs %}name="{{ name|default:widget.name }}"{% endif %}{% if widget.value != None %} model-value="{{ widget.value|stringformat:'s' }}"{% endif %}
|
||||
{% include "django/forms/widgets/attrs.html" %} {{ extra|default:"" }}/>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block row-control %}
|
||||
{% if name == 'user' %}
|
||||
{% form_field field name value %}
|
||||
{% form_field field "inputName" value %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
|
|
|
@ -45,7 +45,7 @@ def do_formset_inline_data(context, formset):
|
|||
# hack for sound list
|
||||
if duration := item.get("duration"):
|
||||
item["duration"] = duration.strftime("%H:%M")
|
||||
if sound := getattr(form.instance, "sound"):
|
||||
if sound := getattr(form.instance, "sound", None):
|
||||
item["name"] = sound.name
|
||||
fields["name"] = str(_("Sound")).capitalize()
|
||||
|
||||
|
@ -55,7 +55,7 @@ def do_formset_inline_data(context, formset):
|
|||
item["tags"] = ", ".join(tag.name for tag in tags)
|
||||
items.append(item)
|
||||
|
||||
data = {"items": items, "fields": fields}
|
||||
data = {"items": items, "fields": fields, "initial": formset.initial and formset.initial[0]}
|
||||
user = context["request"].user
|
||||
settings = getattr(user, "aircox_settings", None)
|
||||
data["settings"] = settings and UserSettingsSerializer(settings).data
|
||||
|
|
|
@ -21,11 +21,13 @@ register_converter(WeekConverter, "week")
|
|||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("user", viewsets.UserViewSet, basename="user")
|
||||
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
|
||||
|
||||
router.register("images", viewsets.ImageViewSet, basename="image")
|
||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
|
||||
router.register("track", viewsets.TrackROViewSet, basename="track")
|
||||
router.register("comment", viewsets.CommentViewSet, basename="comment")
|
||||
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
|
||||
|
||||
|
||||
api = [
|
||||
|
|
|
@ -115,6 +115,9 @@ class VueFormDataMixin:
|
|||
|
||||
# Note: values corresponds to AFormSet expected one
|
||||
|
||||
def get_form_items(self, formset):
|
||||
return [form.initial for form in formset.forms]
|
||||
|
||||
def get_form_field_data(self, form, values=None):
|
||||
"""Return form fields as data."""
|
||||
model = form.Meta.model
|
||||
|
@ -140,5 +143,7 @@ class VueFormDataMixin:
|
|||
"max_num_forms": formset.max_num,
|
||||
},
|
||||
"fields": self.get_form_field_data(formset.form, field_values),
|
||||
"initial_extra": formset.initial_extra and formset.initial_extra[0],
|
||||
"initials": self.get_form_items(formset),
|
||||
**kwargs,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import random
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.urls import reverse
|
||||
|
||||
|
@ -61,31 +60,3 @@ class ProgramUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
|||
def test_func(self):
|
||||
obj = self.get_object()
|
||||
return permissions.program.can(self.request.user, "update", obj)
|
||||
|
||||
def get_editors_queryset(self, program):
|
||||
# TODO: provide username in formset initials
|
||||
return User.groups.through.objects.filter(group_id=program.editors_group_id).order_by("user__username")
|
||||
|
||||
def get_editors_formset(self, program, **kwargs):
|
||||
return forms.UserGroupFormSet(
|
||||
**{
|
||||
**kwargs,
|
||||
"prefix": "editors",
|
||||
"queryset": self.get_editors_queryset(program),
|
||||
"initial": {
|
||||
"group": program.editors_group_id,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def get_context_data(self, editors_formset=None, **kwargs):
|
||||
# TODO: use group and permission system
|
||||
if self.request.user.is_superuser:
|
||||
if editors_formset is None:
|
||||
editors_formset = self.get_editors_formset(self.object)
|
||||
kwargs["editors_formset_data"] = self.get_formset_data(
|
||||
editors_formset, {"group": self.object.editors_group_id}
|
||||
)
|
||||
|
||||
context = super().get_context_data(editors_formset=editors_formset, **kwargs)
|
||||
return context
|
||||
|
|
|
@ -40,6 +40,49 @@ class AutocompleteMixin:
|
|||
return self.list(request)
|
||||
|
||||
|
||||
class ListCommitMixin:
|
||||
@action(name="commit", detail=False, methods=["POST"])
|
||||
def commit(self, request):
|
||||
"""
|
||||
Data:
|
||||
{
|
||||
"delete": [pk],
|
||||
"update": [{pk, **object}],
|
||||
"create": [object_data]
|
||||
}
|
||||
|
||||
Return:
|
||||
{
|
||||
"deleted": [pk],
|
||||
"updated": [object],
|
||||
"created": [object],
|
||||
}
|
||||
"""
|
||||
queryset = self.get_queryset()
|
||||
resp = {"deleted": [], "updated": [], "created": []}
|
||||
if ids := request.data.get("delete"):
|
||||
q = queryset.filter(id__in=ids)
|
||||
resp["deleted"] = list(q.values_list("id", flat=True))
|
||||
q.delete()
|
||||
|
||||
# TODO: bulk save and update
|
||||
if items := request.data.get("update"):
|
||||
resp["updated"] = self._commit_save_many(items)
|
||||
|
||||
if items := request.data.get("create"):
|
||||
resp["created"] = self._commit_save_many(items)
|
||||
|
||||
return Response(data=resp)
|
||||
|
||||
def _commit_save_many(self, data):
|
||||
ser = self.get_serializer(data=data, many=True)
|
||||
ser.is_valid(raise_exception=True)
|
||||
|
||||
items = ser.save()
|
||||
ser = self.get_serializer(items, many=True)
|
||||
return ser.data
|
||||
|
||||
|
||||
class ImageViewSet(viewsets.ModelViewSet):
|
||||
parsers = (parsers.MultiPartParser,)
|
||||
permissions = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
@ -84,7 +127,6 @@ class TrackROViewSet(AutocompleteMixin, viewsets.ReadOnlyModelViewSet):
|
|||
|
||||
serializer_class = serializers.admin.TrackSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
filter_backends = (drf_filters.DjangoFilterBackend,)
|
||||
filterset_class = filters.TrackFilterSet
|
||||
queryset = models.Track.objects.all()
|
||||
|
||||
|
@ -96,10 +138,19 @@ class CommentViewSet(viewsets.ModelViewSet):
|
|||
|
||||
|
||||
# --- admin
|
||||
class UserGroupViewSet(AutocompleteMixin, viewsets.ModelViewSet):
|
||||
class UserViewSet(AutocompleteMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.auth.UserSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
filterset_class = filters.UserFilterSet
|
||||
queryset = User.objects.all().distinct().order_by("username")
|
||||
|
||||
|
||||
class UserGroupViewSet(ListCommitMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.auth.UserGroupSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
queryset = User.groups.through.objects.all().distinct().order_by("user__username")
|
||||
filterset_class = filters.UserGroupFilterSet
|
||||
model = User.groups.through
|
||||
queryset = model.objects.all().distinct().order_by("user__username")
|
||||
|
||||
|
||||
class UserSettingsViewSet(viewsets.ViewSet):
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
# aircox-assets
|
||||
# aircox
|
||||
|
||||
## Project setup
|
||||
```
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
|
@ -1,19 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
{
|
||||
"name": "aircox-assets",
|
||||
"version": "0.1.0",
|
||||
"name": "aircox",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"core-js": "^3.8.3",
|
||||
"lodash": "^4.17.21",
|
||||
"v-calendar": "^3.1.2",
|
||||
"vue": "^3.2.13"
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"bulma": "^0.9.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^12.6.0",
|
||||
"vue-cli": "^2.9.6",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"vite": "^5.2.8"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.1 KiB |
|
@ -1 +0,0 @@
|
|||
../node_modules/vue/dist/vue.esm-browser.js
|
|
@ -1 +0,0 @@
|
|||
../node_modules/vue/dist/vue.esm-browser.prod.js
|
|
@ -2,7 +2,7 @@ import './styles/admin.scss'
|
|||
import './index.js'
|
||||
|
||||
import App from './app';
|
||||
import {admin as components} from './components'
|
||||
import components from './components/admin.js'
|
||||
|
||||
const AdminApp = {
|
||||
...App,
|
||||
|
@ -11,7 +11,21 @@ const AdminApp = {
|
|||
data() {
|
||||
return {
|
||||
...super.data,
|
||||
modalItem: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...App.methods,
|
||||
|
||||
fileSelected(select, input, preview) {
|
||||
const item = this.$refs[select].item
|
||||
if(item) {
|
||||
this.$refs[input].value = item.id
|
||||
if(preview)
|
||||
preview.src = item.file
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
export default AdminApp;
|
||||
|
|
|
@ -17,10 +17,22 @@ const App = {
|
|||
},
|
||||
|
||||
methods: {
|
||||
//! Delete elements from DOM using provided selector.
|
||||
deleteElements(sel) {
|
||||
for(var el of document.querySelectorAll(sel))
|
||||
el.parentNode.removeChild(el)
|
||||
},
|
||||
|
||||
//! File has been selected
|
||||
//! TODO: replace using regular ref and bindings.
|
||||
fileSelected(select, input, preview) {
|
||||
const item = this.$refs[select].item
|
||||
if(item) {
|
||||
this.$refs[input].value = item.id
|
||||
if(preview)
|
||||
preview.src = item.file
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,24 +20,23 @@
|
|||
<span class="is-inline-block" v-if="selected">
|
||||
<slot name="button" :index="selectedIndex" :item="selected"
|
||||
:value-field="valueField" :labelField="labelField">
|
||||
{{ labelField && selected.data[labelField] || selected }}
|
||||
{{ selectedLabel }}
|
||||
</slot>
|
||||
</span>
|
||||
</a>
|
||||
<div :class="dropdownClass">
|
||||
<div class="dropdown-menu is-fullwidth">
|
||||
<div class="dropdown-content" style="overflow: hidden">
|
||||
<a v-for="(item, index) in items" :key="item.id"
|
||||
href="#" :data-autocomplete-index="index"
|
||||
<span v-for="(item, index) in items" :key="item.id"
|
||||
:data-autocomplete-index="index"
|
||||
@click="select(index, false, false)"
|
||||
:class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
|
||||
:title="labelField && item.data[labelField] || item"
|
||||
tabindex="-1">
|
||||
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
||||
:labelField="labelField">
|
||||
{{ labelField && item.data[labelField] || item }}
|
||||
{{ getValue(item, labelField) || item }}
|
||||
</slot>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,12 +55,14 @@ export default {
|
|||
props: {
|
||||
//! Search URL (where `${query}` is replaced by search term)
|
||||
url: String,
|
||||
//! Extra GET url parameters
|
||||
urlParams: Object,
|
||||
//! Items' model
|
||||
model: Function,
|
||||
//! Input tag class
|
||||
inputClass: Array,
|
||||
//! input text placeholder
|
||||
placeholder: String,
|
||||
placeholder: Object,
|
||||
//! input form field name
|
||||
name: String,
|
||||
//! Field on items to use as label
|
||||
|
@ -105,6 +106,20 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
fullUrl() {
|
||||
if(!this.urlParams)
|
||||
return this.url
|
||||
|
||||
const url = new URL(this.url, window.location.origin)
|
||||
const params = new URLSearchParams(url.searchParams)
|
||||
|
||||
for(var key in this.urlParams)
|
||||
params.set(key, this.urlParams[key])
|
||||
const join = this.url.indexOf("?") >= 0 ? "&" : "?"
|
||||
url.search = params.toString()
|
||||
return url.href
|
||||
},
|
||||
|
||||
isFetching() { return !!this.promise },
|
||||
|
||||
selected() {
|
||||
|
@ -136,12 +151,34 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
reset() {
|
||||
this.inputValue = ""
|
||||
this.selectedIndex = -1
|
||||
this.items = []
|
||||
},
|
||||
|
||||
// TODO: move to utils/data
|
||||
getValue(data, path=null) {
|
||||
if(!data)
|
||||
return null
|
||||
if(!path)
|
||||
return data
|
||||
|
||||
const paths = path.split('.')
|
||||
for(const key of paths) {
|
||||
if(key in data)
|
||||
data = data[key]
|
||||
else return null;
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
itemValue(item) {
|
||||
return this.valueField ? item && item[this.valueField] : item;
|
||||
return this.valueField ? this.getValue(item, this.valueField) : item;
|
||||
},
|
||||
|
||||
itemLabel(item) {
|
||||
return this.labelField ? item && item[this.labelField] : item;
|
||||
return this.labelField ? this.getValue(item, this.labelField) : item;
|
||||
},
|
||||
|
||||
hide() {
|
||||
|
@ -183,7 +220,7 @@ export default {
|
|||
if(!this.items.length)
|
||||
return
|
||||
|
||||
var index = event.relatedTarget && Math.parseInt(event.relatedTarget.dataset.autocompleteIndex);
|
||||
var index = event.relatedTarget && Math.floor(event.relatedTarget.dataset.autocompleteIndex);
|
||||
if(index !== undefined && index !== null)
|
||||
this.select(index, false, false)
|
||||
this.cursor = -1;
|
||||
|
@ -227,7 +264,7 @@ export default {
|
|||
return
|
||||
|
||||
this.query = query
|
||||
var url = this.url.replace('${query}', query)
|
||||
var url = this.fullUrl.replace('${query}', query).replace('%24%7Bquery%7D', query)
|
||||
var promise = this.model ? this.model.fetch(url, {many:true})
|
||||
: fetch(url, Model.getOptions()).then(d => d.json())
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {Set} from '../model';
|
||||
import Sound from '../sound';
|
||||
import APage from './APage';
|
||||
import {Set} from '../model.js';
|
||||
import Sound from '../sound.js';
|
||||
import APage from './APage.vue';
|
||||
|
||||
export default {
|
||||
extends: APage,
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {getCsrf} from "../model"
|
||||
import {getCsrf} from "../model.js"
|
||||
|
||||
export default {
|
||||
emit: ["fileChange", "load", "abort", "error"],
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:value="value"/>
|
||||
</template>
|
||||
|
||||
<a-rows ref="rows" :set="set"
|
||||
<a-rows ref="rows" :set="set" :context="this"
|
||||
:columns="visibleFields" :columnsOrderable="columnsOrderable"
|
||||
:orderable="orderable" @move="moveItem" @colmove="onColumnMove"
|
||||
@cell="e => $emit('cell', e)">
|
||||
|
@ -118,8 +118,6 @@
|
|||
formData: Object,
|
||||
//! Model class used for item's set
|
||||
model: {type: Function, default: Model},
|
||||
//! initial data set load at mount
|
||||
initials: Array,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -184,7 +182,7 @@
|
|||
|
||||
//! Reset forms to initials
|
||||
reset() {
|
||||
this.load(this.initials || [], true)
|
||||
this.load(this.formData?.initials || [], true)
|
||||
},
|
||||
},
|
||||
|
||||
|
|
102
assets/src/components/AGroupUsers.vue
Normal file
102
assets/src/components/AGroupUsers.vue
Normal file
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div class="a-group-users">
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Members
|
||||
</th>
|
||||
<th style="width: 1rem">
|
||||
<span class="icon">
|
||||
<i class="fa fa-trash"/>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="item of items" :key="item.id">
|
||||
<tr>
|
||||
<td>
|
||||
<b class="mr-3">{{ item.data.user.username }}</b>
|
||||
<span class="text-light">{{ item.data.user.first_name }} {{ item.data.user.last_name }}</span>
|
||||
</td>
|
||||
<td class="align-center">
|
||||
<input type="checkbox" class="checkbox" @change="item.deleted = $event.target.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<label>
|
||||
<span class="icon">
|
||||
<i class="fa fa-user"/>
|
||||
</span>
|
||||
Add user
|
||||
</label>
|
||||
<a-autocomplete ref="autocomplete" :url="searchUrl"
|
||||
label-field="username" value-field="id"
|
||||
@select="onUserSelect">
|
||||
<template #item="{item}">
|
||||
<b class="mr-3">{{ item.username }}</b>
|
||||
<span class="text-light">{{ item.first_name }} {{ item.last_name }}</span>
|
||||
—
|
||||
<i>{{ item.email }}</i>
|
||||
</template>
|
||||
</a-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Model, { Set } from "../model.js"
|
||||
import AAutocomplete from "./AAutocomplete.vue"
|
||||
|
||||
export default {
|
||||
components: {AAutocomplete},
|
||||
props: {
|
||||
model: {type: Function, default: Model },
|
||||
// List url
|
||||
url: String,
|
||||
// User autocomplete url
|
||||
searchUrl: String,
|
||||
// POST url
|
||||
commitUrl: String,
|
||||
// default values
|
||||
initials: {type: Object, default: () => ({})},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
set: new Set(this.model, {url: this.url, unique: true}),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
items() { return this.set?.items || [] },
|
||||
user_ids() { return this.set?.items.map(i => i.data.user.id) },
|
||||
},
|
||||
|
||||
methods: {
|
||||
onUserSelect(index, item, value) {
|
||||
if(this.user_ids.indexOf(item.id) != -1)
|
||||
return
|
||||
|
||||
this.set.push({
|
||||
...this.initials,
|
||||
user: {...item},
|
||||
})
|
||||
this.$refs.autocomplete.reset()
|
||||
},
|
||||
|
||||
save() {
|
||||
this.set.commit(this.commitUrl, {
|
||||
getData: i => ({...this.initials, user_id: i.data.user.id})
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.set.fetch()
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -4,9 +4,9 @@
|
|||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<div class="modal-card-title">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
<slot name="title" :item="item">{{ title }}</slot>
|
||||
</div>
|
||||
<slot name="bar"></slot>
|
||||
<slot name="bar" :item="item"></slot>
|
||||
<button type="button" class="delete square" aria-label="close" @click="close">
|
||||
<span class="icon">
|
||||
<i class="fa fa-close"></i>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<template>
|
||||
<tr>
|
||||
<slot name="head" :item="item" :row="row"/>
|
||||
<slot name="head" :context="context" :item="item" :row="row"/>
|
||||
<template v-for="(attr,col) in columns" :key="col">
|
||||
<slot name="cell-before" :item="item" :cell="cells[col]"
|
||||
<slot name="cell-before" :context="context" :item="item" :cell="cells[col]"
|
||||
:attr="attr"/>
|
||||
<component :is="cellTag" :class="['cell', 'cell-' + attr]" :data-col="col"
|
||||
:draggable="orderable"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
|
||||
<slot :name="attr" :item="item" :cell="cells[col]"
|
||||
<slot :name="attr" :context="context" :item="item" :cell="cells[col]"
|
||||
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||
:value="itemData && itemData[attr]">
|
||||
{{ itemData && itemData[attr] }}
|
||||
</slot>
|
||||
<slot name="cell" :item="item" :cell="cells[col]"
|
||||
<slot name="cell" :context="context" :item="item" :cell="cells[col]"
|
||||
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||
:value="itemData && itemData[attr]"/>
|
||||
</component>
|
||||
<slot name="cell-after" :item="item" :col="col" :cell="cells[col]"
|
||||
<slot name="cell-after" :context="context" :item="item" :col="col" :cell="cells[col]"
|
||||
:attr="attr"/>
|
||||
</template>
|
||||
<slot name="tail" :item="item" :row="row"/>
|
||||
<slot name="tail" :context="context" :item="item" :row="row"/>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -30,6 +30,8 @@ export default {
|
|||
emits: ['move', 'cell'],
|
||||
|
||||
props: {
|
||||
//! Context object
|
||||
context: {type: Object, default: () => ({})},
|
||||
//! Item to display in row
|
||||
item: {type: Object, default: () => ({})},
|
||||
//! Columns to display, as items' attributes
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<table class="table is-stripped is-fullwidth">
|
||||
<thead>
|
||||
<a-row :columns="columnNames"
|
||||
<a-row :context="context" :columns="columnNames"
|
||||
:orderable="columnsOrderable" cellTag="th"
|
||||
@move="moveColumn">
|
||||
<template v-if="$slots['header-head']" v-slot:head="data">
|
||||
|
@ -26,7 +26,7 @@
|
|||
<slot name="head"/>
|
||||
<template v-for="(item,row) in items" :key="row">
|
||||
<!-- data-index comes from AList component drag & drop -->
|
||||
<a-row :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
|
||||
<a-row :context="context" :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
|
||||
:data-row="row"
|
||||
:draggable="orderable"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
|
||||
|
@ -54,6 +54,9 @@ const Component = {
|
|||
|
||||
props: {
|
||||
...AList.props,
|
||||
//! Context object
|
||||
context: {type: Object, default: () => ({})},
|
||||
|
||||
//! Ordered list of columns, as objects with:
|
||||
//! - name: item attribute value
|
||||
//! - label: display label
|
||||
|
|
23
assets/src/components/admin.js
Normal file
23
assets/src/components/admin.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import AFileUpload from "./AFileUpload.vue"
|
||||
import ASelectFile from "./ASelectFile.vue"
|
||||
import AStatistics from './AStatistics.vue'
|
||||
import AStreamer from './AStreamer.vue'
|
||||
|
||||
import AFormSet from './AFormSet.vue'
|
||||
import ATrackListEditor from './ATrackListEditor.vue'
|
||||
import ASoundListEditor from './ASoundListEditor.vue'
|
||||
|
||||
import AGroupUsers from "./AGroupUsers.vue"
|
||||
|
||||
import base from "./index.js"
|
||||
|
||||
|
||||
export const admin = {
|
||||
...base,
|
||||
AGroupUsers,
|
||||
AFileUpload, ASelectFile,
|
||||
AFormSet, ATrackListEditor, ASoundListEditor,
|
||||
AStatistics, AStreamer,
|
||||
}
|
||||
|
||||
export default admin
|
|
@ -1,45 +1,26 @@
|
|||
import AAutocomplete from './AAutocomplete.vue'
|
||||
import AModal from "./AModal.vue"
|
||||
import AActionButton from './AActionButton.vue'
|
||||
import AAutocomplete from './AAutocomplete'
|
||||
import ACarousel from './ACarousel'
|
||||
import ADropdown from "./ADropdown"
|
||||
import AEpisode from './AEpisode'
|
||||
import AList from './AList'
|
||||
import APage from './APage'
|
||||
import APlayer from './APlayer'
|
||||
import APlaylist from './APlaylist'
|
||||
import AProgress from './AProgress'
|
||||
import ASoundItem from './ASoundItem'
|
||||
import ASwitch from './ASwitch'
|
||||
import ADropdown from "./ADropdown.vue"
|
||||
import ACarousel from './ACarousel.vue'
|
||||
import AEpisode from './AEpisode.vue'
|
||||
import AList from './AList.vue'
|
||||
import APage from './APage.vue'
|
||||
import APlayer from './APlayer.vue'
|
||||
import APlaylist from './APlaylist.vue'
|
||||
import AProgress from './AProgress.vue'
|
||||
import ASoundItem from './ASoundItem.vue'
|
||||
import ASwitch from './ASwitch.vue'
|
||||
|
||||
import AModal from "./AModal"
|
||||
import AFileUpload from "./AFileUpload"
|
||||
import ASelectFile from "./ASelectFile"
|
||||
import AStatistics from './AStatistics'
|
||||
import AStreamer from './AStreamer'
|
||||
|
||||
import AFormSet from './AFormSet'
|
||||
import ATrackListEditor from './ATrackListEditor'
|
||||
import ASoundListEditor from './ASoundListEditor'
|
||||
|
||||
/**
|
||||
* Core components
|
||||
*/
|
||||
export const base = {
|
||||
AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
|
||||
AActionButton, AAutocomplete, AModal,
|
||||
ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
|
||||
AProgress, ASoundItem, ASwitch,
|
||||
|
||||
}
|
||||
|
||||
export default base
|
||||
|
||||
export const admin = {
|
||||
...base,
|
||||
ATrackListEditor
|
||||
}
|
||||
|
||||
export const dashboard = {
|
||||
...base,
|
||||
AActionButton, AFileUpload, ASelectFile, AModal,
|
||||
AFormSet, ATrackListEditor, ASoundListEditor,
|
||||
AStatistics, AStreamer,
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import './styles/admin.scss'
|
||||
import './index.js'
|
||||
|
||||
import App from './app';
|
||||
import {dashboard as components} from './components'
|
||||
|
||||
const DashboardApp = {
|
||||
...App,
|
||||
components: {...App.components, ...components},
|
||||
|
||||
data() {
|
||||
return {
|
||||
modalItem: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...App.methods,
|
||||
|
||||
fileSelected(select, input, preview) {
|
||||
const item = this.$refs[select].item
|
||||
if(item) {
|
||||
this.$refs[input].value = item.id
|
||||
if(preview)
|
||||
preview.src = item.file
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
export default DashboardApp;
|
||||
|
||||
|
||||
window.App = DashboardApp
|
|
@ -3,6 +3,8 @@
|
|||
* administration interface)
|
||||
*/
|
||||
|
||||
import 'vue'
|
||||
|
||||
//-- aircox
|
||||
import App, {PlayerApp} from './app'
|
||||
import VueLoader from './vueLoader'
|
||||
|
|
|
@ -113,7 +113,7 @@ export default class Model {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update instance's data with provided data. Return None
|
||||
* Set instance's data with provided data. Return None
|
||||
*/
|
||||
commit(data) {
|
||||
this.data = data;
|
||||
|
@ -121,11 +121,17 @@ export default class Model {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update model data, without reset previous value
|
||||
* Update model data, without reset previous value.
|
||||
* Item is marked as updated.
|
||||
*/
|
||||
update(data) {
|
||||
this.data = {...this.data, ...data}
|
||||
this.id = this.constructor.getId(this.data)
|
||||
this.updated = true
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.deleted = true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,8 +183,24 @@ export class Set {
|
|||
this.push(item, {args: args, save: false});
|
||||
}
|
||||
|
||||
//! Return total items count
|
||||
get length() { return this.items.length }
|
||||
|
||||
//! Return a list of items marked as deleted
|
||||
get deletedItems() {
|
||||
return this.items.filter(i => i.deleted)
|
||||
}
|
||||
|
||||
//! Return a list of created items
|
||||
get createdItems() {
|
||||
return this.items.filter(i => !i.deleted && !i.id)
|
||||
}
|
||||
|
||||
//! Return a list of updated items
|
||||
get updatedItems() {
|
||||
return this.items.filter(i => i.updated)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple items from server
|
||||
*/
|
||||
|
@ -190,6 +212,58 @@ export class Set {
|
|||
.map(d => new model(d, {url: url, ...args})))
|
||||
}
|
||||
|
||||
fetch({url=null, reset=false, ...options}={}, args=null) {
|
||||
url = url || this.url
|
||||
options = this.model.getOptions(options)
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data =>
|
||||
(data instanceof Array ? data : data.results)
|
||||
.map(d => new this.model(d, {url: url, ...args}))
|
||||
)
|
||||
.then(data => {
|
||||
if(reset)
|
||||
this.items = data
|
||||
else
|
||||
// TODO: remove duplicate
|
||||
this.items = [...this.items, ...data]
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit changes to server.
|
||||
* ref: `views.mixin.ListCommitMixin`
|
||||
*/
|
||||
commit(url, {getData=null, ...options}={}) {
|
||||
const createdItems = this.createdItems
|
||||
const body = {
|
||||
delete: this.deletedItems.map(i => i.id),
|
||||
update: this.updatedItems.map(getData),
|
||||
create: createdItems.map(getData),
|
||||
}
|
||||
if(!body.delete && !body.update && !body.create)
|
||||
return
|
||||
|
||||
getData = getData || ((i) => i.data);
|
||||
options = this.model.getOptions(options)
|
||||
options.method = "POST"
|
||||
options.body = JSON.stringify(body)
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const {created, updated, deleted} = data
|
||||
if(createdItems)
|
||||
this.items = this.items.filter(i => createdItems.indexOf(i) == -1)
|
||||
if(deleted)
|
||||
this.items = this.items.filter(i => deleted.indexOf(i.id) == -1)
|
||||
|
||||
this.extend(created)
|
||||
this.extend(updated)
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load list from localStorage
|
||||
*/
|
||||
|
@ -234,22 +308,30 @@ export class Set {
|
|||
: this.items.findIndex(x => x.id == pred.id);
|
||||
}
|
||||
|
||||
extend(items, options) {
|
||||
items.forEach(i => this.push(i, options))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to set, return index.
|
||||
* If item already exists, replace it.
|
||||
*/
|
||||
push(item, {args={},save=true}={}) {
|
||||
item = item instanceof this.model ? item : new this.model(item, args);
|
||||
if(this.unique) {
|
||||
let index = this.findIndex(item);
|
||||
let index = -1
|
||||
if(this.unique && item.id) {
|
||||
index = this.findIndex(item);
|
||||
if(index > -1)
|
||||
return index;
|
||||
this.items[index] = item
|
||||
}
|
||||
if(index == -1) {
|
||||
if(this.max && this.items.length >= this.max)
|
||||
this.items.splice(0,this.items.length-this.max)
|
||||
|
||||
this.items.push(item);
|
||||
save && this.save();
|
||||
return this.items.length-1;
|
||||
this.items.push(item)
|
||||
index = this.items.length-1
|
||||
}
|
||||
save && this.save()
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,4 @@ import "./styles/public.scss"
|
|||
import './index.js'
|
||||
import App from './app.js'
|
||||
|
||||
export default App
|
||||
|
||||
window.App = App
|
||||
|
|
|
@ -309,9 +309,9 @@
|
|||
.preview-header {
|
||||
width: 100%;
|
||||
|
||||
&:not(.no-cover) {
|
||||
/*&:not(.no-cover) {
|
||||
min-height: var(--header-height);
|
||||
}
|
||||
}*/
|
||||
|
||||
&.no-cover {
|
||||
height: unset;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use "./vars" as v;
|
||||
|
||||
// ---- text
|
||||
.text-light { weight: 400; color: var(--text-color-light); }
|
||||
.text-light { font-weight: 400; color: var(--text-color-light); }
|
||||
|
||||
.bigger { font-size: v.$text-size-bigger !important; }
|
||||
.big { font-size: v.$text-size-big !important; }
|
||||
|
@ -21,6 +21,10 @@
|
|||
|
||||
&.x { padding-right: 0px !important; }
|
||||
}
|
||||
.align-center {
|
||||
text-align: center !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-left { clear: left !important }
|
||||
.clear-right { clear: right !important }
|
||||
|
|
|
@ -398,7 +398,7 @@ nav li {
|
|||
}
|
||||
}
|
||||
.header-cover:only-child {
|
||||
with: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: v.$screen-small) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import 'v-calendar/style.css';
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
// @import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
// ---- bulma
|
||||
$body-color: #000;
|
||||
|
|
44
assets/vite.config.js
Normal file
44
assets/vite.config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { resolve } from 'path'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
build: {
|
||||
outDir: "../aircox/static/aircox/",
|
||||
sourcemap: true,
|
||||
|
||||
rollupOptions: {
|
||||
external: ['vue',],
|
||||
input: {
|
||||
public: "src/public.js",
|
||||
admin: "src/admin.js",
|
||||
},
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
},
|
||||
assetFileNames: "[name].[ext]",
|
||||
chunkFileNames: "[name].js",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
plugins: [commonjs()],
|
||||
},
|
||||
},
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.json', '.vue'],
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,23 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
outputDir: path.resolve('../aircox/static/aircox'),
|
||||
publicPath: './',
|
||||
runtimeCompiler: true,
|
||||
filenameHashing: false,
|
||||
|
||||
css: {
|
||||
extract: true,
|
||||
loaderOptions: {
|
||||
sass: { sourceMap: true },
|
||||
}
|
||||
},
|
||||
|
||||
pages: {
|
||||
public: { entry: 'src/public.js' },
|
||||
dashboard: { entry: 'src/dashboard.js' },
|
||||
admin: { entry: 'src/admin.js' },
|
||||
}
|
||||
})
|
|
@ -254,4 +254,8 @@ WSGI_APPLICATION = "instance.wsgi.application"
|
|||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
|
||||
REST_FRAMEWORK = {"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 50}
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
|
||||
"PAGE_SIZE": 50,
|
||||
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user