Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (115)
Showing
with 452 additions and 114 deletions
......@@ -34,6 +34,13 @@ As legacy pages are no longer themed, you should update them to the new frontend
To make setting names consistent, the setting `auth.login.registration.unique_email`
was renamed to `auth.registration.unique_email`.
To prevent collisions with fields, the class variable `name` on `RegistryObject` has been renamed
to `_class_name`. Please update any references and subclasses.
To prevent collisions with fields, the class variables `verbose_name`, `link`, `color`,
`description` and `permission_required` on `DAVResource` have been prefixed with `dav_`.
Please update any references and subclasses.
Added
~~~~~
......@@ -67,6 +74,7 @@ Added
* Global school term select for limiting data to a specific school term.
* [Dev] Notifications based on calendar alarms.
* CalDAV and CardDAV for syncing calendars and Persons read-only.
* Generic Roles for describing relationships. Currently used for Person-to-Group relationships.
Changed
~~~~~~~
......@@ -92,6 +100,8 @@ Changed
* Adapt permission scheme for announcements to other permissions.
* Use Firefox instead of Chromium for PDF creation and support external webdriver via
`selenium.url` option, e.g. for use in containers.
* Rename `RegistryObject`'s class var `name` to `_class_name`.
* Prefix `DAVResource`'s class vars with `dav_`.
Fixed
~~~~~
......@@ -112,6 +122,7 @@ Fixed
* The OpenID Connect Discovery endpoint now returns the issuer data directly
under the URI without a trailing `/`.
* Not-logged in users were able to access all PDF files (CVE-2025-25683).
* Accessibility issues with new frontend.
Removed
~~~~~~~
......
......@@ -14,6 +14,7 @@ from django.utils.text import slugify
from django.utils.translation import gettext as _
import reversion
from color_contrast import AccessibilityLevel, ModulationMode, check_contrast, modulate
from reversion import set_comment
from tqdm import tqdm
......@@ -40,7 +41,7 @@ class SolveOption:
from django.utils.translation import gettext as _
class DeleteSolveOption(SolveOption):
name = "delete" # has to be unqiue
_class_name = "delete" # has to be unqiue
verbose_name = _("Delete") # should make use of i18n
@classmethod
......@@ -52,7 +53,7 @@ class SolveOption:
the corresponding data check result has to be deleted.
"""
name: str = "default"
_class_name: str = "default"
verbose_name: str = ""
@classmethod
......@@ -63,7 +64,7 @@ class SolveOption:
class IgnoreSolveOption(SolveOption):
"""Mark the object with data issues as solved."""
name = "ignore"
_class_name = "ignore"
verbose_name = _("Ignore problem")
@classmethod
......@@ -101,7 +102,7 @@ class DataCheck(RegistryObject):
required_for_migrations = True # Make mandatory for migrations
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption
IgnoreSolveOption._class_name: IgnoreSolveOption
}
@classmethod
......@@ -132,7 +133,7 @@ class DataCheck(RegistryObject):
The dictionary ``solve_options`` should include at least the IgnoreSolveOption,
but preferably also own solve options. The keys in this dictionary
have to be ``<YourOption>SolveOption.name``
have to be ``<YourOption>SolveOption._class_name``
and the values must be the corresponding solve option classes.
The class method ``check_data`` does the actual work. In this method
......@@ -170,7 +171,7 @@ class DataCheck(RegistryObject):
required_for_migrations: bool = False
migration_dependencies: list[str] = []
solve_options = {IgnoreSolveOption.name: IgnoreSolveOption}
solve_options = {IgnoreSolveOption._class_name: IgnoreSolveOption}
_current_results = []
......@@ -206,7 +207,7 @@ class DataCheck(RegistryObject):
def get_results(cls):
DataCheckResult = apps.get_model("core", "DataCheckResult")
return DataCheckResult.objects.filter(data_check=cls.name)
return DataCheckResult.objects.filter(data_check=cls._class_name)
@classmethod
def register_result(cls, instance) -> "DataCheckResult":
......@@ -219,7 +220,7 @@ class DataCheck(RegistryObject):
ct = ContentType.objects.get_for_model(instance)
result, __ = DataCheckResult.objects.get_or_create(
data_check=cls.name, content_type=ct, object_id=instance.id
data_check=cls._class_name, content_type=ct, object_id=instance.id
)
# Track all existing problems (for deleting old results)
......@@ -242,7 +243,7 @@ class DataCheck(RegistryObject):
@classproperty
def data_checks_choices(cls):
return [(check.name, check.verbose_name) for check in cls.registered_objects_list]
return [(check._class_name, check.verbose_name) for check in cls.registered_objects_list]
@recorded_task(run_every=timedelta(minutes=15))
......@@ -325,7 +326,7 @@ def send_emails_for_data_checks():
class DeactivateDashboardWidgetSolveOption(SolveOption):
name = "deactivate_dashboard_widget"
_class_name = "deactivate_dashboard_widget"
verbose_name = _("Deactivate DashboardWidget")
@classmethod
......@@ -337,12 +338,12 @@ class DeactivateDashboardWidgetSolveOption(SolveOption):
class BrokenDashboardWidgetDataCheck(DataCheck):
name = "broken_dashboard_widgets"
_class_name = "broken_dashboard_widgets"
verbose_name = _("Ensure that there are no broken DashboardWidgets.")
problem_name = _("The DashboardWidget was reported broken automatically.")
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption.name: DeactivateDashboardWidgetSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption._class_name: DeactivateDashboardWidgetSolveOption,
}
@classmethod
......@@ -366,7 +367,7 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
)
problem_name = _("The field {} couldn't be validated successfully.").format(field_name)
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
......@@ -385,11 +386,11 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
class DisallowedUIDDataCheck(DataCheck):
name = "disallowed_uid"
_class_name = "disallowed_uid"
verbose_name = _("Ensure that there are no disallowed usernames.")
problem_name = _("A user with a disallowed username was reported automatically.")
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
......@@ -407,19 +408,19 @@ field_validation_data_check_factory("core", "CustomMenuItem", "icon")
class ChangeEmailAddressSolveOption(SolveOption):
name = "change_email_address"
_class_name = "change_email_address"
verbose_name = _("Change email address")
class EmailUniqueDataCheck(DataCheck):
name = "email_unique"
_class_name = "email_unique"
verbose_name = _("Ensure that email addresses are unique among all persons")
problem_name = _("There was a non-unique email address.")
required_for_migrations = True
migration_dependencies = [("core", "0057_drop_otp_yubikey")]
solve_options = {ChangeEmailAddressSolveOption.name: ChangeEmailAddressSolveOption}
solve_options = {ChangeEmailAddressSolveOption._class_name: ChangeEmailAddressSolveOption}
@classmethod
def check_data(cls):
......@@ -432,3 +433,194 @@ class EmailUniqueDataCheck(DataCheck):
if person.email and person.email in known_email_addresses:
cls.register_result(person)
known_email_addresses.add(person.email)
def accessible_colors_factory(
app_name: str,
model_name: str,
fg_field_name: str = None,
bg_field_name: str = None,
fg_color: str = "#ffffff",
bg_color: str = "#000000",
modulation_mode: ModulationMode = ModulationMode.BOTH,
) -> None:
ColorAccessibilityDataCheck.models.append(
(
app_name,
model_name,
fg_field_name,
bg_field_name,
fg_color,
bg_color,
modulation_mode,
)
)
def _get_colors_from_model_instance(instance, fg_field_name, bg_field_name, fg_color, bg_color):
colors: list[str] = [fg_color, bg_color]
if fg_field_name is not None:
colors[0] = getattr(instance, fg_field_name)
if bg_field_name is not None:
colors[1] = getattr(instance, bg_field_name)
# Transparency is not support for checking contrasts, so simply truncate it
for index, color in enumerate(colors):
if not color.startswith("#"):
continue
if len(color) == 5:
# color is of format "#RGBA"
colors[index] = color[:-1]
elif len(color) == 9:
# color is of format "#RRGGBBAA"
colors[index] = color[:-2]
return colors
class ModulateColorsSolveOption(SolveOption):
name = "modulate_colors"
verbose_name = _("Auto-adjust Colors")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
instance = check_result.related_object
ctype = check_result.content_type
model_info = list(
filter(
lambda m: m[0] == ctype.app_label and m[1] == ctype.model,
ColorAccessibilityDataCheck.models,
)
)
if len(model_info) == 0:
check_result.solved = False
check_result.save()
logging.error(f"Modulate Colors check failed for {check_result}: Model Info not found")
return
elif len(model_info) > 1:
check_result.solved = False
check_result.save()
logging.error(f"Modulate Colors check failed for {check_result}: Duplicate Model Info")
return
[_, _, fg_field_name, bg_field_name, fg_color, bg_color, modulation_mode] = model_info[0]
colors = _get_colors_from_model_instance(
instance, fg_field_name, bg_field_name, fg_color, bg_color
)
fg_new, bg_new, success = modulate(*colors, mode=modulation_mode)
if not success:
check_result.solved = False
check_result.save()
logging.error(
f"Modulate Colors check failed for {check_result}: Modulation not possible"
)
return
if fg_field_name:
setattr(instance, fg_field_name, fg_new)
if bg_field_name:
setattr(instance, bg_field_name, bg_new)
instance.save()
check_result.solved = True
check_result.save()
class ColorAccessibilityDataCheck(DataCheck):
name = "colors_accessibility_datacheck"
verbose_name = _("Validate contrast accessibility of colors of customizable objects.")
problem_name = _("The colors of this object are not accessible.")
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
ModulateColorsSolveOption._class_name: ModulateColorsSolveOption,
}
models = []
@classmethod
def check_data(cls):
from django.apps import apps
for [
app_name,
model_name,
fg_field_name,
bg_field_name,
fg_color,
bg_color,
_modulation_mode,
] in cls.models:
model: Model = apps.get_model(app_name, model_name)
for obj in model.objects.all():
colors = _get_colors_from_model_instance(
obj, fg_field_name, bg_field_name, fg_color, bg_color
)
if not check_contrast(*colors, level=AccessibilityLevel.AA):
logging.info(f"Insufficient contrast in {app_name}.{model_name}.{obj}")
cls.register_result(obj)
class ModulateThemeColorsSolveOption(SolveOption):
name = "modulate_theme_colors"
verbose_name = _("Auto-adjust Color")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
instance = check_result.related_object
preference = f"{instance.section}__{instance.name}"
prefs = get_site_preferences()
color = prefs[preference]
fg_new, color_new, success = modulate("#fff", color, mode=ModulationMode.BACKGROUND)
if not success:
check_result.solved = False
check_result.save()
logging.error(f"Modulate {instance.name} theme color failed: Modulation not possible.")
return
prefs[preference] = str(color_new)
check_result.solved = True
check_result.save()
class AccessibleThemeColorsDataCheck(DataCheck):
name = "accessible_themes_colors_datacheck"
verbose_name = _("Validate that theme colors are accessible.")
problem_name = _("The color does not provide enough contrast")
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
ModulateThemeColorsSolveOption._class_name: ModulateThemeColorsSolveOption,
}
@classmethod
def check_data(cls):
from dynamic_preferences.models import GlobalPreferenceModel
from .util.core_helpers import get_site_preferences
prefs = get_site_preferences()
primary = prefs["theme__primary"]
secondary = prefs["theme__secondary"]
# White text on primary colored background
if not check_contrast("#fff", primary, level=AccessibilityLevel.AA):
logging.info("Insufficient contrast in primary color")
obj = GlobalPreferenceModel.objects.get(section="theme", name="primary")
cls.register_result(obj)
# White text on secondary colored background
if not check_contrast("#fff", secondary, level=AccessibilityLevel.AA):
logging.info("Insufficient contrast in primary color")
obj = GlobalPreferenceModel.objects.get(section="theme", name="secondary")
cls.register_result(obj)
......@@ -31,11 +31,21 @@ const vuetifyOpts = {
holidays: "mdi-calendar-weekend-outline",
home: "mdi-home-outline",
groupType: "mdi-shape-outline",
role: "mdi-badge-account-horizontal-outline",
print: "mdi-printer-outline",
schoolTerm: "mdi-calendar-range-outline",
updatePwa: "mdi-update",
},
},
theme: {
options: {
customProperties: true,
themeCache: {
get: (key) => localStorage.getItem(key),
set: (key, value) => localStorage.setItem(key, value),
},
},
},
};
export default vuetifyOpts;
......@@ -82,6 +82,7 @@ export default {
// Write title of iframe to SPA window
const title = this.$refs.contentIFrame.contentWindow.document.title;
this.$root.$setPageTitle(title);
this.$refs.contentIFrame.title = title;
// Adapt height of IFrame according to the height of its contents once and observe height changes
if (
......
<template>
<v-menu offset-y>
<v-menu offset-y max-height="80vh">
<template #activator="{ on, attrs }">
<v-avatar v-bind="attrs" v-on="on">
<v-avatar
v-bind="attrs"
v-on="on"
tag="button"
tabindex="0"
:aria-label="$t('actions.account_menu')"
>
<img
v-if="
systemProperties.sitePreferences.accountPersonPreferPhoto &&
......
......@@ -17,6 +17,7 @@
/>
<div v-else>
<side-nav
ref="sidenav"
v-model="drawer"
:system-properties="systemProperties"
:side-nav-menu="sideNavMenu"
......@@ -25,14 +26,19 @@
app
:color="$vuetify.theme.dark ? undefined : 'primary white--text'"
>
<v-app-bar-nav-icon @click="drawer = !drawer" color="white" />
<v-app-bar-nav-icon
@click="drawer = !drawer"
color="white"
:aria-label="$t('actions.toogle_sidenav')"
/>
<v-toolbar-title
tag="a"
class="white--text text-decoration-none"
@click="$router.push({ name: 'dashboard' })"
>
{{ $root.toolbarTitle }}
<v-toolbar-title>
<router-link
class="white--text text-decoration-none"
:to="{ name: 'dashboard' }"
>
{{ $root.toolbarTitle }}
</router-link>
</v-toolbar-title>
<v-progress-linear
......@@ -41,7 +47,8 @@
absolute
bottom
:color="$vuetify.theme.dark ? 'primary' : 'grey lighten-3'"
></v-progress-linear>
aria-hidden="true"
/>
<v-spacer />
<v-btn
......@@ -265,7 +272,7 @@ import Mascot from "../generic/mascot/Mascot.vue";
import gqlWhoAmI from "./whoAmI.graphql";
import gqlMessages from "./messages.graphql";
import gqlSystemProperties from "./systemProperties.graphql";
import { gqlSystemProperties } from "./systemProperties.graphql";
import gqlObjectPermissions from "./objectPermissions.graphql";
import useRegisterSWMixin from "../../mixins/useRegisterSW";
......@@ -355,6 +362,12 @@ export default {
},
immediate: true,
},
drawer: function (newValue) {
if (newValue) {
// Drawer was opened, → focus sidenav
this.$refs.sidenav.focusList();
}
},
},
name: "App",
components: {
......@@ -382,7 +395,7 @@ export default {
<style>
div[aria-required="true"] .v-input .v-label::after {
content: " *";
color: red;
color: var(--v-error-base);
}
.main-container {
......
<script setup>
import Mascot from "../generic/mascot/Mascot.vue";
import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue";
</script>
<template>
......@@ -15,14 +16,13 @@ import Mascot from "../generic/mascot/Mascot.vue";
/>
<h1 class="text-h2">{{ $t(shortErrorMessageKey) }}</h1>
<div>{{ $t(longErrorMessageKey) }}</div>
<v-btn
<primary-action-button
color="secondary"
:to="{ name: redirectRouteName }"
v-if="!hideButton"
>
<v-icon left>{{ redirectButtonIcon }}</v-icon>
{{ $t(redirectButtonTextKey) }}
</v-btn>
:icon-text="redirectButtonIcon"
:i18n-key="redirectButtonTextKey"
/>
</div>
</template>
......
......@@ -43,6 +43,7 @@ export default {
document.cookie = languageOption.cookie;
this.$i18n.locale = languageOption.code;
this.$vuetify.lang.current = languageOption.code;
document.getElementsByTagName("html")[0].lang = languageOption.code;
this.language = languageOption;
},
nameForMenu: function (item) {
......
......@@ -4,16 +4,17 @@
:value="value"
height="100dvh"
@input="$emit('input', $event)"
tag="aside"
>
<v-list nav dense shaped>
<v-list-item class="logo">
<a
id="logo-container"
@click="$router.push({ name: 'dashboard' })"
class="brand-logo"
>
<brand-logo :site-preferences="systemProperties.sitePreferences" />
</a>
<v-list nav dense shaped tag="nav">
<v-list-item
class="focusable"
ref="listItem"
:to="{ name: 'dashboard' }"
exact
color="transparent"
>
<brand-logo :site-preferences="systemProperties.sitePreferences" />
</v-list-item>
<v-list-item v-if="checkPermission('core.search_rule')" class="search">
<sidenav-search />
......@@ -146,6 +147,15 @@ export default {
comparator(array, value) {
return Array.isArray(array) && array.includes(value);
},
focusList() {
this.$nextTick(() => {
// console.log(this.$refs.listItem)
console.log(this.$refs.listItem.$el);
this.$refs.listItem.$el.focus();
// let el = document.querySelector(".focusable")
// el.focus()
});
},
},
};
</script>
......
query systemProperties {
query gqlSystemProperties {
systemProperties {
availableLanguages {
code
......@@ -26,3 +26,12 @@ query systemProperties {
}
}
}
query gqlUsernamePreferences {
systemProperties {
sitePreferences {
authAllowedUsernameRegex
authDisallowedUids
}
}
}
......@@ -14,14 +14,20 @@ export default {
<template>
<div class="d-flex justify-center mx-2">
<v-btn icon @click="$emit('prev')" :small="small">
<v-icon>$prev</v-icon>
</v-btn>
<icon-button
@click="$emit('prev')"
:small="small"
icon-text="$prev"
i18n-key="actions.scroll_prev"
/>
<v-btn outlined text class="mx-1" @click="$emit('today')" :small="small">
{{ $t("calendar.today") }}
</v-btn>
<v-btn icon @click="$emit('next')" :small="small">
<v-icon>$next</v-icon>
</v-btn>
<icon-button
@click="$emit('next')"
:small="small"
icon-text="$next"
i18n-key="actions.scroll_next"
/>
</div>
</template>
......@@ -4,12 +4,14 @@
v-for="calendarFeed in calendarFeeds"
:key="calendarFeed.name"
:value="calendarFeed.name"
:tabindex="-1"
>
<template #default="{ active }">
<v-list-item-action>
<v-checkbox
:input-value="active"
:color="calendarFeed.color"
class="focusable"
></v-checkbox>
</v-list-item-action>
......@@ -20,23 +22,22 @@
</v-list-item-content>
<v-list-item-action>
<v-menu bottom>
<template #activator="{ on, attrs }">
<v-btn fab x-small icon v-bind="attrs" v-on="on">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item :href="calendarFeed.url">
<v-list-item-icon>
<v-icon>mdi-calendar-export</v-icon>
</v-list-item-icon>
<v-list-item-title>
{{ $t("calendar.download_ics") }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<button-menu
icon-only
:outlined="false"
icon="mdi-dots-vertical"
:text="false"
text-translation-key="actions.more_actions"
>
<v-list-item :href="calendarFeed.url">
<v-list-item-icon>
<v-icon>mdi-calendar-export</v-icon>
</v-list-item-icon>
<v-list-item-title>
{{ $t("calendar.download_ics") }}
</v-list-item-title>
</v-list-item>
</button-menu>
</v-list-item-action>
</template>
</v-list-item>
......
......@@ -54,6 +54,7 @@ export default {
v-for="calendarType in availableCalendarTypes"
:value="calendarType.type"
:key="calendarType.type"
:aria-label="nameForMenu(calendarType)"
>
<v-icon v-if="$vuetify.breakpoint.smAndDown">{{
calendarType.type === innerValue
......
<template>
<v-btn color="secondary" v-bind="$attrs">
<v-icon left>$prev</v-icon>
{{ $t("actions.back") }}
</v-btn>
</template>
<script>
import PrimaryActionButton from "./buttons/PrimaryActionButton.vue";
export default {
name: "BackButton",
extends: PrimaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$prev",
},
i18nKey: {
type: String,
required: false,
default: "actions.back",
},
color: {
required: false,
default: "secondary",
},
},
};
</script>
......@@ -4,14 +4,26 @@
offset-y
:close-on-content-click="closeOnContentClick"
>
<template #activator="{ on, attrs }">
<slot name="activator" v-bind="{ on, attrs }">
<v-btn outlined text v-bind="attrs" v-on="on">
<v-icon :left="!!textTranslationKey" :center="!textTranslationKey">
{{ icon }}
</v-icon>
<span v-if="textTranslationKey">{{ $t(textTranslationKey) }}</span>
</v-btn>
<template #activator="menu">
<slot name="activator" v-bind="menu">
<v-tooltip bottom :disabled="!iconOnly">
<template #activator="tooltip">
<v-btn
:outlined="outlined"
:text="text"
:icon="!outlined && iconOnly"
v-bind="{ ...tooltip.attrs, ...menu.attrs }"
v-on="{ ...tooltip.on, ...menu.on }"
:aria-label="$t(textTranslationKey)"
>
<v-icon :left="!iconOnly" :center="iconOnly">
{{ icon }}
</v-icon>
<span v-if="!iconOnly">{{ $t(textTranslationKey) }}</span>
</v-btn>
</template>
<span v-if="iconOnly">{{ $t(textTranslationKey) }}</span>
</v-tooltip>
</slot>
</template>
......@@ -32,14 +44,26 @@ export default {
},
textTranslationKey: {
type: String,
required: true,
},
iconOnly: {
type: Boolean,
required: false,
default: "",
default: false,
},
closeOnContentClick: {
type: Boolean,
required: false,
default: true,
},
outlined: {
type: Boolean,
default: true,
},
text: {
type: Boolean,
default: true,
},
},
};
</script>
......
......@@ -26,9 +26,13 @@ export default {
<v-card tile class="full-width">
<v-card-title class="auto-margin">
<div class="d-flex align-center justify-center full-width">
<v-btn icon large class="me-4" @click="$emit('prev')">
<v-icon>$prev</v-icon>
</v-btn>
<icon-button
large
class="me-4"
@click="$emit('prev')"
icon-text="$prev"
i18n-key="actions.scroll_prev"
/>
<div class="flex-grow-0">
<date-field
solo-inverted
......@@ -41,9 +45,13 @@ export default {
readonly
/>
</div>
<v-btn icon large class="ms-4" @click="$emit('next')">
<v-icon>$next</v-icon>
</v-btn>
<icon-button
large
class="ms-4"
@click="$emit('next')"
icon-text="$next"
i18n-key="actions.scroll_next"
/>
</div>
</v-card-title>
</v-card>
......
<script>
import SecondaryActionButton from "./buttons/SecondaryActionButton.vue";
export default {
name: "SlideIterator",
components: { SecondaryActionButton },
extends: "v-data-iterator",
data() {
return {
......@@ -113,9 +110,12 @@ export default {
/>
</v-list-item-content>
<v-list-item-action>
<v-btn icon large :disabled="disabled">
<v-icon large>$next</v-icon>
</v-btn>
<icon-button
large
:disabled="disabled"
icon-text="$next"
i18n-key="actions.open_details"
/>
</v-list-item-action>
</template>
</v-list-item>
......
<template>
<v-btn v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<slot>
<v-icon v-if="iconText" :left="!icon">{{ iconText }}</v-icon>
<span v-if="!icon" v-t="i18nKey" />
</slot>
</v-btn>
<v-tooltip bottom :disabled="!icon && !forceTooltip" eager tag="div">
<template #activator="{ on, attrs }">
<v-btn
v-bind="{ ...$props, ...attrs, ...$attrs }"
v-on="{ ...on, ...$listeners }"
:aria-label="$t(i18nKey)"
>
<slot>
<v-icon v-if="iconText" :left="!icon">{{ iconText }}</v-icon>
<span v-if="!icon" v-t="i18nKey" />
</slot>
</v-btn>
</template>
<span v-if="forceTooltip || icon" v-t="i18nKey" />
</v-tooltip>
</template>
<script>
import VBtn from "@/vuetify/lib/components/VBtn";
export default {
name: "BaseButton",
inheritAttrs: true,
extends: "v-btn",
extends: VBtn,
props: {
i18nKey: {
type: String,
......@@ -31,6 +42,11 @@ export default {
required: false,
default: false,
},
forceTooltip: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
......
<template>
<v-btn v-bind="{ ...$props, ...$attrs }" v-on="$listeners" icon large>
<v-icon>$close</v-icon>
</v-btn>
<icon-button
v-bind="{ ...$props, ...$attrs }"
v-on="$listeners"
:icon-text="iconText"
:i18n-key="i18nKey"
large
/>
</template>
<script>
import IconButton from "./IconButton.vue";
export default {
name: "DialogCloseButton",
components: { IconButton },
inheritAttrs: true,
extends: "v-btn",
extends: IconButton,
props: {
iconText: {
required: false,
default: "$close",
},
i18nKey: {
required: false,
default: "actions.close",
},
},
};
</script>
......@@ -8,24 +8,24 @@
<v-badge color="secondary" :value="numFilters" :content="numFilters" inline>
<span v-t="i18nKey" />
</v-badge>
<v-btn
icon
<icon-button
@click.stop="$emit('clear')"
small
v-if="numFilters"
class="mr-n1"
>
<v-icon>$clear</v-icon>
</v-btn>
icon-text="$clear"
i18n-key="actions.clear_filters"
/>
</secondary-action-button>
</template>
<script>
import SecondaryActionButton from "./SecondaryActionButton.vue";
import IconButton from "./IconButton.vue";
export default {
name: "FilterButton",
components: { SecondaryActionButton },
components: { IconButton, SecondaryActionButton },
extends: SecondaryActionButton,
computed: {
filterIcon() {
......