dev into staging #442

Merged
edraft merged 43 commits from dev into staging 2023-11-19 14:50:00 +01:00
65 changed files with 2977 additions and 188 deletions
Showing only changes of commit c8d3bf780d - Show all commits

View File

@ -21,7 +21,7 @@ class TaskABC(commands.Cog):
@ServiceProviderABC.inject
async def _wait_until_ready(self, config: ConfigurationABC, logger: TaskLogger, bot: DiscordBotServiceABC):
logger.debug(__name__, f"Waiting before {type(self).__name__}")
logger.debug(__name__, f"Waiting before ready {type(self).__name__}")
await bot.wait_until_ready()
async def wait():

View File

@ -27,3 +27,4 @@ class FeatureFlagsEnum(Enum):
short_role_name = "ShortRoleName"
technician_full_access = "TechnicianFullAccess"
steam_special_offers = "SteamSpecialOffers"
scheduled_events = "ScheduledEvents"

View File

@ -29,6 +29,7 @@ class FeatureFlagsSettings(ConfigurationModelABC):
FeatureFlagsEnum.short_role_name.value: False, # 28.09.2023 #378
FeatureFlagsEnum.technician_full_access.value: False, # 03.10.2023 #393
FeatureFlagsEnum.steam_special_offers.value: False, # 11.10.2023 #188
FeatureFlagsEnum.scheduled_events.value: False, # 14.11.2023 #410
}
def __init__(self, **kwargs: dict):

View File

@ -0,0 +1,35 @@
from abc import ABC, abstractmethod
from cpl_query.extension import List
from bot_data.model.scheduled_event import ScheduledEvent
class ScheduledEventRepositoryABC(ABC):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def get_scheduled_events(self) -> List[ScheduledEvent]:
pass
@abstractmethod
def get_scheduled_event_by_id(self, id: int) -> ScheduledEvent:
pass
@abstractmethod
def get_scheduled_events_by_server_id(self, id: int) -> List[ScheduledEvent]:
pass
@abstractmethod
def add_scheduled_event(self, scheduled_event: ScheduledEvent):
pass
@abstractmethod
def update_scheduled_event(self, scheduled_event: ScheduledEvent):
pass
@abstractmethod
def delete_scheduled_event(self, scheduled_event: ScheduledEvent):
pass

View File

@ -14,6 +14,7 @@ from bot_data.abc.data_seeder_abc import DataSeederABC
from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC
from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC
from bot_data.abc.level_repository_abc import LevelRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.abc.server_config_repository_abc import ServerConfigRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC
@ -45,6 +46,7 @@ from bot_data.service.client_repository_service import ClientRepositoryService
from bot_data.service.game_server_repository_service import GameServerRepositoryService
from bot_data.service.known_user_repository_service import KnownUserRepositoryService
from bot_data.service.level_repository_service import LevelRepositoryService
from bot_data.service.scheduled_event_repository_service import ScheduledEventRepositoryService
from bot_data.service.seeder_service import SeederService
from bot_data.service.server_config_repository_service import (
ServerConfigRepositoryService,
@ -115,6 +117,7 @@ class DataModule(ModuleABC):
services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService)
services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService)
services.add_transient(SteamSpecialOfferRepositoryABC, SteamSpecialOfferRepositoryService)
services.add_transient(ScheduledEventRepositoryABC, ScheduledEventRepositoryService)
services.add_transient(SeederService)
services.add_transient(DataSeederABC, TechnicianConfigSeeder)

View File

@ -0,0 +1,188 @@
from datetime import datetime
from typing import Optional
import discord
from cpl_core.database import TableABC
from bot_data.model.scheduled_event_interval_enum import ScheduledEventIntervalEnum
from bot_data.model.server import Server
class ScheduledEvent(TableABC):
def __init__(
self,
interval: ScheduledEventIntervalEnum,
name: str,
description: str,
channel_id: int,
start_time: datetime,
end_time: Optional[datetime],
entity_type: discord.EntityType,
location: Optional[str],
server: Optional[Server],
created_at: datetime = None,
modified_at: datetime = None,
id=0,
):
self._id = id
self._interval = interval
self._name = name
self._description = description
self._channel_id = channel_id
self._start_time = start_time
self._end_time = end_time
self._entity_type = entity_type
self._location = location
self._server = server
TableABC.__init__(self)
self._created_at = created_at if created_at is not None else self._created_at
self._modified_at = modified_at if modified_at is not None else self._modified_at
@property
def id(self) -> int:
return self._id
@property
def interval(self) -> ScheduledEventIntervalEnum:
return self._interval
@interval.setter
def interval(self, value: ScheduledEventIntervalEnum):
self._interval = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def description(self) -> str:
return self._description
@description.setter
def description(self, value: str):
self._description = value
@property
def channel_id(self) -> int:
return self._channel_id
@channel_id.setter
def channel_id(self, value: int):
self._channel_id = value
@property
def start_time(self) -> datetime:
return self._start_time
@start_time.setter
def start_time(self, value: datetime):
self._start_time = value
@property
def end_time(self) -> datetime:
return self._end_time
@end_time.setter
def end_time(self, value: datetime):
self._end_time = value
@property
def entity_type(self) -> discord.EntityType:
return self._entity_type
@entity_type.setter
def entity_type(self, value: discord.EntityType):
self._entity_type = value
@property
def location(self) -> str:
return self._location
@location.setter
def location(self, value: str):
self._location = value
@property
def server(self) -> Server:
return self._server
@server.setter
def server(self, value: Server):
self._server = value
@staticmethod
def get_select_all_string() -> str:
return str(
f"""
SELECT * FROM `ScheduledEvents`;
"""
)
@staticmethod
def get_select_by_id_string(id: int) -> str:
return str(
f"""
SELECT * FROM `ScheduledEvents`
WHERE `Id` = {id};
"""
)
@staticmethod
def get_select_by_server_id_string(s_id: int) -> str:
return str(
f"""
SELECT * FROM `ScheduledEvents`
WHERE `ServerId` = {s_id};
"""
)
@property
def insert_string(self) -> str:
return str(
f"""
INSERT INTO `ScheduledEvents` (
`Interval`, `Name`, `Description`, `ChannelId`, `StartTime`, `EndTime`, `EntityType`, `Location`, `ServerId`
) VALUES (
'{self._interval.value}',
'{self._name}',
{"NULL" if self._description is None else f"'{self._description}'"},
{"NULL" if self._channel_id is None else f"'{self._channel_id}'"},
'{self._start_time}',
{"NULL" if self._end_time is None else f"'{self._end_time}'"},
'{self._entity_type.value}',
{"NULL" if self._location is None else f"'{self._location}'"},
{self._server.id}
);
"""
)
@property
def udpate_string(self) -> str:
return str(
f"""
UPDATE `ScheduledEvents`
SET `Interval` = '{self._interval.value}',
`Name` = '{self._name}',
`Description` = {"NULL" if self._description is None else f"'{self._description}'"},
`ChannelId` = {"NULL" if self._channel_id is None else f"'{self._channel_id}'"},
`StartTime` = '{self._start_time}',
`EndTime` = {"NULL" if self._end_time is None else f"'{self._end_time}'"},
`EntityType` = '{self._entity_type.value}',
`Location` = {"NULL" if self._location is None else f"'{self._location}'"}
WHERE `Id` = {self._id};
"""
)
@property
def delete_string(self) -> str:
return str(
f"""
DELETE FROM `ScheduledEvents`
WHERE `Id` = {self._id};
"""
)

View File

@ -0,0 +1,118 @@
from datetime import datetime
from typing import Optional
import discord
from cpl_core.database import TableABC
from bot_data.abc.history_table_abc import HistoryTableABC
from bot_data.model.server import Server
class ScheduledEventHistory(HistoryTableABC):
def __init__(
self,
interval: str,
name: str,
description: str,
channel_id: int,
start_time: datetime,
end_time: Optional[datetime],
entity_type: discord.EntityType,
location: Optional[str],
server: Optional[Server],
deleted: bool,
date_from: str,
date_to: str,
id=0,
):
HistoryTableABC.__init__(self)
self._id = id
self._interval = interval
self._name = name
self._description = description
self._channel_id = channel_id
self._start_time = start_time
self._end_time = end_time
self._entity_type = entity_type
self._location = location
self._server = server
self._deleted = deleted
self._date_from = date_from
self._date_to = date_to
@property
def id(self) -> int:
return self._id
@property
def interval(self) -> str:
return self._interval
@interval.setter
def interval(self, value: str):
self._interval = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def description(self) -> str:
return self._description
@description.setter
def description(self, value: str):
self._description = value
@property
def channel_id(self) -> int:
return self._channel_id
@channel_id.setter
def channel_id(self, value: int):
self._channel_id = value
@property
def start_time(self) -> datetime:
return self._start_time
@start_time.setter
def start_time(self, value: datetime):
self._start_time = value
@property
def end_time(self) -> datetime:
return self._end_time
@end_time.setter
def end_time(self, value: datetime):
self._end_time = value
@property
def entity_type(self) -> discord.EntityType:
return self._entity_type
@entity_type.setter
def entity_type(self, value: discord.EntityType):
self._entity_type = value
@property
def location(self) -> str:
return self._location
@location.setter
def location(self, value: str):
self._location = value
@property
def server(self) -> Server:
return self._server
@server.setter
def server(self, value: Server):
self._server = value

View File

@ -0,0 +1,8 @@
from enum import Enum
class ScheduledEventIntervalEnum(Enum):
daily = "daily"
weekly = "weekly"
monthly = "monthly"
yearly = "yearly"

View File

@ -0,0 +1,3 @@
DROP TABLE `ShortRoleNames`;
DROP TABLE `ShortRoleNamesHistory`;

View File

@ -0,0 +1,83 @@
CREATE TABLE IF NOT EXISTS `ScheduledEvents`
(
`Id` BIGINT NOT NULL AUTO_INCREMENT,
`Interval` ENUM ('daily', 'weekly', 'monthly', 'yearly') NOT NULL,
`Name` VARCHAR(255) NOT NULL,
`Description` VARCHAR(255) NULL,
`ChannelId` BIGINT NULL,
`StartTime` DATETIME(6) NOT NULL,
`EndTime` DATETIME(6) NULL,
`EntityType` ENUM ('1','2','3') NOT NULL,
`Location` VARCHAR(255) NULL,
`ServerId` BIGINT,
`CreatedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6),
`LastModifiedAt` DATETIME(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`Id`),
FOREIGN KEY (`ServerId`) REFERENCES `Servers` (`ServerId`)
);
CREATE TABLE IF NOT EXISTS `ScheduledEventsHistory`
(
`Id` BIGINT(20) NOT NULL,
`Interval` ENUM ('daily', 'weekly', 'monthly', 'yearly') NOT NULL,
`Name` VARCHAR(255) NOT NULL,
`Description` VARCHAR(255) NULL,
`ChannelId` BIGINT NULL,
`StartTime` DATETIME(6) NOT NULL,
`EndTime` DATETIME(6) NULL,
`EntityType` ENUM ('1','2','3') NOT NULL,
`Location` VARCHAR(255) NULL,
`ServerId` BIGINT(20) DEFAULT NULL,
`Deleted` BOOL DEFAULT FALSE,
`DateFrom` DATETIME(6) NOT NULL,
`DateTo` DATETIME(6) NOT NULL
);
DROP TRIGGER IF EXISTS `TR_ScheduledEventsUpdate`;
CREATE TRIGGER `TR_ScheduledEventsUpdate`
AFTER UPDATE
ON `ScheduledEvents`
FOR EACH ROW
BEGIN
INSERT INTO `ScheduledEventsHistory` (`Id`,
`Interval`,
`Name`,
`Description`,
`ChannelId`,
`StartTime`,
`EndTime`,
`EntityType`,
`Location`,
`ServerId`,
`DateFrom`,
`DateTo`)
VALUES (OLD.Id, OLD.Interval, OLD.Name, OLD.Description, OLD.ChannelId, OLD.StartTime, OLD.EndTime, OLD.EntityType,
OLD.Location, OLD.ServerId, OLD.LastModifiedAt,
CURRENT_TIMESTAMP(6));
END;
DROP TRIGGER IF EXISTS `TR_ScheduledEventsDelete`;
CREATE TRIGGER `TR_ScheduledEventsDelete`
AFTER DELETE
ON `ScheduledEvents`
FOR EACH ROW
BEGIN
INSERT INTO `ScheduledEventsHistory` (`Id`,
`Interval`,
`Name`,
`Description`,
`ChannelId`,
`StartTime`,
`EndTime`,
`EntityType`,
`Location`,
`ServerId`,
`Deleted`,
`DateFrom`,
`DateTo`)
VALUES (OLD.Id, OLD.Interval, OLD.Name, OLD.Description, OLD.ChannelId, OLD.StartTime, OLD.EndTime, OLD.EntityType,
OLD.Location, OLD.ServerId, TRUE, OLD.LastModifiedAt,
CURRENT_TIMESTAMP(6));
END;

View File

@ -0,0 +1,91 @@
from typing import Optional
from cpl_core.database.context import DatabaseContextABC
from cpl_query.extension import List
from discord import EntityType
from bot_core.logging.database_logger import DatabaseLogger
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.model.scheduled_event import ScheduledEvent
from bot_data.model.scheduled_event_interval_enum import ScheduledEventIntervalEnum
class ScheduledEventRepositoryService(ScheduledEventRepositoryABC):
def __init__(
self,
logger: DatabaseLogger,
db_context: DatabaseContextABC,
servers: ServerRepositoryABC,
):
self._logger = logger
self._context = db_context
self._servers = servers
ScheduledEventRepositoryABC.__init__(self)
@staticmethod
def _get_value_from_result(value: any) -> Optional[any]:
if isinstance(value, str) and "NULL" in value:
return None
return value
def _scheduled_event_from_result(self, sql_result: tuple) -> ScheduledEvent:
return ScheduledEvent(
self._get_value_from_result(ScheduledEventIntervalEnum(sql_result[1])), # interval
self._get_value_from_result(sql_result[2]), # name
self._get_value_from_result(sql_result[3]), # description
int(self._get_value_from_result(sql_result[4])), # channel_id
self._get_value_from_result(sql_result[5]), # start_time
self._get_value_from_result(sql_result[6]), # end_time
EntityType(int(self._get_value_from_result(sql_result[7]))), # entity_type
self._get_value_from_result(sql_result[8]), # location
self._servers.get_server_by_id((sql_result[9])), # server
self._get_value_from_result(sql_result[10]), # created_at
self._get_value_from_result(sql_result[11]), # modified_at
id=self._get_value_from_result(sql_result[0]),
)
def get_scheduled_events(self) -> List[ScheduledEvent]:
scheduled_events = List(ScheduledEvent)
self._logger.trace(__name__, f"Send SQL command: {ScheduledEvent.get_select_all_string()}")
results = self._context.select(ScheduledEvent.get_select_all_string())
for result in results:
self._logger.trace(__name__, f"Get scheduled_event with id {result[0]}")
scheduled_events.append(self._scheduled_event_from_result(result))
return scheduled_events
def get_scheduled_event_by_id(self, id: int) -> ScheduledEvent:
self._logger.trace(__name__, f"Send SQL command: {ScheduledEvent.get_select_by_id_string(id)}")
result = self._context.select(ScheduledEvent.get_select_by_id_string(id))[0]
return self._scheduled_event_from_result(result)
def get_scheduled_events_by_server_id(self, server_id: int) -> List[ScheduledEvent]:
scheduled_events = List(ScheduledEvent)
self._logger.trace(
__name__,
f"Send SQL command: {ScheduledEvent.get_select_by_server_id_string(server_id)}",
)
results = self._context.select(ScheduledEvent.get_select_by_server_id_string(server_id))
for result in results:
self._logger.trace(__name__, f"Get scheduled_event with id {result[0]}")
scheduled_events.append(self._scheduled_event_from_result(result))
return scheduled_events
def add_scheduled_event(self, scheduled_event: ScheduledEvent):
self._logger.trace(__name__, f"Send SQL command: {scheduled_event.insert_string}")
self._context.cursor.execute(scheduled_event.insert_string)
def update_scheduled_event(self, scheduled_event: ScheduledEvent):
self._logger.trace(__name__, f"Send SQL command: {scheduled_event.udpate_string}")
self._context.cursor.execute(scheduled_event.udpate_string)
def delete_scheduled_event(self, scheduled_event: ScheduledEvent):
self._logger.trace(__name__, f"Send SQL command: {scheduled_event.delete_string}")
self._context.cursor.execute(scheduled_event.delete_string)

View File

@ -1,8 +1,10 @@
import functools
from abc import ABC, abstractmethod
from inspect import signature, Parameter
from typing import Callable
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.type import R, T
from cpl_query.extension import List
@ -52,3 +54,18 @@ class FilterABC(ABC):
return f(*args, **kwargs)
return decorator
def _get_attributes_from_dict(self, attrs: list[tuple[str, type]], values: dict):
for key_pair in attrs:
attr = key_pair[0]
attr_type = key_pair[1]
if attr in values:
self.__setattr__(f"_{attr}", attr_type(values[attr]))
@staticmethod
def _filter_by_attributes(attrs: list[dict], values: List[T]) -> List[R]:
for attr in attrs:
if attr["attr"] is None:
continue
values = values.where(attr["func"])
return values

View File

@ -20,6 +20,7 @@ from bot_data.model.auto_role_rule import AutoRoleRule
from bot_data.model.client import Client
from bot_data.model.known_user import KnownUser
from bot_data.model.level import Level
from bot_data.model.scheduled_event import ScheduledEvent
from bot_data.model.server import Server
from bot_data.model.server_config import ServerConfig
from bot_data.model.short_role_name import ShortRoleName
@ -213,6 +214,16 @@ class QueryABC(ObjectType):
access = True
break
elif type(element) == ScheduledEvent:
element: ScheduledEvent = element
for u in user.users:
u: User = u
guild = bot.get_guild(u.server.discord_id)
member = guild.get_member(u.discord_id)
if permissions.is_member_moderator(member) and u.server.id == element.server.id:
access = True
break
elif type(element) == dict and "key" in element and element["key"] in [e.value for e in FeatureFlagsEnum]:
for u in user.users:
u: User = u

View File

@ -0,0 +1,71 @@
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from bot_data.model.scheduled_event import ScheduledEvent
from bot_graphql.abc.filter_abc import FilterABC
class ScheduledEventFilter(FilterABC):
def __init__(self, bot: DiscordBotServiceABC, services: ServiceProviderABC):
FilterABC.__init__(self)
self._bot = bot
self._services = services
self._id = None
self._interval = None
self._name = None
self._description = None
self._channel_id = None
self._start_time = None
self._end_time = None
self._entity_type = None
self._location = None
self._server = None
def from_dict(self, values: dict):
self._get_attributes_from_dict(
[
("id", int),
("interval", str),
("name", str),
("description", str),
("channel_id", str),
("start_time", str),
("end_time", str),
("entity_type", str),
("location", str),
],
values,
)
if "server" in values:
from bot_graphql.filter.server_filter import ServerFilter
self._server: ServerFilter = self._services.get_service(ServerFilter)
self._server.from_dict(values["server"])
def filter(self, query: List[ScheduledEvent]) -> List[ScheduledEvent]:
if self._id is not None:
query = query.where(lambda x: x.id == self._id)
query = self._filter_by_attributes(
[
{"attr": self._id, "func": lambda x: x.id == self._id},
{"attr": self._interval, "func": lambda x: x.interval == self._interval},
{"attr": self._name, "func": lambda x: x.name == self._name},
{"attr": self._description, "func": lambda x: x.description == self._description},
{"attr": self._channel_id, "func": lambda x: x.channel_id == self._channel_id},
{"attr": self._start_time, "func": lambda x: x.start_time == self._start_time},
{"attr": self._end_time, "func": lambda x: x.end_time == self._end_time},
{"attr": self._entity_type, "func": lambda x: x.entity_type == self._entity_type},
{"attr": self._location, "func": lambda x: x.location == self._location},
],
query,
)
if self._server is not None:
servers = self._server.filter(query.select(lambda x: x.server)).select(lambda x: x.id)
query = query.where(lambda x: x.server.id in servers)
return query

View File

@ -1,3 +1,4 @@
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
@ -8,9 +9,10 @@ from bot_graphql.abc.filter_abc import FilterABC
class ShortRoleNameFilter(FilterABC):
def __init__(self, bot: DiscordBotServiceABC):
def __init__(self, bot: DiscordBotServiceABC, services: ServiceProviderABC):
FilterABC.__init__(self)
self._bot = bot
self._services = services
self._id = None
self._short_name = None

View File

@ -6,6 +6,7 @@ type Mutation {
userJoinedGameServer: UserJoinedGameServerMutation
achievement: AchievementMutation
shortRoleName: ShortRoleNameMutation
scheduledEvent: ScheduledEventMutation
technicianConfig: TechnicianConfigMutation
serverConfig: ServerConfigMutation
}

View File

@ -41,6 +41,9 @@ type Query {
shortRoleNames(filter: ShortRoleNameFilter, page: Page, sort: Sort): [ShortRoleName]
shortRoleNamePositions: [String]
scheduledEventCount: Int
scheduledEvents(filter: ScheduledEventFilter, page: Page, sort: Sort): [ScheduledEvent]
userWarningCount: Int
userWarnings(filter: UserWarningFilter, page: Page, sort: Sort): [UserWarning]

View File

@ -0,0 +1,68 @@
type ScheduledEvent implements TableWithHistoryQuery {
id: ID
interval: String
name: String
description: String
channelId: String
startTime: String
endTime: String
entityType: Int
location: String
server: Server
createdAt: String
modifiedAt: String
history: [ScheduledEventHistory]
}
type ScheduledEventHistory implements HistoryTableQuery {
id: ID
interval: String
name: String
description: String
channelId: String
startTime: String
endTime: String
entityType: Int
location: String
server: ID
deleted: Boolean
dateFrom: String
dateTo: String
}
input ScheduledEventFilter {
id: ID
interval: String
name: String
description: String
channelId: String
startTime: String
endTime: String
entityType: Int
location: String
server: ServerFilter
}
type ScheduledEventMutation {
createScheduledEvent(input: ScheduledEventInput!): ScheduledEvent
updateScheduledEvent(input: ScheduledEventInput!): ScheduledEvent
deleteScheduledEvent(id: ID): ScheduledEvent
}
input ScheduledEventInput {
id: ID
interval: String
name: String
description: String
channelId: String
startTime: String
endTime: String
entityType: Int
location: String
serverId: ID
}

View File

@ -35,6 +35,9 @@ type Server implements TableWithHistoryQuery {
shortRoleNameCount: Int
shortRoleNames(filter: ShortRoleNameFilter, page: Page, sort: Sort): [ShortRoleName]
scheduledEventCount: Int
scheduledEvents(filter: ScheduledEventFilter, page: Page, sort: Sort): [ScheduledEvent]
config: ServerConfig
hasFeatureFlag(flag: String): FeatureFlag

View File

@ -12,6 +12,7 @@ from bot_graphql.filter.auto_role_filter import AutoRoleFilter
from bot_graphql.filter.auto_role_rule_filter import AutoRoleRuleFilter
from bot_graphql.filter.client_filter import ClientFilter
from bot_graphql.filter.level_filter import LevelFilter
from bot_graphql.filter.scheduled_event_filter import ScheduledEventFilter
from bot_graphql.filter.server_filter import ServerFilter
from bot_graphql.filter.short_role_name_filter import ShortRoleNameFilter
from bot_graphql.filter.user_filter import UserFilter
@ -27,6 +28,7 @@ from bot_graphql.mutations.achievement_mutation import AchievementMutation
from bot_graphql.mutations.auto_role_mutation import AutoRoleMutation
from bot_graphql.mutations.auto_role_rule_mutation import AutoRoleRuleMutation
from bot_graphql.mutations.level_mutation import LevelMutation
from bot_graphql.mutations.scheduled_event_mutation import ScheduledEventMutation
from bot_graphql.mutations.server_config_mutation import ServerConfigMutation
from bot_graphql.mutations.short_role_name_mutation import ShortRoleNameMutation
from bot_graphql.mutations.technician_config_mutation import TechnicianConfigMutation
@ -54,6 +56,7 @@ from bot_graphql.queries.known_user_history_query import KnownUserHistoryQuery
from bot_graphql.queries.known_user_query import KnownUserQuery
from bot_graphql.queries.level_history_query import LevelHistoryQuery
from bot_graphql.queries.level_query import LevelQuery
from bot_graphql.queries.scheduled_event_query import ScheduledEventQuery
from bot_graphql.queries.server_config_query import ServerConfigQuery
from bot_graphql.queries.server_history_query import ServerHistoryQuery
from bot_graphql.queries.server_query import ServerQuery
@ -140,6 +143,7 @@ class GraphQLModule(ModuleABC):
services.add_transient(QueryABC, UserWarningHistoryQuery)
services.add_transient(QueryABC, UserWarningQuery)
services.add_transient(QueryABC, ServerStatisticQuery)
services.add_transient(QueryABC, ScheduledEventQuery)
services.add_transient(QueryABC, DiscordQuery)
services.add_transient(QueryABC, GuildQuery)
@ -161,6 +165,7 @@ class GraphQLModule(ModuleABC):
services.add_transient(FilterABC, UserJoinedGameServerFilter)
services.add_transient(FilterABC, ShortRoleNameFilter)
services.add_transient(FilterABC, UserWarningFilter)
services.add_transient(FilterABC, ScheduledEventFilter)
# mutations
services.add_transient(QueryABC, AutoRoleMutation)
@ -172,3 +177,4 @@ class GraphQLModule(ModuleABC):
services.add_transient(QueryABC, UserJoinedGameServerMutation)
services.add_transient(QueryABC, TechnicianConfigMutation)
services.add_transient(QueryABC, ServerConfigMutation)
services.add_transient(QueryABC, ScheduledEventMutation)

View File

@ -4,6 +4,7 @@ from bot_graphql.mutations.achievement_mutation import AchievementMutation
from bot_graphql.mutations.auto_role_mutation import AutoRoleMutation
from bot_graphql.mutations.auto_role_rule_mutation import AutoRoleRuleMutation
from bot_graphql.mutations.level_mutation import LevelMutation
from bot_graphql.mutations.scheduled_event_mutation import ScheduledEventMutation
from bot_graphql.mutations.server_config_mutation import ServerConfigMutation
from bot_graphql.mutations.short_role_name_mutation import ShortRoleNameMutation
from bot_graphql.mutations.technician_config_mutation import TechnicianConfigMutation
@ -23,6 +24,7 @@ class Mutation(MutationType):
achievement_mutation: AchievementMutation,
user_joined_game_server: UserJoinedGameServerMutation,
technician_config: TechnicianConfigMutation,
scheduled_event: ScheduledEventMutation,
server_config: ServerConfigMutation,
short_role_name_mutation: ShortRoleNameMutation,
):
@ -36,4 +38,5 @@ class Mutation(MutationType):
self.set_field("userJoinedGameServer", lambda *_: user_joined_game_server)
self.set_field("shortRoleName", lambda *_: short_role_name_mutation)
self.set_field("technicianConfig", lambda *_: technician_config)
self.set_field("scheduledEvent", lambda *_: scheduled_event)
self.set_field("serverConfig", lambda *_: server_config)

View File

@ -0,0 +1,107 @@
from datetime import datetime
from cpl_core.database.context import DatabaseContextABC
from cpl_discord.service import DiscordBotServiceABC
from discord import EntityType
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.model.scheduled_event import ScheduledEvent
from bot_data.model.scheduled_event_interval_enum import ScheduledEventIntervalEnum
from bot_data.model.user_role_enum import UserRoleEnum
from bot_graphql.abc.query_abc import QueryABC
from modules.permission.service.permission_service import PermissionService
class ScheduledEventMutation(QueryABC):
def __init__(
self,
servers: ServerRepositoryABC,
scheduled_events: ScheduledEventRepositoryABC,
bot: DiscordBotServiceABC,
db: DatabaseContextABC,
permissions: PermissionService,
):
QueryABC.__init__(self, "ScheduledEventMutation")
self._servers = servers
self._scheduled_events = scheduled_events
self._bot = bot
self._db = db
self._permissions = permissions
self.set_field("createScheduledEvent", self.resolve_create_scheduled_event)
self.set_field("updateScheduledEvent", self.resolve_update_scheduled_event)
self.set_field("deleteScheduledEvent", self.resolve_delete_scheduled_event)
def resolve_create_scheduled_event(self, *_, input: dict):
server = self._servers.get_server_by_id(input["serverId"])
self._can_user_mutate_data(server, UserRoleEnum.moderator)
scheduled_event = ScheduledEvent(
ScheduledEventIntervalEnum(input["interval"]),
input["name"],
input["description"] if "description" in input else None,
input["channelId"] if "channelId" in input else None,
datetime.strptime(input["startTime"], "%Y-%m-%dT%H:%M:%S.%fZ"),
datetime.strptime(input["endTime"], "%Y-%m-%dT%H:%M:%S.%fZ") if "endTime" in input else None,
input["entityType"],
input["location"] if "location" in input else None,
server,
)
self._scheduled_events.add_scheduled_event(scheduled_event)
self._db.save_changes()
def get_new_scheduled_event(srn: ScheduledEvent):
return (
srn.interval == scheduled_event.interval
and srn.name == scheduled_event.name
and srn.description == scheduled_event.description
)
return (
self._scheduled_events.get_scheduled_events_by_server_id(scheduled_event.server.id)
.where(get_new_scheduled_event)
.last()
)
def resolve_update_scheduled_event(self, *_, input: dict):
scheduled_event = self._scheduled_events.get_scheduled_event_by_id(input["id"])
self._can_user_mutate_data(scheduled_event.server, UserRoleEnum.moderator)
scheduled_event.interval = (
ScheduledEventIntervalEnum(input["interval"]) if "interval" in input else scheduled_event.interval
)
scheduled_event.name = input["name"] if "name" in input else scheduled_event.name
scheduled_event.description = input["description"] if "description" in input else scheduled_event.description
scheduled_event.channel_id = input["channelId"] if "channelId" in input else scheduled_event.channel_id
scheduled_event.start_time = (
datetime.strptime(input["startTime"], "%Y-%m-%dT%H:%M:%S.%fZ")
if "startTime" in input
else scheduled_event.start_time
)
scheduled_event.end_time = (
datetime.strptime(input["endTime"], "%Y-%m-%dT%H:%M:%S.%fZ")
if "endTime" in input
else scheduled_event.end_time
)
scheduled_event.entity_type = (
EntityType(int(input["entityType"])) if "entityType" in input else scheduled_event.entity_type
)
scheduled_event.location = input["location"] if "location" in input else scheduled_event.location
self._scheduled_events.update_scheduled_event(scheduled_event)
self._db.save_changes()
scheduled_event = self._scheduled_events.get_scheduled_event_by_id(input["id"])
return scheduled_event
def resolve_delete_scheduled_event(self, *_, id: int):
scheduled_event = self._scheduled_events.get_scheduled_event_by_id(id)
self._can_user_mutate_data(scheduled_event.server, UserRoleEnum.admin)
self._scheduled_events.delete_scheduled_event(scheduled_event)
self._db.save_changes()
return scheduled_event

View File

@ -0,0 +1,24 @@
from cpl_core.database.context import DatabaseContextABC
from bot_data.model.scheduled_event_history import ScheduledEventHistory
from bot_graphql.abc.data_query_with_history_abc import DataQueryWithHistoryABC
class ScheduledEventHistoryQuery(DataQueryWithHistoryABC):
def __init__(
self,
db: DatabaseContextABC,
):
DataQueryWithHistoryABC.__init__(self, "ScheduledEvent", "ScheduledEventsHistory", ScheduledEventHistory, db)
self.set_field("id", lambda x, *_: x.id)
self.set_field("id", lambda x, *_: x.id)
self.set_field("interval", lambda x, *_: x.interval.value)
self.set_field("name", lambda x, *_: x.name)
self.set_field("description", lambda x, *_: x.description)
self.set_field("channel_id", lambda x, *_: x.channel_id)
self.set_field("start_time", lambda x, *_: x.start_time)
self.set_field("end_time", lambda x, *_: x.end_time)
self.set_field("entity_type", lambda x, *_: x.entity_type)
self.set_field("location", lambda x, *_: x.location)
self.set_field("server", lambda x, *_: x.server)

View File

@ -0,0 +1,23 @@
from cpl_core.database.context import DatabaseContextABC
from bot_data.model.scheduled_event_history import ScheduledEventHistory
from bot_graphql.abc.data_query_with_history_abc import DataQueryWithHistoryABC
class ScheduledEventQuery(DataQueryWithHistoryABC):
def __init__(
self,
db: DatabaseContextABC,
):
DataQueryWithHistoryABC.__init__(self, "ScheduledEvent", "ScheduledEventsHistory", ScheduledEventHistory, db)
self.set_field("id", lambda x, *_: x.id)
self.set_field("interval", lambda x, *_: x.interval.value)
self.set_field("name", lambda x, *_: x.name)
self.set_field("description", lambda x, *_: x.description)
self.set_field("channelId", lambda x, *_: x.channel_id)
self.set_field("startTime", lambda x, *_: x.start_time)
self.set_field("endTime", lambda x, *_: x.end_time)
self.set_field("entityType", lambda x, *_: x.entity_type.value)
self.set_field("location", lambda x, *_: x.location)
self.set_field("server", lambda x, *_: x.server)

View File

@ -9,6 +9,7 @@ from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC
from bot_data.abc.client_repository_abc import ClientRepositoryABC
from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC
from bot_data.abc.level_repository_abc import LevelRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.abc.server_config_repository_abc import ServerConfigRepositoryABC
from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC
from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC
@ -24,6 +25,7 @@ from bot_graphql.filter.achievement_filter import AchievementFilter
from bot_graphql.filter.auto_role_filter import AutoRoleFilter
from bot_graphql.filter.client_filter import ClientFilter
from bot_graphql.filter.level_filter import LevelFilter
from bot_graphql.filter.scheduled_event_filter import ScheduledEventFilter
from bot_graphql.filter.short_role_name_filter import ShortRoleNameFilter
from bot_graphql.filter.user_filter import UserFilter
from bot_graphql.model.server_statistics import ServerStatistics
@ -44,6 +46,7 @@ class ServerQuery(DataQueryWithHistoryABC):
ujvs: UserJoinedVoiceChannelRepositoryABC,
achievements: AchievementRepositoryABC,
short_role_names: ShortRoleNameRepositoryABC,
scheduled_events: ScheduledEventRepositoryABC,
server_configs: ServerConfigRepositoryABC,
):
DataQueryWithHistoryABC.__init__(self, "Server", "ServersHistory", ServerHistory, db)
@ -100,6 +103,11 @@ class ServerQuery(DataQueryWithHistoryABC):
lambda server, *_: short_role_names.get_short_role_names_by_server_id(server.id),
ShortRoleNameFilter,
)
self.add_collection(
"scheduledEvent",
lambda server, *_: scheduled_events.get_scheduled_events_by_server_id(server.id),
ScheduledEventFilter,
)
self.set_field(
"config",
lambda server, *_: server_configs.get_server_config_by_server(server.id),

View File

@ -8,6 +8,7 @@ from bot_data.abc.client_repository_abc import ClientRepositoryABC
from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC
from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC
from bot_data.abc.level_repository_abc import LevelRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC
from bot_data.abc.technician_config_repository_abc import TechnicianConfigRepositoryABC
@ -27,6 +28,7 @@ from bot_graphql.filter.auto_role_filter import AutoRoleFilter
from bot_graphql.filter.auto_role_rule_filter import AutoRoleRuleFilter
from bot_graphql.filter.client_filter import ClientFilter
from bot_graphql.filter.level_filter import LevelFilter
from bot_graphql.filter.scheduled_event_filter import ScheduledEventFilter
from bot_graphql.filter.server_filter import ServerFilter
from bot_graphql.filter.short_role_name_filter import ShortRoleNameFilter
from bot_graphql.filter.user_filter import UserFilter
@ -59,6 +61,7 @@ class Query(QueryABC):
user_warnings: UserWarningsRepositoryABC,
achievement_service: AchievementService,
technician_config: TechnicianConfigRepositoryABC,
scheduled_events: ScheduledEventRepositoryABC,
):
QueryABC.__init__(self, "Query")
@ -95,6 +98,11 @@ class Query(QueryABC):
lambda *_: short_role_names.get_short_role_names(),
ShortRoleNameFilter,
)
self.add_collection(
"scheduledEvent",
lambda server, *_: scheduled_events.get_scheduled_events_by_server_id(server.id),
ScheduledEventFilter,
)
self.add_collection(
"userWarning",
lambda *_: user_warnings.get_user_warnings(),

View File

@ -16,10 +16,10 @@
"LicenseName": "MIT",
"LicenseDescription": "MIT, see LICENSE for more details.",
"Dependencies": [
"cpl-core>=1.2.1"
"cpl-core>=1.2.dev410"
],
"DevDependencies": [
"cpl-cli>=1.2.1"
"cpl-cli>=1.2.dev410"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {},

View File

@ -7,7 +7,7 @@ from cpl_discord.service.discord_collection_abc import DiscordCollectionABC
from bot_core.abc.module_abc import ModuleABC
from bot_core.abc.task_abc import TaskABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from modules.base.birthday_watcher import BirthdayWatcher
from modules.base.tasks.birthday_watcher import BirthdayWatcher
from modules.base.command.afk_command import AFKCommand
from modules.base.command.game_server_group import GameServerGroup
from modules.base.command.help_command import HelpCommand
@ -45,6 +45,7 @@ from modules.base.events.base_on_voice_state_update_event_scheduled_event_bonus
from modules.base.forms.bug_report_form import BugReportForm
from modules.base.forms.complaint_form import ComplaintForm
from modules.base.helper.base_reaction_handler import BaseReactionHandler
from modules.base.tasks.scheduled_events_watcher import ScheduledEventsWatcher
from modules.base.service.event_service import EventService
from modules.base.service.user_warnings_service import UserWarningsService
@ -61,6 +62,7 @@ class BaseModule(ModuleABC):
services.add_singleton(EventService)
services.add_transient(UserWarningsService)
services.add_singleton(TaskABC, BirthdayWatcher)
services.add_singleton(TaskABC, ScheduledEventsWatcher)
# forms
services.add_transient(BugReportForm)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "modules.base.tasks"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.2"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="2")

View File

@ -0,0 +1,126 @@
import calendar
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from cpl_core.configuration import ConfigurationABC
from cpl_core.database.context import DatabaseContextABC
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from cpl_translation import TranslatePipe
from discord import Guild, PrivacyLevel
from discord.ext import tasks
from discord.scheduled_event import ScheduledEvent as DiscordEvent
from bot_core.abc.task_abc import TaskABC
from bot_core.logging.task_logger import TaskLogger
from bot_core.service.message_service import MessageService
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.model.scheduled_event import ScheduledEvent
from bot_data.model.scheduled_event_interval_enum import ScheduledEventIntervalEnum
class ScheduledEventsWatcher(TaskABC):
def __init__(
self,
config: ConfigurationABC,
logger: TaskLogger,
bot: DiscordBotServiceABC,
db: DatabaseContextABC,
servers: ServerRepositoryABC,
events: ScheduledEventRepositoryABC,
message_service: MessageService,
t: TranslatePipe,
):
TaskABC.__init__(self)
self._config = config
self._logger = logger
self._bot = bot
self._db = db
self._servers = servers
self._events = events
self._message_service = message_service
self._t = t
if not self._is_maintenance():
self.watch.start()
def _append_interval(self, interval: ScheduledEventIntervalEnum, ts: datetime) -> datetime:
now = datetime.now()
if ts >= now:
return ts
if interval == ScheduledEventIntervalEnum.daily:
ts = ts + timedelta(days=1)
elif interval == ScheduledEventIntervalEnum.weekly:
ts = ts + timedelta(weeks=1)
elif interval == ScheduledEventIntervalEnum.monthly:
days_in_month = calendar.monthrange(ts.year, ts.month + 1)[1]
ts = ts + timedelta(days=days_in_month)
elif interval == ScheduledEventIntervalEnum.yearly:
ts = ts + timedelta(days=365)
return ts
@tasks.loop(hours=24)
async def watch(self):
self._logger.info(__name__, "Watching scheduled events")
try:
for guild in self._bot.guilds:
guild: Guild = guild
server = self._servers.get_server_by_discord_id(guild.id)
scheduled_events_from_guild = self._events.get_scheduled_events_by_server_id(server.id)
for scheduled_event in scheduled_events_from_guild:
scheduled_event: ScheduledEvent = scheduled_event
from_guild = List(DiscordEvent, guild.scheduled_events).where(
lambda x: x.name == scheduled_event.name
and x.description == scheduled_event.description
and x.entity_type == scheduled_event.entity_type
)
if from_guild.count() != 0:
continue
kwargs = {"name": scheduled_event.name, "description": scheduled_event.description}
if scheduled_event.channel_id is not None:
kwargs["channel"] = guild.get_channel(scheduled_event.channel_id)
if scheduled_event.start_time is not None:
scheduled_event.start_time = self._append_interval(
scheduled_event.interval, scheduled_event.start_time
)
start_time = scheduled_event.start_time.replace(tzinfo=ZoneInfo("Europe/Berlin"))
kwargs["start_time"] = start_time
if scheduled_event.end_time is not None:
scheduled_event.end_time = self._append_interval(
scheduled_event.interval, scheduled_event.end_time
)
end_time = scheduled_event.end_time.replace(tzinfo=ZoneInfo("Europe/Berlin"))
kwargs["end_time"] = end_time
kwargs["entity_type"] = scheduled_event.entity_type
if scheduled_event.location is not None:
kwargs["location"] = scheduled_event.location
kwargs["privacy_level"] = PrivacyLevel.guild_only
try:
self._logger.debug(__name__, f"Try to create scheduled event for guild {guild.name}")
await guild.create_scheduled_event(**kwargs)
self._events.update_scheduled_event(scheduled_event)
self._db.save_changes()
except Exception as e:
self._logger.error(__name__, f"Watching scheduled events failed", e)
except Exception as e:
self._logger.error(__name__, f"Watching scheduled events failed", e)
@watch.before_loop
async def wait(self):
await self._wait_until_ready()

View File

@ -16,10 +16,10 @@
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core>=1.2.1"
"cpl-core>=1.2.dev410"
],
"DevDependencies": [
"cpl-cli>=1.2.1"
"cpl-cli>=1.2.dev410"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {

View File

@ -16,10 +16,10 @@
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core>=1.2.1"
"cpl-core>=1.2.dev410"
],
"DevDependencies": [
"cpl-cli>=1.2.1"
"cpl-cli>=1.2.dev410"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {

View File

@ -16,10 +16,10 @@
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core>=1.2.1"
"cpl-core>=1.2.dev410"
],
"DevDependencies": [
"cpl-cli>=1.2.1"
"cpl-cli>=1.2.dev410"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {

View File

@ -2,9 +2,9 @@
"ProjectSettings": {
"Name": "migration-to-sql",
"Version": {
"Major": "0",
"Minor": "0",
"Micro": "0"
"Major": "1",
"Minor": "2",
"Micro": "2"
},
"Author": "",
"AuthorEmail": "",
@ -16,10 +16,10 @@
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core>=2023.10.0"
"cpl-core>=1.2.dev410"
],
"DevDependencies": [
"cpl-cli>=2023.4.0.post3"
"cpl-cli>=1.2.dev410"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {

View File

@ -1,56 +1,56 @@
{
"name": "web",
"version": "1.2.2",
"scripts": {
"ng": "ng",
"update-version": "ts-node update-version.ts",
"prestart": "npm run update-version",
"start": "ng serve",
"prebuild": "npm run update-version",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"gv": "echo $npm_package_version",
"predocker-build": "npm run update-version",
"docker-build": "export VERSION=$npm_package_version; ng build; docker build -t sh-edraft.de/sdb-web:$VERSION .",
"docker-build-dev": "export VERSION=$npm_package_version; ng build --configuration development; docker build -t sh-edraft.de/sdb-web:$VERSION .",
"docker-build-stage": "export VERSION=$npm_package_version; ng build --configuration staging; docker build -t sh-edraft.de/sdb-web:$VERSION ."
},
"private": true,
"dependencies": {
"@angular/animations": "^15.1.4",
"@angular/common": "^15.1.4",
"@angular/compiler": "^15.1.4",
"@angular/core": "^15.1.4",
"@angular/forms": "^15.1.4",
"@angular/platform-browser": "^15.1.4",
"@angular/platform-browser-dynamic": "^15.1.4",
"@angular/router": "^15.1.4",
"@auth0/angular-jwt": "^5.1.0",
"@microsoft/signalr": "^6.0.9",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@types/socket.io-client": "^3.0.0",
"moment": "^2.29.4",
"primeicons": "^6.0.1",
"primeng": "^15.2.0",
"rxjs": "~7.5.0",
"socket.io-client": "^4.5.3",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.1.5",
"@angular/cli": "~15.1.5",
"@angular/compiler-cli": "^15.1.4",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.9",
"jasmine-core": "~4.1.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"tslib": "^2.4.1",
"typescript": "~4.9.5"
}
}
"name": "web",
"version": "1.2.dev410",
"scripts": {
"ng": "ng",
"update-version": "ts-node update-version.ts",
"prestart": "npm run update-version",
"start": "ng serve",
"prebuild": "npm run update-version",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"gv": "echo $npm_package_version",
"predocker-build": "npm run update-version",
"docker-build": "export VERSION=$npm_package_version; ng build; docker build -t sh-edraft.de/sdb-web:$VERSION .",
"docker-build-dev": "export VERSION=$npm_package_version; ng build --configuration development; docker build -t sh-edraft.de/sdb-web:$VERSION .",
"docker-build-stage": "export VERSION=$npm_package_version; ng build --configuration staging; docker build -t sh-edraft.de/sdb-web:$VERSION ."
},
"private": true,
"dependencies": {
"@angular/animations": "^15.1.4",
"@angular/common": "^15.1.4",
"@angular/compiler": "^15.1.4",
"@angular/core": "^15.1.4",
"@angular/forms": "^15.1.4",
"@angular/platform-browser": "^15.1.4",
"@angular/platform-browser-dynamic": "^15.1.4",
"@angular/router": "^15.1.4",
"@auth0/angular-jwt": "^5.1.0",
"@microsoft/signalr": "^6.0.9",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@types/socket.io-client": "^3.0.0",
"moment": "^2.29.4",
"primeicons": "^6.0.1",
"primeng": "^15.2.0",
"rxjs": "~7.5.0",
"socket.io-client": "^4.5.3",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.1.5",
"@angular/cli": "~15.1.5",
"@angular/compiler-cli": "^15.1.4",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.9",
"jasmine-core": "~4.1.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"tslib": "^2.4.1",
"typescript": "~4.9.5"
}
}

View File

@ -1,4 +1,4 @@
export interface Discord {
export interface Discord {
guilds?: Guild[];
users?: DiscordUser[];
}
@ -21,7 +21,8 @@ export interface Channel {
export enum ChannelType {
category = "CategoryChannel",
text = "TextChannel",
voice = "VoiceChannel"
voice = "VoiceChannel",
stage = "StageChannel"
}
export interface Role {

View File

@ -0,0 +1,43 @@
import { DataWithHistory } from "./data.model";
import { Server, ServerFilter } from "./server.model";
export enum EventType {
stageInstance = 1,
voice = 2,
external = 3,
}
export interface ScheduledEvent extends DataWithHistory {
id?: number;
interval?: ScheduledEventInterval;
name?: string;
description?: string;
channelId?: string;
startTime?: Date;
endTime?: Date;
entityType?: EventType;
location?: string;
server?: Server;
}
export interface ScheduledEventFilter {
id?: number;
interval?: string;
name?: string;
description?: string;
channelId?: string;
startTime?: string;
endTime?: string;
entityType?: number;
location?: string;
server?: ServerFilter;
}
// export enum ScheduledEventInterval {
// daily = "daily",
// weekly = "weekly",
// monthly = "monthly",
// yearly = "yearly"
// }
export type ScheduledEventInterval = "daily" | "weekly" | "monthly" | "month"

View File

@ -173,6 +173,78 @@ export class Mutations {
}
`;
static createScheduledEvent = `
mutation createScheduledEvent($interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) {
scheduledEvent {
createScheduledEvent(input: {
interval: $interval,
name: $name,
description: $description,
channelId: $channelId,
startTime: $startTime,
endTime: $endTime,
entityType: $entityType,
location: $location,
serverId: $serverId
}) {
interval
name
description
channelId
startTime
endTime
entityType
location
server {
id
}
}
}
}
`;
static updateScheduledEvent = `
mutation updateScheduledEvent($id: ID, $interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) {
scheduledEvent {
updateScheduledEvent(input: {
id: $id,
interval: $interval,
name: $name,
description: $description,
channelId: $channelId,
startTime: $startTime,
endTime: $endTime,
entityType: $entityType,
location: $location,
serverId: $serverId}
) {
interval
name
description
channelId
startTime
endTime
entityType
location
server {
id
}
}
}
}
`;
static deleteScheduledEvent = `
mutation deleteScheduledEvent($id: ID) {
scheduledEvent {
deleteScheduledEvent(id: $id) {
id
name
}
}
}
`;
static createShortRoleName = `
mutation createShortRoleName($shortName: String, $roleId: String, $position: String, $serverId: ID) {
shortRoleName {
@ -333,7 +405,6 @@ export class Mutations {
`;
static createUserWarning = `
mutation createUserWarning($name: String, $description: String, $attribute: String, $operator: String, $value: String, $serverId: ID) {
userWarning {

View File

@ -229,6 +229,58 @@ export class Queries {
}
`;
static scheduledEventQuery = `
query ScheduledEventList($serverId: ID, $filter: ScheduledEventFilter, $page: Page, $sort: Sort) {
servers(filter: {id: $serverId}) {
scheduledEventCount
scheduledEvents(filter: $filter, page: $page, sort: $sort) {
id
interval
name
description
channelId
startTime
endTime
entityType
location
server {
id
name
}
createdAt
modifiedAt
}
}
}
`;
static scheduledEventWithHistoryQuery = `
query ScheduledEventHistory($serverId: ID, $id: ID) {
servers(filter: {id: $serverId}) {
scheduledEventCount
scheduledEvents(filter: {id: $id}) {
id
history {
id
interval
name
description
channelId
startTime
endTime
entityType
location
server
deleted
dateFrom
dateTo
}
}
}
}
`;
static shortRoleNamePositionsQuery = `
query {

View File

@ -9,6 +9,7 @@ import { ServerConfig } from "../config/server-config.model";
import { ShortRoleName } from "../data/short_role_name.model";
import { FeatureFlag } from "../config/feature-flags.model";
import { UserWarning } from "../data/user_warning.model";
import { ScheduledEvent } from "../data/scheduled_events.model";
export interface Query {
serverCount: number;
@ -57,6 +58,11 @@ export interface AchievementListQuery {
achievements: Achievement[];
}
export interface ScheduledEventListQuery {
scheduledEventCount: number;
scheduledEvents: ScheduledEvent[];
}
export interface AutoRoleQuery {
autoRoleCount: number;
autoRoles: AutoRole[];

View File

@ -7,6 +7,7 @@ import { TechnicianConfig } from "../config/technician-config.model";
import { ServerConfig } from "../config/server-config.model";
import { ShortRoleName } from "../data/short_role_name.model";
import { UserWarning } from "../data/user_warning.model";
import { ScheduledEvent } from "../data/scheduled_events.model";
export interface GraphQLResult {
data: {
@ -71,6 +72,14 @@ export interface AchievementMutationResult {
};
}
export interface ScheduledEventMutationResult {
scheduledEvent: {
createScheduledEvent?: ScheduledEvent
updateScheduledEvent?: ScheduledEvent
deleteScheduledEvent?: ScheduledEvent
};
}
export interface ShortRoleNameMutationResult {
shortRoleName: {
createShortRoleName?: ShortRoleName

View File

@ -1,4 +1,4 @@
import { CommonModule } from "@angular/common";
import { CommonModule, DatePipe } from "@angular/common";
import { HttpClientModule } from "@angular/common/http";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
@ -13,7 +13,7 @@ import { InputTextModule } from "primeng/inputtext";
import { MenuModule } from "primeng/menu";
import { PasswordModule } from "primeng/password";
import { ProgressSpinnerModule } from "primeng/progressspinner";
import { SortableColumn, TableModule } from "primeng/table";
import { TableModule } from "primeng/table";
import { ToastModule } from "primeng/toast";
import { AuthRolePipe } from "./pipes/auth-role.pipe";
import { IpAddressPipe } from "./pipes/ip-address.pipe";
@ -24,18 +24,22 @@ import { InputNumberModule } from "primeng/inputnumber";
import { ImageModule } from "primeng/image";
import { SidebarModule } from "primeng/sidebar";
import { HistoryBtnComponent } from "./components/history-btn/history-btn.component";
import { DataViewModule, DataViewLayoutOptions } from "primeng/dataview";
import { DataViewLayoutOptions, DataViewModule } from "primeng/dataview";
import { ConfigListComponent } from "./components/config-list/config-list.component";
import { MultiSelectModule } from "primeng/multiselect";
import { HideableColumnComponent } from './components/hideable-column/hideable-column.component';
import { HideableHeaderComponent } from './components/hideable-header/hideable-header.component';
import { MultiSelectColumnsComponent } from './base/multi-select-columns/multi-select-columns.component';
import { FeatureFlagListComponent } from './components/feature-flag-list/feature-flag-list.component';
import { HideableColumnComponent } from "./components/hideable-column/hideable-column.component";
import { HideableHeaderComponent } from "./components/hideable-header/hideable-header.component";
import { MultiSelectColumnsComponent } from "./base/multi-select-columns/multi-select-columns.component";
import { FeatureFlagListComponent } from "./components/feature-flag-list/feature-flag-list.component";
import { InputSwitchModule } from "primeng/inputswitch";
import { CalendarModule } from "primeng/calendar";
import { DataImportAndExportComponent } from './components/data-import-and-export/data-import-and-export.component';
import { DataImportAndExportComponent } from "./components/data-import-and-export/data-import-and-export.component";
import { FileUploadModule } from "primeng/fileupload";
import { SelectButtonModule } from "primeng/selectbutton";
import { TabViewModule } from "primeng/tabview";
import { RadioButtonModule } from "primeng/radiobutton";
import { InputTextareaModule } from "primeng/inputtextarea";
import { InputMaskModule } from "primeng/inputmask";
const PrimeNGModules = [
@ -66,7 +70,11 @@ const PrimeNGModules = [
CalendarModule,
FileUploadModule,
SelectButtonModule,
]
TabViewModule,
RadioButtonModule,
InputTextareaModule,
InputMaskModule
];
@NgModule({
declarations: [
@ -79,7 +87,7 @@ const PrimeNGModules = [
HideableHeaderComponent,
MultiSelectColumnsComponent,
FeatureFlagListComponent,
DataImportAndExportComponent,
DataImportAndExportComponent
],
imports: [
CommonModule,
@ -98,6 +106,9 @@ const PrimeNGModules = [
MultiSelectColumnsComponent,
FeatureFlagListComponent,
DataImportAndExportComponent
],
providers: [
DatePipe
]
})
export class SharedModule {

View File

@ -0,0 +1,92 @@
<div class="edit-dialog">
<p-dialog [contentStyle]="{'overflow':'visible'}" [(visible)]="visible" [header]="header" [modal]="true"
[draggable]="false" [resizable]="false"
[style]="{ width: '500px', height: '575px' }">
<form [formGroup]="inputForm">
<p-tabView [(activeIndex)]="activeIndex">
<p-tabPanel header="{{'view.server.scheduled_events.edit_dialog.location.tab_name' | translate}}">
<div class="form">
<div style="display: flex; flex-direction: column; gap: 5px;">
{{'view.server.scheduled_events.edit_dialog.location.interval' | translate}}:
<p-dropdown [options]="interval"
formControlName="interval"
placeholder="{{'view.server.scheduled_events.edit_dialog.location.interval' | translate}}"></p-dropdown>
</div>
<div class="type">
<div *ngFor="let enum of EventType | keyvalue" class="field-checkbox">
<p-radioButton [inputId]="enum.key" [value]="enum.value" formControlName="entityType"></p-radioButton>
<label [for]="enum.key"
class="ml-2">{{'view.server.scheduled_events.edit_dialog.location.' + enum.key | translate}}</label>
</div>
</div>
<div class="input">
<p-dropdown *ngIf="inputForm.controls.entityType.value == 1"
[options]="stageChannels"
optionLabel="name"
optionValue="id"
formControlName="channelId"
placeholder="{{'view.server.scheduled_events.edit_dialog.location.stage_input' | translate}}"></p-dropdown>
<p-dropdown *ngIf="inputForm.controls.entityType.value == 2"
[options]="voiceChannels"
optionLabel="name"
optionValue="id"
formControlName="channelId"
placeholder="{{'view.server.scheduled_events.edit_dialog.location.voice_input' | translate}}"></p-dropdown>
<input *ngIf="inputForm.controls.entityType.value == 3" pInputText class="table-edit-input"
formControlName="location"
placeholder="{{'view.server.scheduled_events.edit_dialog.location.somewhere_else' | translate}}">
</div>
</div>
</p-tabPanel>
<p-tabPanel header="{{'view.server.scheduled_events.edit_dialog.event_info.tab_name' | translate}}">
<div class="form">
<div style="display: flex; flex-direction: column; gap: 5px;">
{{'view.server.scheduled_events.edit_dialog.event_info.event_topic' | translate}}:
<input pInputText class="table-edit-input"
formControlName="name"
placeholder="{{'view.server.scheduled_events.edit_dialog.event_info.event_topic_input' | translate}}">
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
{{'view.server.scheduled_events.edit_dialog.event_info.start_date_time' | translate}}:
<p-calendar formControlName="startTime" dateFormat="dd.mm.yy" [showIcon]="true"
[showTime]="true" [stepMinute]="15"></p-calendar>
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
{{'view.server.scheduled_events.edit_dialog.event_info.end_date_time' | translate}}:
<p-calendar formControlName="endTime" dateFormat="dd.mm.yy" [showIcon]="true"
[showTime]="true"></p-calendar>
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
{{'view.server.scheduled_events.edit_dialog.event_info.description' | translate}}:
<textarea rows="5" cols="30" pInputTextarea [autoResize]="true"
class="table-edit-input"
formControlName="description"
placeholder="{{'view.server.scheduled_events.edit_dialog.event_info.description_input' | translate}}"></textarea>
</div>
</div>
</p-tabPanel>
</p-tabView>
</form>
<ng-template pTemplate="footer">
<div style="display: flex;">
<div class="btn-wrapper" style="flex: 1;">
<button pButton label="{{'common.back' | translate}}" class="text-btn"
(click)="back()" [disabled]="activeIndex == 0"></button>
</div>
<div class="btn-wrapper">
<button pButton label="{{'common.abort' | translate}}" class="btn danger-btn"
(click)="event = undefined"></button>
<button *ngIf="activeIndex < 1" pButton label="{{'common.continue' | translate}}" class="btn"
(click)="next()" [disabled]="!inputForm"></button>
<button *ngIf="activeIndex == 1" pButton label="{{'common.save' | translate}}" class="btn"
(click)="saveEvent()" [disabled]="!inputForm.valid"></button>
</div>
</div>
</ng-template>
</p-dialog>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditScheduledEventDialogComponent } from './edit-scheduled-event-dialog.component';
describe('EditScheduledEventDialogComponent', () => {
let component: EditScheduledEventDialogComponent;
let fixture: ComponentFixture<EditScheduledEventDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditScheduledEventDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(EditScheduledEventDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,200 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from "@angular/core";
import {
EventType,
ScheduledEvent,
ScheduledEventInterval
} from "../../../../../../models/data/scheduled_events.model";
import { TranslateService } from "@ngx-translate/core";
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { SingleDiscordQuery } from "../../../../../../models/graphql/query.model";
import { Queries } from "../../../../../../models/graphql/queries.model";
import { Channel, ChannelType, Guild } from "../../../../../../models/data/discord.model";
import { DataService } from "../../../../../../services/data/data.service";
import { SpinnerService } from "../../../../../../services/spinner/spinner.service";
import { ActivatedRoute } from "@angular/router";
import { Server } from "../../../../../../models/data/server.model";
import { DatePipe } from "@angular/common";
@Component({
selector: "app-edit-scheduled-event-dialog",
templateUrl: "./edit-scheduled-event-dialog.component.html",
styleUrls: ["./edit-scheduled-event-dialog.component.scss"]
})
export class EditScheduledEventDialogComponent implements OnInit, OnChanges {
public _event?: ScheduledEvent;
@Input()
set event(event: ScheduledEvent | undefined) {
this._event = event;
this.eventChange.emit(event);
}
get event(): ScheduledEvent | undefined {
return this._event;
}
@Output() eventChange: EventEmitter<ScheduledEvent> = new EventEmitter<ScheduledEvent>();
get header() {
if (this.event && this.event.createdAt === "" && this.event.modifiedAt === "") {
return this.translate.instant("view.server.scheduled_events.edit_dialog.add_header");
} else {
return this.translate.instant("view.server.scheduled_events.edit_dialog.edit_header");
}
}
get visible() {
return this.event != undefined;
}
set visible(val: boolean) {
if (!val) {
this.event = undefined;
this.inputForm.reset();
this.activeIndex = 0;
}
}
public activeIndex: number = 0;
public inputForm!: FormGroup<{
entityType: FormControl<number | undefined | null>;
name: FormControl<string | undefined | null>;
description: FormControl<string | undefined | null>;
interval: FormControl<ScheduledEventInterval | undefined | null>;
location: FormControl<string | undefined | null>;
startTime: FormControl<Date | undefined | null>;
id: FormControl<number | undefined | null>;
endTime: FormControl<Date | undefined | null>;
channelId: FormControl<string | undefined | null>
}>;
server: Server = {};
public voiceChannels: Channel[] = [];
public stageChannels: Channel[] = [];
public guild: Guild = { channels: [], emojis: [], roles: [] };
public times: string[] = [];
public now = new Date();
public interval = [
{
label: this.translate.instant("view.server.scheduled_events.edit_dialog.location.intervals.daily"),
value: "daily"
},
{
label: this.translate.instant("view.server.scheduled_events.edit_dialog.location.intervals.weekly"),
value: "weekly"
},
{
label: this.translate.instant("view.server.scheduled_events.edit_dialog.location.intervals.monthly"),
value: "monthly"
},
{
label: this.translate.instant("view.server.scheduled_events.edit_dialog.location.intervals.yearly"),
value: "yearly"
}
];
constructor(
private translate: TranslateService,
private fb: FormBuilder,
private data: DataService,
private spinner: SpinnerService,
private route: ActivatedRoute,
private datePipe: DatePipe
) {
for (let i = 0; i < 25; i++) {
let time = "";
if (i < 10) {
time = `0${i}`;
} else {
time = `${i}`;
}
this.times.push(`${time}:00`);
this.times.push(`${time}:15`);
this.times.push(`${time}:30`);
this.times.push(`${time}:45`);
}
this.setInputForm();
}
public ngOnChanges() {
this.setInputForm();
}
public ngOnInit() {
this.data.getServerFromRoute(this.route).then(server => {
this.server = server;
this.spinner.showSpinner();
this.data.query<SingleDiscordQuery>(Queries.guildsQuery, {
id: server?.discordId
}
).subscribe(data => {
if (data.discord.guilds) {
this.guild = data.discord.guilds[0];
}
this.voiceChannels = this.guild.channels.filter(x => x.type === ChannelType.voice);
this.stageChannels = this.guild.channels.filter(x => x.type === ChannelType.stage);
this.setInputForm();
this.spinner.hideSpinner();
});
});
}
public setInputForm() {
if (this.now.getMinutes() % 15 != 0) {
if (this.now.getMinutes() < 15) {
this.now.setMinutes(15);
} else if (this.now.getMinutes() < 30) {
this.now.setMinutes(30);
} else if (this.now.getMinutes() < 45) {
this.now.setMinutes(45);
} else if (this.now.getMinutes() > 45) {
this.now.setMinutes(0);
this.now.setHours(this.now.getHours() + 1);
}
}
this.now.setSeconds(0);
this.inputForm = this.fb.group({
id: new FormControl<number | undefined>(this.event?.id),
interval: new FormControl<ScheduledEventInterval | undefined>(this.event?.interval, [Validators.required]),
entityType: new FormControl<number | undefined>(this.event?.entityType, [Validators.required]),
channelId: new FormControl<string | undefined>(this.event?.channelId, this.event?.entityType == EventType.voice || this.event?.entityType == EventType.stageInstance ? [Validators.required] : []),
location: new FormControl<string | undefined>(this.event?.location, this.event?.entityType == EventType.external ? [Validators.required] : []),
name: new FormControl<string | undefined>(this.event?.name, [Validators.required]),
startTime: new FormControl<Date | undefined>(this.event?.startTime ? new Date(this.event.startTime) : this.now, [Validators.required]),
endTime: new FormControl<Date | undefined>(this.event?.endTime ? new Date(this.event.endTime) : undefined),
description: new FormControl<string | undefined>(this.event?.description)
});
}
public saveEvent() {
this.event = {
id: this.inputForm.controls.id.value ?? undefined,
interval: this.inputForm.controls.interval.value ?? undefined,
name: this.inputForm.controls.name.value ?? undefined,
description: this.inputForm.controls.description.value ?? undefined,
channelId: this.inputForm.controls.channelId.value ?? undefined,
startTime: this.inputForm.controls.startTime.value ?? undefined,
endTime: this.inputForm.controls.endTime.value ?? undefined,
entityType: this.inputForm.controls.entityType.value ?? undefined,
location: this.inputForm.controls.location.value ?? undefined,
server: this.server
};
this.visible = false;
}
public next() {
this.activeIndex++;
}
public back() {
this.activeIndex--;
}
protected readonly EventType = {
stage: EventType.stageInstance,
voice: EventType.voice,
somewhere_else: EventType.external
};
}

View File

@ -0,0 +1,244 @@
<div *ngIf="editableScheduledEvent">
<app-edit-scheduled-event-dialog [(event)]="editableScheduledEvent"></app-edit-scheduled-event-dialog>
</div>
<h1>
{{'view.server.scheduled_events.header' | translate}}
</h1>
<div class="content-wrapper">
<div class="content">
<p-table #dt [value]="scheduledEvents" [responsive]="true" responsiveLayout="stack" [breakpoint]="'720px'"
dataKey="id" [rowHover]="true" [rows]="10"
[rowsPerPageOptions]="[10,25,50]" [paginator]="true" [loading]="loading" [totalRecords]="totalRecords"
[lazy]="true" (onLazyLoad)="nextPage($event)">
<ng-template pTemplate="caption">
<div class="table-caption">
<div class="table-caption-table-info">
<div class="table-caption-text">
<ng-container *ngIf="!loading">{{scheduledEvents.length}} {{'common.of' | translate}}
{{dt.totalRecords}}
</ng-container>
{{'view.server.scheduled_events.scheduled_events' | translate}}
</div>
<app-multi-select-columns [table]="name" [columns]="columns"
[(hiddenColumns)]="hiddenColumns"></app-multi-select-columns>
</div>
<div class="table-caption-btn-wrapper btn-wrapper">
<button pButton label="{{'common.add' | translate}}" class="icon-btn btn"
icon="pi pi-plus" (click)="addScheduledEvent()"
[disabled]="isEditingNew || !user?.isModerator && !user?.isAdmin">
</button>
<button pButton label="{{'common.reset_filters' | translate}}" icon="pi pi-undo"
class="icon-btn btn" (click)="resetFilters()">
</button>
<app-data-import-and-export name="scheduledEvent" [(data)]="scheduledEvents"
[callback]="callback" [validator]="validator"></app-data-import-and-export>
</div>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th hideable-th="id" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.id' | translate}}</div>
<p-sortIcon field="id" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="interval" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.interval' | translate}}</div>
<p-sortIcon field="interval" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="name" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.name' | translate}}</div>
<p-sortIcon field="name" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="description" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.description' | translate}}</div>
<p-sortIcon field="description" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="channel_id" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.channel_id' | translate}}</div>
<p-sortIcon field="channelId" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="start_time" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.start_time' | translate}}</div>
<p-sortIcon field="startTime" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="end_time" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.end_time' | translate}}</div>
<p-sortIcon field="endTime" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="type" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.type' | translate}}</div>
<p-sortIcon field="entityType" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="location" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.location' | translate}}</div>
<p-sortIcon field="location" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.created_at' | translate}}</div>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.modified_at' | translate}}</div>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.actions' | translate}}</div>
</div>
</th>
</tr>
<tr>
<th hideable-th="id" [parent]="this" class="table-header-small">
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="id"
placeholder="{{'common.id' | translate}}">
</form>
</th>
<th hideable-th="interval" [parent]="this"></th>
<th hideable-th="name" [parent]="this">
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="name"
placeholder="{{'common.name' | translate}}">
</form>
</th>
<th hideable-th="description" [parent]="this">
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="description"
placeholder="{{'common.description' | translate}}">
</form>
</th>
<th hideable-th="channel_id" [parent]="this">
<form [formGroup]="filterForm">
<input type="number" pInputText formControlName="channelId"
placeholder="{{'common.channel_id' | translate}}">
</form>
</th>
<th hideable-th="start_time" [parent]="this"></th>
<th hideable-th="end_time" [parent]="this"></th>
<th hideable-th="type" [parent]="this"></th>
<th hideable-th="location" [parent]="this"></th>
<th class="table-header-small-dropdown"></th>
<th class="table-header-small-dropdown"></th>
<th class="table-header-actions"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-scheduledEvent let-editing="editing" let-ri="rowIndex">
<tr [pEditableRow]="scheduledEvent">
<td hideable-td="id" [parent]="this">
<span class="p-column-title">{{'common.id' | translate}}:</span>
{{scheduledEvent.id}}
</td>
<td hideable-td="interval" [parent]="this">
<span class="p-column-title">{{'common.interval' | translate}}:</span>
{{scheduledEvent.interval}}
</td>
<td hideable-td="name" [parent]="this">
<span class="p-column-title">{{'common.name' | translate}}:</span>
{{scheduledEvent.name}}
</td>
<td hideable-td="description" [parent]="this">
<span class="p-column-title">{{'common.description' | translate}}:</span>
{{scheduledEvent.description}}
</td>
<td hideable-td="channel_id" [parent]="this">
<span class="p-column-title">{{'common.channel_id' | translate}}:</span>
{{scheduledEvent.channelId}}
</td>
<td hideable-td="start_time" [parent]="this">
<span class="p-column-title">{{'common.start_time' | translate}}:</span>
{{scheduledEvent.startTime}}
</td>
<td hideable-td="end_time" [parent]="this">
<span class="p-column-title">{{'common.end_time' | translate}}:</span>
{{scheduledEvent.endTime}}
</td>
<td hideable-td="type" [parent]="this">
<span class="p-column-title">{{'common.type' | translate}}:</span>
{{scheduledEvent.entityType}}
</td>
<td hideable-td="location" [parent]="this">
<span class="p-column-title">{{'common.location' | translate}}:</span>
{{scheduledEvent.location}}
</td>
<td>
<span class="p-column-title">{{'common.created_at' | translate}}:</span>
{{scheduledEvent.createdAt | date:'dd.MM.yy HH:mm'}}
</td>
<td>
<span class="p-column-title">{{'common.modified_at' | translate}}:</span>
{{scheduledEvent.modifiedAt | date:'dd.MM.yy HH:mm'}}
</td>
<td>
<div class="btn-wrapper">
<app-history-btn *ngIf="!isEditingNew" [id]="scheduledEvent.id" [query]="query"
translationKey="view.server.scheduledEvent.header"></app-history-btn>
<button *ngIf="!editing" pButton class="btn icon-btn" icon="pi pi-pencil"
(click)="onRowEditInit(dt, scheduledEvent, ri)"
[disabled]="!user || !user.isModerator && !user.isAdmin"></button>
<button *ngIf="!editing" pButton class="btn icon-btn danger-icon-btn" icon="pi pi-trash"
(click)="deleteScheduledEvent(scheduledEvent)"
[disabled]="!user || !user.isModerator && !user.isAdmin"></button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr></tr>
<tr>
<td colspan="14">{{'common.no_entries_found' | translate}}</td>
</tr>
<tr></tr>
</ng-template>
<ng-template pTemplate="paginatorleft">
</ng-template>
</p-table>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduledEventsComponent } from './scheduled-events.component';
describe('ScheduledEventsComponent', () => {
let component: ScheduledEventsComponent;
let fixture: ComponentFixture<ScheduledEventsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ScheduledEventsComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ScheduledEventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,306 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { Page } from "../../../../../../models/graphql/filter/page.model";
import { Sort, SortDirection } from "../../../../../../models/graphql/filter/sort.model";
import { Subject, throwError } from "rxjs";
import { Server } from "../../../../../../models/data/server.model";
import { UserDTO } from "../../../../../../models/auth/auth-user.dto";
import { LazyLoadEvent } from "primeng/api";
import { Queries } from "../../../../../../models/graphql/queries.model";
import { AuthService } from "../../../../../../services/auth/auth.service";
import { SpinnerService } from "../../../../../../services/spinner/spinner.service";
import { ToastService } from "../../../../../../services/toast/toast.service";
import { ConfirmationDialogService } from "../../../../../../services/confirmation-dialog/confirmation-dialog.service";
import { TranslateService } from "@ngx-translate/core";
import { DataService } from "../../../../../../services/data/data.service";
import { SidebarService } from "../../../../../../services/sidebar/sidebar.service";
import { ActivatedRoute } from "@angular/router";
import { Query, ScheduledEventListQuery } from "../../../../../../models/graphql/query.model";
import { catchError, debounceTime, takeUntil } from "rxjs/operators";
import { Table } from "primeng/table";
import { ScheduledEventMutationResult } from "../../../../../../models/graphql/result.model";
import { Mutations } from "../../../../../../models/graphql/mutations.model";
import { ComponentWithTable } from "../../../../../../base/component-with-table";
import { EventType, ScheduledEvent, ScheduledEventFilter } from "../../../../../../models/data/scheduled_events.model";
@Component({
selector: "app-scheduled-events",
templateUrl: "./scheduled-events.component.html",
styleUrls: ["./scheduled-events.component.scss"]
})
export class ScheduledEventsComponent extends ComponentWithTable implements OnInit, OnDestroy {
public scheduledEvents: ScheduledEvent[] = [];
public loading = true;
public filterForm!: FormGroup<{
id: FormControl<number | null>,
interval: FormControl<string | null>,
name: FormControl<string | null>,
description: FormControl<string | null>,
channelId: FormControl<string | null>,
startTime: FormControl<string | null>,
endTime: FormControl<string | null>,
entityType: FormControl<EventType | null>,
location: FormControl<string | null>,
}>;
public filter: ScheduledEventFilter = {};
public page: Page = {
pageSize: undefined,
pageIndex: undefined
};
public sort: Sort = {
sortColumn: undefined,
sortDirection: undefined
};
public totalRecords: number = 0;
private unsubscriber = new Subject<void>();
private server: Server = {};
public user: UserDTO | null = null;
public query: string = Queries.scheduledEventWithHistoryQuery;
public _editableScheduledEvent?: ScheduledEvent = undefined;
set editableScheduledEvent(event: ScheduledEvent | undefined) {
if (!event) {
this.isEditingNew = false;
} else if (this._editableScheduledEvent && event && this._editableScheduledEvent !== event) {
this.onRowEditSave(event);
}
this._editableScheduledEvent = event;
}
get editableScheduledEvent(): ScheduledEvent | undefined {
return this._editableScheduledEvent;
}
public constructor(
private authService: AuthService,
private spinner: SpinnerService,
private toastService: ToastService,
private confirmDialog: ConfirmationDialogService,
private fb: FormBuilder,
private translate: TranslateService,
private data: DataService,
private sidebar: SidebarService,
private route: ActivatedRoute) {
super("ScheduledEvent", ["id", "interval", "name", "description", "channel_id", "start_time", "end_time", "type", "location"],
(oldElement: ScheduledEvent, newElement: ScheduledEvent) => {
return oldElement.name === newElement.name;
});
}
public ngOnInit(): void {
this.loading = true;
this.setFilterForm();
this.data.getServerFromRoute(this.route).then(async server => {
this.server = server;
let authUser = await this.authService.getLoggedInUser();
this.user = authUser?.users?.find(u => u.server == this.server.id) ?? null;
});
}
public ngOnDestroy(): void {
this.unsubscriber.next();
this.unsubscriber.complete();
}
public loadNextPage(): void {
this.data.query<ScheduledEventListQuery>(Queries.scheduledEventQuery, {
serverId: this.server.id, filter: this.filter, page: this.page, sort: this.sort
},
(data: Query) => {
return data.servers[0];
}
).subscribe(data => {
this.totalRecords = data.scheduledEventCount;
this.scheduledEvents = data.scheduledEvents;
this.spinner.hideSpinner();
this.loading = false;
});
}
public setFilterForm(): void {
this.filterForm = this.fb.group({
id: new FormControl<number | null>(null),
interval: new FormControl<string | null>(null),
name: new FormControl<string | null>(null),
description: new FormControl<string | null>(null),
channelId: new FormControl<string | null>(null),
startTime: new FormControl<string | null>(null),
endTime: new FormControl<string | null>(null),
entityType: new FormControl<EventType | null>(null),
location: new FormControl<string | null>(null)
});
this.filterForm.valueChanges.pipe(
takeUntil(this.unsubscriber),
debounceTime(600)
).subscribe(changes => {
if (changes.id) {
this.filter.id = changes.id;
} else {
this.filter.id = undefined;
}
if (changes.interval) {
this.filter.interval = changes.interval;
} else {
this.filter.interval = undefined;
}
if (changes.name) {
this.filter.name = changes.name;
} else {
this.filter.name = undefined;
}
if (changes.description) {
this.filter.description = changes.description;
} else {
this.filter.description = undefined;
}
if (changes.channelId) {
this.filter.channelId = changes.channelId;
} else {
this.filter.channelId = undefined;
}
if (changes.startTime) {
this.filter.startTime = changes.startTime;
} else {
this.filter.startTime = undefined;
}
if (changes.endTime) {
this.filter.endTime = changes.endTime;
} else {
this.filter.endTime = undefined;
}
if (changes.entityType) {
this.filter.entityType = changes.entityType;
} else {
this.filter.entityType = undefined;
}
if (changes.location) {
this.filter.location = changes.location;
} else {
this.filter.location = undefined;
}
if (this.page.pageSize)
this.page.pageSize = 10;
if (this.page.pageIndex)
this.page.pageIndex = 0;
this.loadNextPage();
});
}
public newScheduledEventTemplate: ScheduledEvent = {
createdAt: "",
modifiedAt: ""
};
public nextPage(event: LazyLoadEvent): void {
this.page.pageSize = event.rows ?? 0;
if (event.first != null && event.rows != null)
this.page.pageIndex = event.first / event.rows;
this.sort.sortColumn = event.sortField ?? undefined;
this.sort.sortDirection = event.sortOrder === 1 ? SortDirection.ASC : event.sortOrder === -1 ? SortDirection.DESC : SortDirection.ASC;
this.loadNextPage();
}
public resetFilters(): void {
this.filterForm.reset();
}
public onRowEditInit(table: Table, event: ScheduledEvent, index: number): void {
this.editableScheduledEvent = event;
}
public override onRowEditSave(newScheduledEvent: ScheduledEvent): void {
if (this.isEditingNew && JSON.stringify(newScheduledEvent) === JSON.stringify(this.newScheduledEventTemplate)) {
this.isEditingNew = false;
return;
}
if (this.isEditingNew) {
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.createScheduledEvent, {
interval: newScheduledEvent.interval,
name: newScheduledEvent.name,
description: newScheduledEvent.description,
channelId: newScheduledEvent.channelId,
startTime: newScheduledEvent.startTime,
endTime: newScheduledEvent.endTime,
entityType: newScheduledEvent.entityType,
location: newScheduledEvent.location,
serverId: this.server.id
}
).pipe(catchError(err => {
this.isEditingNew = false;
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(result => {
this.isEditingNew = false;
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.scheduled_events.message.scheduled_event_create"), this.translate.instant("view.server.scheduled_events.message.scheduled_event_create_d", { name: result.scheduledEvent.createScheduledEvent?.name }));
this.loadNextPage();
});
return;
}
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.updateScheduledEvent, {
id: newScheduledEvent.id,
interval: newScheduledEvent.interval,
name: newScheduledEvent.name,
description: newScheduledEvent.description,
channelId: newScheduledEvent.channelId,
startTime: newScheduledEvent.startTime,
endTime: newScheduledEvent.endTime,
entityType: newScheduledEvent.entityType,
location: newScheduledEvent.location,
serverId: this.server.id
}
).pipe(catchError(err => {
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(_ => {
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.scheduled_events.message.scheduled_event_update"), this.translate.instant("view.server.scheduled_events.message.scheduled_event_update_d", { name: newScheduledEvent.name }));
this.loadNextPage();
});
}
public deleteScheduledEvent(ScheduledEvent: ScheduledEvent): void {
this.confirmDialog.confirmDialog(
this.translate.instant("view.server.scheduled_events.message.scheduled_event_delete"), this.translate.instant("view.server.scheduled_events.message.scheduled_event_delete_q", { name: ScheduledEvent.name }),
() => {
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.deleteScheduledEvent, {
id: ScheduledEvent.id
}
).pipe(catchError(err => {
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(l => {
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.scheduled_events.message.scheduled_event_deleted"), this.translate.instant("view.server.scheduled_events.message.scheduled_event_deleted_d", { name: ScheduledEvent.name }));
this.loadNextPage();
});
});
}
public addScheduledEvent(): void {
this.isEditingNew = true;
this.editableScheduledEvent = JSON.parse(JSON.stringify(this.newScheduledEventTemplate));
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component";
const routes: Routes = [
{ path: "", component: ScheduledEventsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ScheduledEventsRoutingModule {
}

View File

@ -0,0 +1,25 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ScheduledEventsRoutingModule } from "./scheduled-events-routing.module";
import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component";
import { SharedModule } from "../../../shared/shared.module";
import {
EditScheduledEventDialogComponent
} from "./components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component";
@NgModule({
declarations: [
ScheduledEventsComponent,
EditScheduledEventDialogComponent
],
imports: [
CommonModule,
SharedModule,
ScheduledEventsRoutingModule
],
exports: []
})
export class ScheduledEventsModule {
}

View File

@ -19,6 +19,7 @@ const routes: Routes = [
{ path: "levels", loadChildren: () => import("./levels/levels.module").then(m => m.LevelsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "achievements", loadChildren: () => import("./achievements/achievements.module").then(m => m.AchievementsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "short-role-names", loadChildren: () => import("./short-role-name/short-role-name.module").then(m => m.ShortRoleNameModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "scheduled-events", loadChildren: () => import("./scheduled-events/scheduled-events.module").then(m => m.ScheduledEventsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "config", loadChildren: () => import("./config/config.module").then(m => m.ConfigModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Admin } }
];

View File

@ -1,14 +1,8 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ShortRoleNamesComponent } from './components/short-role-names/short-role-names.component';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ShortRoleNamesComponent } from "./components/short-role-names/short-role-names.component";
import { ShortRoleNameRoutingModule } from "./short-role-name-routing.module";
import { ButtonModule } from "primeng/button";
import { InputTextModule } from "primeng/inputtext";
import { ReactiveFormsModule } from "@angular/forms";
import { SharedModule } from "../../../shared/shared.module";
import { TableModule } from "primeng/table";
import { TranslateModule } from "@ngx-translate/core";
@NgModule({
@ -18,13 +12,7 @@ import { TranslateModule } from "@ngx-translate/core";
imports: [
CommonModule,
ShortRoleNameRoutingModule,
ButtonModule,
InputTextModule,
ReactiveFormsModule,
SharedModule,
SharedModule,
TableModule,
TranslateModule
]
})
export class ShortRoleNameModule { }

View File

@ -4,7 +4,7 @@ import { BehaviorSubject, forkJoin, Observable } from "rxjs";
import { AuthRoles } from "../../models/auth/auth-roles.enum";
import { AuthService } from "../auth/auth.service";
import { TranslateService } from "@ngx-translate/core";
import { NavigationEnd, Router } from "@angular/router";
import { Router } from "@angular/router";
import { ThemeService } from "../theme/theme.service";
import { Server } from "../../models/data/server.model";
import { UserDTO } from "../../models/auth/auth-user.dto";
@ -30,6 +30,7 @@ export class SidebarService {
serverAutoRoles: MenuItem = {};
serverLevels: MenuItem = {};
serverAchievements: MenuItem = {};
serverScheduledEvents: MenuItem = {};
serverShortRoleNames: MenuItem = {};
serverConfig: MenuItem = {};
serverMenu: MenuItem = {};
@ -110,6 +111,13 @@ export class SidebarService {
routerLink: `server/${this.server?.id}/achievements`
};
this.serverScheduledEvents = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.scheduled_events") : "",
icon: "pi pi-calendar",
visible: true,
routerLink: `server/${this.server?.id}/scheduled-events`
};
this.serverShortRoleNames = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.short_role_names") : "",
icon: "pi pi-list",
@ -129,7 +137,7 @@ export class SidebarService {
icon: "pi pi-server",
visible: false,
expanded: true,
items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverShortRoleNames, this.serverConfig]
items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverScheduledEvents, this.serverShortRoleNames, this.serverConfig]
};
this.adminConfig = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.config") : "",
@ -205,6 +213,7 @@ export class SidebarService {
this.serverAutoRoles.visible = isTechnicianAndFullAccessActive || this.hasFeature("AutoRoleModule") && user?.isModerator;
this.serverLevels.visible = isTechnicianAndFullAccessActive || this.hasFeature("LevelModule") && user?.isModerator;
this.serverAchievements.visible = isTechnicianAndFullAccessActive || this.hasFeature("AchievementsModule") && user?.isModerator;
this.serverScheduledEvents.visible = isTechnicianAndFullAccessActive || this.hasFeature("ScheduledEvents") && user?.isModerator;
this.serverShortRoleNames.visible = isTechnicianAndFullAccessActive || this.hasFeature("ShortRoleName") && user?.isAdmin;
this.serverConfig.visible = isTechnicianAndFullAccessActive || user?.isAdmin;

View File

@ -124,12 +124,14 @@
},
"common": {
"404": "404 - Der Eintrag konnte nicht gefunden werden",
"abort": "Abbrechen",
"actions": "Aktionen",
"active": "Aktiv",
"add": "Hinzufügen",
"attribute": "Attribut",
"auth_role": "Rolle",
"author": "Autor",
"back": "Zurück",
"bool_as_string": {
"false": "Nein",
"true": "Ja"
@ -137,12 +139,14 @@
"channel_id": "Kanal Id",
"channel_name": "Kanal",
"color": "Farbe",
"continue": "Weiter",
"created_at": "Erstellt am",
"description": "Beschreibung",
"discord_id": "Discord Id",
"edit": "Bearbeiten",
"email": "E-Mail",
"emoji": "Emoji",
"end_time": "Endzeit",
"error": "Fehler",
"export": "Exportieren",
"feature_flags": "Funktionen",
@ -176,11 +180,13 @@
},
"id": "Id",
"import": "Importieren",
"interval": "Interval",
"joined_at": "Beigetreten am",
"last_name": "Nachname",
"leaved_at": "Verlassen am",
"left_server": "Aktiv",
"level": "Level",
"location": "Ort",
"message_id": "Nachricht Id",
"min_xp": "Min. XP",
"modified_at": "Bearbeitet am",
@ -200,10 +206,12 @@
"role": "Rolle",
"rule_count": "Regeln",
"save": "Speichern",
"start_time": "Startzeit",
"state": {
"off": "Aus",
"on": "Ein"
},
"type": "Typ",
"user_warnings": "Verwarnungen",
"users": "Benutzer",
"value": "Wert",
@ -327,6 +335,7 @@
"config": "Konfiguration",
"dashboard": "Dashboard",
"members": "Mitglieder",
"scheduled_events": "Geplante Events",
"server": {
"achievements": "Errungenschaften",
"auto_roles": "Auto Rollen",
@ -335,6 +344,7 @@
"levels": "Level",
"members": "Mitglieder",
"profile": "Dein Profil",
"scheduled_events": "Geplante Events",
"short_role_names": "Rollen Kürzel"
},
"server_empty": "Kein Server ausgewählt",
@ -538,6 +548,57 @@
"reaction_count": "Anzahl Reaktionen",
"xp": "XP"
},
"scheduled_events": {
"edit_dialog": {
"add_header": "Event hinzufügen",
"edit_header": "Event bearbeiten",
"event_info": {
"description": "Beschreibung",
"description_input": "Erzähl den Leuten ein wenig mehr über dein Event. Markdown, neue Zeilen und Links werden unterstützt.",
"end_date_time": "Endzeitpunkt",
"event_topic": "Thema",
"event_topic_input": "Worum geht es bei deinem Event?",
"header": "Worum geht es bei deinem Event?",
"start_date_time": "Startzeitpunkt",
"tab_name": "Eventinformationen"
},
"location": {
"header": "Wo ist dein Event?",
"interval": "Interval",
"intervals": {
"daily": "Täglich",
"monthly": "Monatlich",
"weekly": "Wöchentlich",
"yearly": "Jährlich"
},
"somewhere_else": "Irgendwo anders",
"somewhere_else_input": "Ort eingeben",
"stage": "Stage-Kanal",
"stage_input": "Stage-Kanal auswählen",
"tab_name": "Verzeichnis",
"voice": "Sprachkanal",
"voice_input": "Sprachkanal auswählen"
}
},
"header": "Geplante Events",
"message": {
"scheduled_event_create": "Geplantes Event erstellt",
"scheduled_event_create_d": "Geplantes Event {{name}} erfolgreich erstellt",
"scheduled_event_create_failed": "Geplantes Event Erstellung fehlgeschlagen",
"scheduled_event_create_failed_d": "Die Erstellung der Geplantes Event ist fehlgeschlagen!",
"scheduled_event_delete": "Geplantes Event löschen",
"scheduled_event_delete_failed": "Geplantes Event Löschung fehlgeschlagen",
"scheduled_event_delete_failed_d": "Die Löschung der Geplantes Event {{name}} ist fehlgeschlagen!",
"scheduled_event_delete_q": "Sind Sie sich sicher, dass Sie das Geplantes Event {{name}} löschen möchten?",
"scheduled_event_deleted": "Geplantes Event gelöscht",
"scheduled_event_deleted_d": "Geplantes Event {{name}} erfolgreich gelöscht",
"scheduled_event_update": "Geplantes Event bearbeitet",
"scheduled_event_update_d": "Geplantes Event {{name}} erfolgreich bearbeitet",
"scheduled_event_update_failed": "Geplantes Event Bearbeitung fehlgeschlagen",
"scheduled_event_update_failed_d": "Die Bearbeitung der Geplantes Event ist fehlgeschlagen!"
},
"scheduled_events": "Geplante Events"
},
"short_role_names": {
"header": "Rollen Kürzel",
"message": {

View File

@ -124,12 +124,14 @@
},
"common": {
"404": "404 - Entry not found!",
"abort": "Abort",
"actions": "Actions",
"active": "Active",
"add": "Add",
"attribute": "Attribute",
"auth_role": "Role",
"author": "Author",
"back": "Back",
"bool_as_string": {
"false": "No",
"true": "Yes"
@ -137,12 +139,14 @@
"channel_id": "Channel Id",
"channel_name": "Channel",
"color": "Color",
"continue": "Continue",
"created_at": "Created at",
"description": "Description",
"discord_id": "Discord Id",
"edit": "Edit",
"email": "E-Mail",
"emoji": "Emoji",
"end_time": "End time",
"error": "Error",
"export": "Export",
"feature_flags": "Features",
@ -176,11 +180,13 @@
},
"id": "Id",
"import": "Import",
"interval": "interval",
"joined_at": "Joined at",
"last_name": "Last name",
"leaved_at": "Leaved at",
"left_server": "Active",
"level": "Level",
"location": "Location",
"message_id": "Message Id",
"min_xp": "Min. XP",
"modified_at": "Modified at",
@ -200,10 +206,12 @@
"role": "Role",
"rule_count": "Rules",
"save": "Save",
"start_time": "Start time",
"state": {
"off": "Off",
"on": "On"
},
"type": "Type",
"user_warnings": "User warnings",
"users": "User",
"value": "Value",
@ -327,6 +335,7 @@
"config": "Configuration",
"dashboard": "Dashboard",
"members": "Members",
"scheduled_events": "Scheduled event",
"server": {
"achievements": "Achievements",
"auto_roles": "Auto role",
@ -335,6 +344,7 @@
"levels": "Level",
"members": "Members",
"profile": "Your profile",
"scheduled_events": "Scheduled events",
"short_role_names": "Short role names"
},
"server_empty": "No server selected",
@ -538,6 +548,57 @@
"reaction_count": "Reaction count",
"xp": "XP"
},
"scheduled_events": {
"edit_dialog": {
"add_header": "Add event",
"edit_header": "Edit event",
"event_info": {
"description": "Description",
"description_input": "Tell people a little more about your event. Markdown, new lines and links are supported.",
"end_date_time": "End date",
"event_topic": "Event topic",
"event_topic_input": "What's your event?",
"header": "What's your event about?",
"start_date_time": "Start date",
"tab_name": "Event info"
},
"location": {
"header": "Where is your event?",
"interval": "Interval",
"intervals": {
"daily": "Daily",
"monthly": "Monthly",
"weekly": "Weekly",
"yearly": "Yearly"
},
"somewhere_else": "Somewhere else",
"somewhere_else_input": "Enter a location",
"stage": "Stage channel",
"stage_input": "Select a stage channel",
"tab_name": "Location",
"voice": "Voice channel",
"voice_input": "Select a voice channel"
}
},
"header": "Scheduled events",
"message": {
"scheduled_event_create": "Scheduled event created",
"scheduled_event_create_d": "Scheduled event {{name}} successfully created",
"scheduled_event_create_failed": "Scheduled event creation failed",
"scheduled_event_create_failed_d": "Creation of scheduled event failed!",
"scheduled_event_delete": "Delete scheduled event",
"scheduled_event_delete_failed": "Scheduled event deletion failed",
"scheduled_event_delete_failed_d": "Deletion of scheduled event {{name}} failed!",
"scheduled_event_delete_q": "Are you sure you want to delete the {{name}} scheduled_event?",
"scheduled_event_deleted": "Scheduled event deleted",
"scheduled_event_deleted_d": "Scheduled event {{name}} successfully deleted\t",
"scheduled_event_update": "Scheduled event edited",
"scheduled_event_update_d": "Scheduled event {{name}} edited successfully",
"scheduled_event_update_failed": "Scheduled event editing failed",
"scheduled_event_update_failed_d": "Scheduled event editing failed!"
},
"scheduled_events": "Scheduled events"
},
"short_role_names": {
"header": "Level",
"message": {

View File

@ -1,7 +1,7 @@
{
"WebVersion": {
"Major": "1",
"Minor": "2",
"Micro": "2"
}
}
"WebVersion": {
"Major": "1",
"Minor": "2",
"Micro": "dev410"
}
}

View File

@ -635,6 +635,18 @@ footer {
border: 0;
}
.btn,
.icon-btn,
.text-btn,
.danger-btn,
.danger-icon-btn {
span {
transition-duration: unset !important;
}
transition: none !important;
}
.spinner-component-wrapper {
position: absolute;
top: 0;
@ -661,6 +673,61 @@ p-inputNumber {
border: none !important;
}
.edit-dialog {
.p-dialog-content {
padding: 0 !important;
.p-tabview-nav {
justify-content: space-between;
li {
width: 100%;
a {
width: 100%;
justify-content: center;
}
}
}
}
.form {
display: flex;
flex-direction: column;
gap: 20px;
textarea {
min-height: 101px !important;
&:focus {
box-shadow: none !important;
}
}
.type {
display: flex;
flex-direction: column;
gap: 10px;
.field-checkbox {
display: flex;
gap: 15px;
}
}
input,
.p-dropdown {
width: 100%;
}
}
}
p-calendar {
.p-calendar {
width: 100% !important;
}
}
@media (max-width: 720px) {
footer {
.left,

View File

@ -113,3 +113,4 @@ p-table {
.pi-sort-amount-down:before {
content: "\e913" !important;
}

View File

@ -187,12 +187,15 @@
}
}
.p-dialog-header {
background-color: $secondaryBackgroundColor !important;
background-color: $primaryBackgroundColor !important;
color: $primaryTextColor !important;
}
.p-dialog-content {
background-color: $secondaryBackgroundColor !important;
.content-data-name,
.content-data-value {
color: $primaryTextColor;
@ -345,9 +348,11 @@
}
p-dropdown {
.p-dropdown {
background-color: $primaryBackgroundColor !important;
border-color: $primaryTextColor !important;
border: $default-border;
border-radius: 10px;
background-color: $secondaryBackgroundColor !important;
color: $primaryTextColor !important;
span {
@ -411,8 +416,6 @@
}
}
}
}
.table-edit-input {
@ -537,6 +540,10 @@
}
.icon-btn {
.p-button-label {
transition-duration: unset !important;
}
&:hover {
background-color: transparent !important;
color: $primaryHeaderColor !important;
@ -544,6 +551,7 @@
}
}
.danger-btn,
.danger-icon-btn {
background-color: transparent !important;
color: $primaryTextColor !important;
@ -560,25 +568,11 @@
}
}
.danger-btn {
background-color: $primaryErrorColor !important;
color: $primaryErrorColor !important;
border: 0 !important;
&:hover {
background-color: $primaryErrorColor !important;
color: $primaryTextColor !important;
border: 0;
}
.pi {
font-size: 1.275rem !important;
}
}
.p-datatable .p-sortable-column.p-highlight,
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon {
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon,
.p-datatable .p-sortable-column:not(.p-highlight):hover {
color: $primaryHeaderColor !important;
background-color: transparent !important;
}
.p-dropdown:not(.p-disabled):hover,
@ -616,6 +610,27 @@
}
}
.p-selectbutton {
.p-highlight {
background-color: $primaryHeaderColor !important;
}
.p-button {
border: 1px solid $primaryHeaderColor !important;
&:hover {
background-color: $secondaryHeaderColor !important;
border: 1px solid $secondaryHeaderColor !important;
}
&:focus {
border-color: $primaryHeaderColor !important;
box-shadow: none !important;
}
}
}
.p-multiselect {
background-color: $primaryBackgroundColor !important;
color: $primaryTextColor !important;
@ -661,4 +676,141 @@
}
}
}
.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider {
background: $primaryHeaderColor !important;
}
.p-inputswitch.p-focus .p-inputswitch-slider {
box-shadow: none !important;
}
p-inputNumber {
background-color: $primaryBackgroundColor !important;
}
p-calendar > span > button {
background-color: $primaryHeaderColor !important;
border: 1px solid $primaryHeaderColor !important;
&:focus {
box-shadow: none !important;
}
}
.p-calendar {
.p-datepicker:not(.p-datepicker-inline) {
background-color: $secondaryBackgroundColor !important;
}
.p-datepicker {
color: $primaryTextColor !important;
.p-datepicker-header {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
.p-datepicker-title .p-datepicker-year,
.p-datepicker-title .p-datepicker-month,
.p-datepicker .p-datepicker-header .p-datepicker-title .p-datepicker-month {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
}
table td > span {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
&:focus {
box-shadow: none !important;
}
}
table td.p-datepicker-today > span {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
table td > span.p-highlight {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
.p-yearpicker .p-yearpicker-year,
.p-monthpicker .p-monthpicker-month:not(.p-disabled):not(.p-highlight) {
color: $primaryTextColor !important;
background-color: $secondaryBackgroundColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
.edit-dialog {
textarea {
background-color: $secondaryBackgroundColor;
color: $primaryTextColor;
&:hover {
border-color: $primaryHeaderColor;
}
&:focus {
border-color: $primaryHeaderColor;
}
}
.p-dialog-content {
.p-tabview {
.p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus {
box-shadow: none !important;
}
.p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: $primaryHeaderColor !important;
border-color: $primaryHeaderColor !important;
}
.p-tabview-nav,
.p-tabview-nav li .p-tabview-nav-link,
.p-tabview-panels {
background-color: $secondaryBackgroundColor !important;
color: $primaryTextColor !important;
}
}
}
}
.p-radiobutton {
.p-radiobutton-box.p-highlight {
border-color: $primaryHeaderColor !important;
background: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled):not(.p-highlight):hover {
border-color: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled).p-focus {
box-shadow: none !important;
}
}
}

View File

@ -544,6 +544,7 @@
}
}
.danger-btn,
.danger-icon-btn {
background-color: transparent !important;
color: $primaryTextColor !important;
@ -560,22 +561,6 @@
}
}
.danger-btn {
background-color: $primaryErrorColor !important;
color: $primaryErrorColor !important;
border: 0 !important;
&:hover {
background-color: $primaryErrorColor !important;
color: $primaryTextColor !important;
border: 0;
}
.pi {
font-size: 1.275rem !important;
}
}
.p-datatable .p-sortable-column.p-highlight,
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon {
color: $primaryHeaderColor !important;
@ -602,16 +587,23 @@
color: $primaryHeaderColor !important;
}
.input-number {
span {
.p-button {
background-color: $primaryHeaderColor !important;
border: 1px solid $primaryHeaderColor !important;
.p-selectbutton {
.p-highlight {
background-color: $primaryHeaderColor !important;
}
&:hover {
background-color: $secondaryHeaderColor !important;
border: 1px solid $secondaryHeaderColor !important;
}
.p-button {
border: 1px solid $primaryHeaderColor !important;
&:hover {
background-color: $secondaryHeaderColor !important;
border: 1px solid $secondaryHeaderColor !important;
}
&:focus {
border-color: $primaryHeaderColor !important;
box-shadow: none !important;
}
}
}
@ -629,12 +621,12 @@
.p-multiselect-panel {
.p-multiselect-header {
background-color: $secondaryBackgroundColor !important;
background-color: $primaryBackgroundColor !important;
}
.p-multiselect-items,
.p-multiselect-item {
background-color: $primaryBackgroundColor !important;
background-color: $secondaryBackgroundColor !important;
}
.p-multiselect-item {
@ -661,4 +653,141 @@
}
}
}
.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider {
background: $primaryHeaderColor !important;
}
.p-inputswitch.p-focus .p-inputswitch-slider {
box-shadow: none !important;
}
p-inputNumber {
background-color: $primaryBackgroundColor !important;
}
p-calendar > span > button {
background-color: $primaryHeaderColor !important;
border: 1px solid $primaryHeaderColor !important;
&:focus {
box-shadow: none !important;
}
}
.p-calendar {
.p-datepicker:not(.p-datepicker-inline) {
background-color: $primaryBackgroundColor !important;
}
.p-datepicker {
color: $primaryTextColor !important;
.p-datepicker-header {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
.p-datepicker-title .p-datepicker-year,
.p-datepicker-title .p-datepicker-month,
.p-datepicker .p-datepicker-header .p-datepicker-title .p-datepicker-month {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
}
table td > span {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
&:focus {
box-shadow: none !important;
}
}
table td.p-datepicker-today > span {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
table td > span.p-highlight {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
.p-yearpicker .p-yearpicker-year,
.p-monthpicker .p-monthpicker-month:not(.p-disabled):not(.p-highlight) {
color: $primaryTextColor !important;
background-color: $secondaryBackgroundColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
.edit-dialog {
textarea {
background-color: $primaryBackgroundColor;
color: $primaryTextColor;
&:hover {
border-color: $primaryHeaderColor;
}
&:focus {
border-color: $primaryHeaderColor;
}
}
.p-dialog-content {
.p-tabview {
.p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus {
box-shadow: none !important;
}
.p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: $primaryHeaderColor !important;
border-color: $primaryHeaderColor !important;
}
.p-tabview-nav,
.p-tabview-nav li .p-tabview-nav-link,
.p-tabview-panels {
background-color: $primaryBackgroundColor !important;
color: $primaryTextColor !important;
}
}
}
}
.p-radiobutton {
.p-radiobutton-box.p-highlight {
border-color: $primaryHeaderColor !important;
background: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled):not(.p-highlight):hover {
border-color: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled).p-focus {
box-shadow: none !important;
}
}
}

View File

@ -552,6 +552,7 @@
}
}
.danger-btn,
.danger-icon-btn {
background-color: transparent !important;
color: $primaryTextColor !important;
@ -568,22 +569,6 @@
}
}
.danger-btn {
background-color: $primaryErrorColor !important;
color: $primaryErrorColor !important;
border: 0 !important;
&:hover {
background-color: $primaryErrorColor !important;
color: $primaryTextColor !important;
border: 0;
}
.pi {
font-size: 1.275rem !important;
}
}
.p-datatable .p-sortable-column.p-highlight,
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon,
.p-datatable .p-sortable-column:not(.p-highlight):hover {
@ -720,6 +705,8 @@
}
.p-datepicker {
color: $primaryTextColor !important;
.p-datepicker-header {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
@ -777,4 +764,54 @@
}
}
}
.edit-dialog {
textarea {
background-color: $secondaryBackgroundColor;
color: $primaryTextColor;
&:hover {
border-color: $primaryHeaderColor;
}
&:focus {
border-color: $primaryHeaderColor;
}
}
.p-dialog-content {
.p-tabview {
.p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus {
box-shadow: none !important;
}
.p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: $primaryHeaderColor !important;
border-color: $primaryHeaderColor !important;
}
.p-tabview-nav,
.p-tabview-nav li .p-tabview-nav-link,
.p-tabview-panels {
background-color: $secondaryBackgroundColor !important;
color: $primaryTextColor !important;
}
}
}
}
.p-radiobutton {
.p-radiobutton-box.p-highlight {
border-color: $primaryHeaderColor !important;
background: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled):not(.p-highlight):hover {
border-color: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled).p-focus {
box-shadow: none !important;
}
}
}

View File

@ -544,6 +544,7 @@
}
}
.danger-btn,
.danger-icon-btn {
background-color: transparent !important;
color: $primaryTextColor !important;
@ -560,22 +561,6 @@
}
}
.danger-btn {
background-color: $primaryErrorColor !important;
color: $primaryErrorColor !important;
border: 0 !important;
&:hover {
background-color: $primaryErrorColor !important;
color: $primaryTextColor !important;
border: 0;
}
.pi {
font-size: 1.275rem !important;
}
}
.p-datatable .p-sortable-column.p-highlight,
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon {
color: $primaryHeaderColor !important;
@ -661,4 +646,142 @@
}
}
}
.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider {
background: $primaryHeaderColor !important;
}
.p-inputswitch.p-focus .p-inputswitch-slider {
box-shadow: none !important;
}
p-inputNumber {
background-color: $primaryBackgroundColor !important;
}
p-calendar > span > button {
background-color: $primaryHeaderColor !important;
border: 1px solid $primaryHeaderColor !important;
&:focus {
box-shadow: none !important;
}
}
.p-calendar {
.p-datepicker:not(.p-datepicker-inline) {
background-color: $primaryBackgroundColor !important;
}
.p-datepicker {
color: $primaryTextColor !important;
.p-datepicker-header {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
.p-datepicker-title .p-datepicker-year,
.p-datepicker-title .p-datepicker-month,
.p-datepicker .p-datepicker-header .p-datepicker-title .p-datepicker-month {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
}
table td > span {
color: $primaryTextColor !important;
&:hover {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
&:focus {
box-shadow: none !important;
}
}
table td.p-datepicker-today > span {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
table td > span.p-highlight {
color: $primaryHeaderColor !important;
background-color: $primaryBackgroundColor !important;
}
.p-yearpicker .p-yearpicker-year,
.p-monthpicker .p-monthpicker-month:not(.p-disabled):not(.p-highlight) {
color: $primaryTextColor !important;
background-color: $secondaryBackgroundColor !important;
&:hover {
color: $primaryHeaderColor !important;
}
&:focus {
box-shadow: none !important;
}
}
}
.edit-dialog {
textarea {
background-color: $primaryBackgroundColor;
color: $primaryTextColor;
&:hover {
border-color: $primaryHeaderColor;
}
&:focus {
border-color: $primaryHeaderColor;
}
}
.p-dialog-content {
.p-tabview {
.p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus {
box-shadow: none !important;
}
.p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: $primaryHeaderColor !important;
border-color: $primaryHeaderColor !important;
}
.p-tabview-nav,
.p-tabview-nav li .p-tabview-nav-link,
.p-tabview-panels {
background-color: $primaryBackgroundColor !important;
color: $primaryTextColor !important;
}
}
}
}
.p-radiobutton {
.p-radiobutton-box.p-highlight {
border-color: $primaryHeaderColor !important;
background: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled):not(.p-highlight):hover {
border-color: $primaryHeaderColor !important;
}
.p-radiobutton-box:not(.p-disabled).p-focus {
box-shadow: none !important;
}
}
}