add css utils, display form, enhance Text, add profile base format

* fix format logic with optional list
* add forms display handling logic
* add Image component
* update Text component to work with Icons + enhance card and content style
* add tailwind css utils for builder
* add Profile page, builder component and display store
* add profile page format
* update profile test script with right value format to fit components props
* add Collection component that is a builder with all component
* misc enhancement
This commit is contained in:
Jordan Blasenhauer 2024-08-15 22:04:27 +02:00
parent db3a6ac7bd
commit 6c8ac4b7a1
26 changed files with 936 additions and 158 deletions

View file

@ -83,7 +83,7 @@ def ban_item(id: str, ip: str, reason: str, ban_start_date: int, ban_end_date: i
def bans_items(bans: Optional[list] = None) -> list:
if bans is None or len(bans) == 0:
if bans is None or (isinstance(bans, list) and len(bans) == 0):
return []
items = []
@ -129,7 +129,7 @@ def bans_filters(reasons: Optional[list] = None, remains: Optional[list] = None)
]
# Case "all" ans
if reasons is not None and len(reasons) >= 2:
if reasons is not None and (isinstance(reasons, list) and len(reasons) >= 2):
filters.append(
{
"type": "=",
@ -154,7 +154,7 @@ def bans_filters(reasons: Optional[list] = None, remains: Optional[list] = None)
},
)
if remains is not None and len(remains) >= 2:
if remains is not None and (isinstance(remains, list) and len(remains) >= 2):
filters.append(
{
"type": "=",
@ -195,7 +195,7 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def bans_list(bans: Optional[list] = None, reasons: Optional[list] = None, remains: Optional[list] = None) -> dict:
if bans is None or len(bans) == 0:
if bans is None or (isinstance(bans, list) and len(bans) == 0):
return fallback_message(msg="bans_not_found", display=["main", 0])
actions_table_list = [

View file

@ -72,7 +72,7 @@ def configs_filter(config_types: Optional[list] = None) -> list: # healths = "u
},
]
if config_types is not None and len(config_types) >= 2:
if config_types is not None and (isinstance(config_types, list) and len(config_types) >= 2):
filters.append(
{
"type": "=",
@ -436,10 +436,10 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def configs_builder(configs: Optional[list] = None, config_types: Optional[list] = None, services: Optional[list] = None) -> list:
if config_types is None or len(config_types) == 0:
if config_types is None or (isinstance(config_types, list) and len(config_types) == 0):
return [fallback_message(msg="configs_missing_types")]
if services is None or len(services) == 0:
if services is None or (isinstance(services, list) and len(services) == 0):
return [fallback_message(msg="configs_missing_services")]
configs_items = []
@ -459,7 +459,7 @@ def configs_builder(configs: Optional[list] = None, config_types: Optional[list]
)
)
if configs is None or len(configs) == 0:
if configs is None or (isinstance(configs, list) and len(configs) == 0):
return [
# Tabs is button group with display value and a size tab inside a tabs container
configs_tabs(),

View file

@ -50,7 +50,7 @@ def instances_filter(healths: str, types: Optional[list] = None, methods: Option
}
]
if types is not None and len(types) >= 2:
if types is not None and (isinstance(types, list) and len(types) >= 2):
filters.append(
{
"type": "=",
@ -75,7 +75,7 @@ def instances_filter(healths: str, types: Optional[list] = None, methods: Option
}
)
if methods is not None and len(methods) >= 2:
if methods is not None and (isinstance(methods, list) and len(methods) >= 2):
filters.append(
{
"type": "=",
@ -100,7 +100,7 @@ def instances_filter(healths: str, types: Optional[list] = None, methods: Option
},
)
if healths is not None and len(healths) >= 2:
if healths is not None and (isinstance(healths, list) and len(healths) >= 2):
filters.append(
{
"type": "=",
@ -396,7 +396,7 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def instances_list(instances: Optional[list] = None, types: Optional[list] = None, methods: Optional[list] = None, healths: Optional[list] = None) -> dict:
if instances is None or len(instances) == 0:
if instances is None or (isinstance(instances, list) and len(instances) == 0):
return fallback_message(msg="instances_not_found", display=["main", 0])
items = []

View file

@ -36,6 +36,7 @@ from .utils.widgets import (
regular_widget,
unmatch_widget,
pairs_widget,
image_widget,
)
from .utils.table import add_column
from .utils.format import get_fields_from_field
@ -55,7 +56,7 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def profile_info(user_profile: Optional[list] = None) -> dict:
if user_profile is None or len(user_profile) == 0:
if user_profile is None or (isinstance(user_profile, list) and len(user_profile) == 0):
return fallback_message("profile_info_not_found", display=["main", 0])
return {
@ -67,9 +68,12 @@ def profile_info(user_profile: Optional[list] = None) -> dict:
title="profile_info_title", # keep it (a18n)
),
subtitle_widget(
subtitle="profile__info_subtitle", # keep it (a18n)
subtitle="profile_info_subtitle", # keep it (a18n)
),
pairs_widget(
pairs=user_profile,
columns={"pc": 6, "tablet": 12, "mobile": 12},
),
pairs_widget(pairs=user_profile),
],
}
@ -86,16 +90,34 @@ def profile_account_form(email: str) -> dict:
subtitle_widget(
subtitle="profile_account_subtitle", # keep it (a18n)
),
button_group_widget(
buttons=[
button_widget(
text="profile_account_tab_email",
display=["account", 0],
size="normal",
color="back",
),
button_widget(
text="profile_account_tab_password",
display=["account", 1],
size="normal",
color="back",
),
],
buttonGroupClass="bouton-group-card mt-2",
),
regular_widget(
display=["account", 0],
title="profile_account_form_email_title",
maxWidthScreen="xs",
endpoint="/totp-enable",
endpoint="/edit",
method="POST",
fields=[
get_fields_from_field(
input_widget(
id="profile-email",
name="email",
type="password",
label="profile_email", # keep it (a18n)
value=email,
pattern="", # add your pattern if needed
@ -111,63 +133,16 @@ def profile_account_form(email: str) -> dict:
),
get_fields_from_field(
input_widget(
id="profile-password",
name="new_password",
label="profile_password", # keep it (a18n)
value="",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_password_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "exclamation",
"color": "yellow-darker",
"text": "profile_password_warning_desc",
},
{
"iconName": "info",
"text": "profile_password_desc",
},
],
)
),
get_fields_from_field(
input_widget(
id="profile-password-confirm",
name="new_password_confirm",
label="profile_password_confirm", # keep it (a18n)
value="",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_password_confirm_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "exclamation",
"color": "yellow-darker",
"text": "profile_password_confirm_warning_desc",
},
{
"iconName": "info",
"text": "profile_password_confirm_desc",
},
],
)
),
get_fields_from_field(
input_widget(
id="profile-password",
id="profile-password-email",
name="current_password",
label="profile_current_password", # keep it (a18n)
value="",
required=True,
type="password",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_current_password_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "exclamation",
"color": "yellow-darker",
"text": "profile_current_password_warning_desc",
},
{
"iconName": "info",
"text": "profile_current_password_desc",
@ -179,13 +154,87 @@ def profile_account_form(email: str) -> dict:
buttons=[
button_widget(
id="profile-account-submit",
text="action_update",
iconName="plus",
text="action_edit",
iconName="pen",
iconColor="white",
color="success",
color="edit",
size="normal",
type="submit",
)
],
),
regular_widget(
display=["account", 1],
title="profile_account_form_password_title",
maxWidthScreen="xs",
endpoint="/edit",
method="POST",
fields=[
get_fields_from_field(
input_widget(
id="profile-password-account",
name="new_password",
label="profile_new_password", # keep it (a18n)
value="",
type="password",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_new_password_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "info",
"text": "profile_new_password_desc",
},
],
)
),
get_fields_from_field(
input_widget(
id="profile-password-confirm",
name="new_password_confirm",
type="password",
label="profile_new_password_confirm", # keep it (a18n)
value="",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_new_password_confirm_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "info",
"text": "profile_new_password_confirm_desc",
},
],
)
),
get_fields_from_field(
input_widget(
id="profile-password-update",
name="current_password",
label="profile_current_password", # keep it (a18n)
value="",
type="password",
required=True,
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_current_password_placeholder", # keep it (a18n)
popovers=[
{
"iconName": "info",
"text": "profile_current_password_desc",
},
],
)
),
],
buttons=[
button_widget(
id="profile-account-submit",
text="action_edit",
iconName="pen",
iconColor="white",
color="edit",
size="normal",
type="submit",
containerClass="flex justify-center",
)
],
),
@ -200,11 +249,11 @@ def totp_enable_form(
recovery_widgets = []
if is_recovery_refreshed and len(totp_recovery_codes) > 0:
if is_recovery_refreshed and (totp_recovery_codes is not None and (isinstance(totp_recovery_codes, list) and len(totp_recovery_codes) > 0)):
recovery_widgets.append(pairs_widget(pairs=totp_recovery_codes))
if is_recovery_refreshed and totp_recovery_codes is None or len(totp_recovery_codes) == 0:
recovery_widgets.append(text_widget(text="profile_recovery_codes_refresh_but_not_found", color="error", iconName="", iconColor=""))
if is_recovery_refreshed and (totp_recovery_codes is None or (isinstance(totp_recovery_codes, list) and len(totp_recovery_codes) == 0)):
recovery_widgets.append(text_widget(text="profile_recovery_codes_refresh_but_not_found", iconName="", iconColor="error"))
recovery_widgets.append(
button_group_widget(
@ -238,25 +287,25 @@ def totp_enable_form(
subtitle="profile_totp_subtitle", # keep it (a18n)
),
text_widget(
text="profile_totp_enable", # keep it (a18n)
icon="check",
textClass="flex justify-center",
text="profile_totp_enable_state", # keep it (a18n)
iconName="check",
iconColor="success",
textIconContainerClass="col-span-12 flex justify-center items-center mt-2",
),
# totp secret (type password), totp code, password
regular_widget(
title="profile_totp_disable_title",
subtitle="profile_totp_disable_subtitle",
title="profile_totp_disable_form_title",
maxWidthScreen="xs",
endpoint="/totp-disable",
method="POST",
fields=[
get_fields_from_field(
input_widget(
id="profile-totp-code",
id="profile-totp-code-enable",
name="totp_code",
label="profile_totp_code", # keep it (a18n)
value="",
isClipboard=True,
required=True,
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_totp_code_placeholder", # keep it (a18n)
@ -270,11 +319,11 @@ def totp_enable_form(
),
get_fields_from_field(
input_widget(
id="profile-totp-code",
type="password",
id="profile-totp-current-password",
name="current_password",
label="profile_current_password", # keep it (a18n)
value="",
isClipboard=True,
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_current_password_placeholder", # keep it (a18n)
@ -290,9 +339,9 @@ def totp_enable_form(
buttons=[
button_widget(
id="profile-disable-submit",
text="action_enable",
iconName="plus",
iconColor="check",
text="action_disable",
iconName="cross",
iconColor="white",
color="success",
size="normal",
type="submit",
@ -316,9 +365,10 @@ def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
subtitle="profile_totp_subtitle", # keep it (a18n)
),
text_widget(
text="profile_totp_disable", # keep it (a18n)
icon="cross",
textClass="flex justify-center",
text="profile_totp_disable_state", # keep it (a18n)
iconName="uncheck",
iconColor="error",
textIconContainerClass="col-span-12 flex justify-center items-center mt-2",
),
image_widget(
src=totp_img,
@ -326,6 +376,7 @@ def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
),
# totp secret (type password), totp code, password
regular_widget(
title="profile_totp_enable_form_title",
maxWidthScreen="xs",
endpoint="/edit",
method="POST",
@ -351,11 +402,11 @@ def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
),
get_fields_from_field(
input_widget(
id="profile-totp-code",
id="profile-totp-code-disabled",
name="totp_code",
label="profile_totp_code", # keep it (a18n)
value="",
isClipboard=True,
required=True,
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_totp_code_placeholder", # keep it (a18n)
@ -373,7 +424,8 @@ def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
name="current_password",
label="profile_current_password", # keep it (a18n)
value="",
isClipboard=True,
required=True,
type="password",
pattern="", # add your pattern if needed
columns={"pc": 12, "tablet": 12, "mobile": 12},
placeholder="profile_current_password_placeholder", # keep it (a18n)
@ -391,7 +443,7 @@ def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
id="profile-disable-submit",
text="action_enable",
iconName="plus",
iconColor="check",
iconColor="white",
color="success",
size="normal",
type="submit",
@ -451,7 +503,7 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def profile_builder(user: Optional[dict] = None) -> list:
if user is None or len(user) == 0:
if user is None or (isinstance(user, list) and len(user) == 0):
return [fallback_message("profile_user_not_found")]
totp_data = user.get("totp", None)

View file

@ -41,7 +41,7 @@ def reports_filters(reasons: Optional[list] = None, countries: Optional[list] =
},
]
if reasons is not None and len(reasons) >= 2:
if reasons is not None and (isinstance(reasons, list) and len(reasons) >= 2):
reports_filters.append(
{
"type": "=",
@ -66,7 +66,7 @@ def reports_filters(reasons: Optional[list] = None, countries: Optional[list] =
}
)
if countries is not None and len(countries) >= 2:
if countries is not None and (isinstance(countries, list) and len(countries) >= 2):
reports_filters.append(
{
"type": "=",
@ -91,7 +91,7 @@ def reports_filters(reasons: Optional[list] = None, countries: Optional[list] =
}
)
if methods is not None and len(methods) >= 2:
if methods is not None and (isinstance(methods, list) and len(methods) >= 2):
reports_filters.append(
{
"type": "=",
@ -116,7 +116,7 @@ def reports_filters(reasons: Optional[list] = None, countries: Optional[list] =
}
)
if codes is not None and len(codes) >= 2:
if codes is not None and (isinstance(codes, list) and len(codes) >= 2):
reports_filters.append(
{
"type": "=",
@ -185,7 +185,7 @@ def reports_builder(
reports: list, reasons: Optional[list] = None, countries: Optional[list] = None, methods: Optional[list] = None, codes: Optional[list] = None
) -> str:
if reports is None or len(reports) == 0:
if reports is None or (isinstance(reports, list) and len(reports) == 0):
return [fallback_message("reports_not_found")]
reports_items = [report_item(**report, id=index) for index, report in enumerate(reports)]

View file

@ -60,7 +60,7 @@ def users_filter(roles: Optional[list] = None, totp_states: Optional[list] = Non
}
]
if roles is not None and len(roles) >= 2:
if roles is not None and (isinstance(roles, list) and len(roles) >= 2):
filters.append(
{
"type": "=",
@ -85,7 +85,7 @@ def users_filter(roles: Optional[list] = None, totp_states: Optional[list] = Non
}
)
if totp_states is not None and len(totp_states) >= 2:
if totp_states is not None and (isinstance(totp_states, list) and len(totp_states) >= 2):
filters.append(
{
"type": "=",
@ -399,14 +399,14 @@ def user_management_builder(
users: Optional[list] = None, roles: Optional[list] = None, roles_form: Optional[list] = None, totp_states: Optional[list] = None
) -> list:
if roles is None or len(roles) == 0 or roles_form is None or len(roles_form) == 0:
if roles is None or (isinstance(roles, list) and len(roles) == 0) or roles_form is None or (isinstance(roles_form, list) and len(roles_form) == 0):
return [fallback_message("user_management_missing_roles")]
users_items = []
users_forms = []
users_forms.append(user_management_form(is_new=True, display_index=1, role="", username="", roles=roles_form))
if users is None or len(users) == 0:
if users is None or (isinstance(users, list) and len(users) == 0):
return [
# Tabs is button group with display value and a size tab inside a tabs container
user_management_tabs(),

View file

@ -14,7 +14,8 @@ def advanced_widget(
endpoint: str = "",
method: str = "POST",
oldServerName: str = "",
columns: dict = {"pc":"12","tablet":"12","mobile":"12"}
columns: dict = {"pc":"12","tablet":"12","mobile":"12"},
display: Optional[list] = None
):
"""
This component is used to create a complete advanced form with plugin selection.
@ -28,6 +29,7 @@ def advanced_widget(
- `method` **String** Http method to be used on form submit. (optional, default `"POST"`)
- `oldServerName` **String** Old server name. This is a server name before any changes. (optional, default `""`)
- `columns` **Object** Columns object. (optional, default `{"pc":"12","tablet":"12","mobile":"12"}`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
EXAMPLE
@ -66,7 +68,7 @@ def advanced_widget(
# List of params that will be add only if not default value
list_params = [("containerClass", containerClass, ""),("operation", operation, "edit"),("endpoint", endpoint, ""),("method", method, "POST"),("oldServerName", oldServerName, ""),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"})]
list_params = [("containerClass", containerClass, ""),("operation", operation, "edit"),("endpoint", endpoint, ""),("method", method, "POST"),("oldServerName", oldServerName, ""),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"}),("display", display, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -139,7 +141,7 @@ def button_widget(
def button_group_widget(
buttons: list,
boutonGroupClass: str = ""
buttonGroupClass: str = ""
):
"""
This component allow to display multiple buttons in the same row using flex.
@ -149,7 +151,7 @@ def button_group_widget(
PARAMETERS
- `buttons` **Array** List of buttons to display. Button component is used.
- `boutonGroupClass` **String** Additional class for the flex container (optional, default `""`)
- `buttonGroupClass` **String** Additional class for the flex container (optional, default `""`)
EXAMPLE
@ -188,7 +190,7 @@ def button_group_widget(
# List of params that will be add only if not default value
list_params = [("boutonGroupClass", boutonGroupClass, "")]
list_params = [("buttonGroupClass", buttonGroupClass, "")]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -635,7 +637,8 @@ def easy_widget(
endpoint: str = "",
method: str = "POST",
oldServerName: str = "",
columns: dict = {"pc":"12","tablet":"12","mobile":"12"}
columns: dict = {"pc":"12","tablet":"12","mobile":"12"},
display: Optional[list] = None
):
"""
This component is used to create a complete easy form with plugin selection.
@ -649,6 +652,7 @@ def easy_widget(
- `method` **String** Http method to be used on form submit. (optional, default `"POST"`)
- `oldServerName` **String** Old server name. This is a server name before any changes. (optional, default `""`)
- `columns` **Object** Columns object. (optional, default `{"pc":"12","tablet":"12","mobile":"12"}`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
EXAMPLE
@ -687,7 +691,7 @@ def easy_widget(
# List of params that will be add only if not default value
list_params = [("containerClass", containerClass, ""),("operation", operation, "edit"),("endpoint", endpoint, ""),("method", method, "POST"),("oldServerName", oldServerName, ""),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"})]
list_params = [("containerClass", containerClass, ""),("operation", operation, "edit"),("endpoint", endpoint, ""),("method", method, "POST"),("oldServerName", oldServerName, ""),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"}),("display", display, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -1069,6 +1073,47 @@ def icons_widget(
return { "type" : "Icons", "data" : data }
def image_widget(
src: str,
alt: str = "",
imgClass: str = "",
imgContainerClass: str = "",
attrs: Optional[dict] = None
):
"""
This component is used for regular paragraph.
PARAMETERS
- `src` **String** The src value of the image.
- `alt` **String** The alt value of the image. Can be a translation key or by default raw text. (optional, default `""`)
- `imgClass` **String** (optional, default `""`)
- `imgContainerClass` **String** (optional, default `""`)
- `attrs` **Object** List of attributes to add to the image. (optional, default `{}`)
EXAMPLE
{
src: "https://via.placeholder.com/150",
alt: "My image",
attrs: { id: "paragraph" },
}
"""
data = {
"src" : src,
}
# List of params that will be add only if not default value
list_params = [("alt", alt, ""),("imgClass", imgClass, ""),("imgContainerClass", imgContainerClass, ""),("attrs", attrs, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
return { "type" : "Image", "data" : data }
def input_widget(
label: str,
name: str,
@ -1569,7 +1614,8 @@ def raw_widget(
containerClass: str = "",
endpoint: str = "",
method: str = "POST",
columns: dict = {"pc":"12","tablet":"12","mobile":"12"}
columns: dict = {"pc":"12","tablet":"12","mobile":"12"},
display: Optional[list] = None
):
"""
This component is used to create a complete raw form with settings as JSON format.
@ -1583,6 +1629,7 @@ def raw_widget(
- `endpoint` **String** Form endpoint. Case none, will be ignored. (optional, default `""`)
- `method` **String** Http method to be used on form submit. (optional, default `"POST"`)
- `columns` **Object** Columns object. (optional, default `{"pc":"12","tablet":"12","mobile":"12"}`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
EXAMPLE
@ -1602,7 +1649,7 @@ def raw_widget(
# List of params that will be add only if not default value
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, ""),("containerClass", containerClass, ""),("endpoint", endpoint, ""),("method", method, "POST"),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"})]
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, ""),("containerClass", containerClass, ""),("endpoint", endpoint, ""),("method", method, "POST"),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"}),("display", display, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -1612,9 +1659,12 @@ def raw_widget(
def regular_widget(
fields: dict,
buttons: dict,
title: str = "",
subtitle: str = "",
containerClass: str = "",
endpoint: str = "",
method: str = "POST",
display: Optional[list] = None,
maxWidthScreen: str = "lg",
columns: dict = {"pc":"12","tablet":"12","mobile":"12"}
):
@ -1623,11 +1673,14 @@ def regular_widget(
PARAMETERS
- `title` **String** Form title (optional, default `""`)
- `subtitle` **String** Form subtitle (optional, default `""`)
- `fields` **Object** List of Fields component to display
- `buttons` **Object** We need to send a regular ButtonGroup buttons prop
- `containerClass` **String** Container additional class (optional, default `""`)
- `endpoint` **String** Form endpoint. Case none, will be ignored. (optional, default `""`)
- `method` **String** Http method to be used on form submit. (optional, default `"POST"`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
- `maxWidthScreen` **String** Max screen width for the settings based on the breakpoint (xs, sm, md, lg, xl, 2xl) (optional, default `"lg"`)
- `columns` **Object** Columns object. (optional, default `{"pc":"12","tablet":"12","mobile":"12"}`)
@ -1667,7 +1720,7 @@ def regular_widget(
# List of params that will be add only if not default value
list_params = [("containerClass", containerClass, ""),("endpoint", endpoint, ""),("method", method, "POST"),("maxWidthScreen", maxWidthScreen, "lg"),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"})]
list_params = [("title", title, ""),("subtitle", subtitle, ""),("containerClass", containerClass, ""),("endpoint", endpoint, ""),("method", method, "POST"),("display", display, None),("maxWidthScreen", maxWidthScreen, "lg"),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"})]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -2009,6 +2062,7 @@ def tabulator_widget(
itemsBeforePagination: int = 10,
paginationSize: int = 10,
paginationInitialPage: int = 1,
paginationButtonCount: int = 3,
paginationSizeSelector: list = [10,25,50,100]
):
"""
@ -2037,6 +2091,7 @@ def tabulator_widget(
- `itemsBeforePagination` **Number** Hide pagination unless number is reach. (optional, default `10`)
- `paginationSize` **Number** Number of items per page (optional, default `10`)
- `paginationInitialPage` **Number** Initial page (optional, default `1`)
- `paginationButtonCount` **Number** Available pagination buttons (optional, default `3`)
- `paginationSizeSelector` **Array** Select number of items per page (optional, default `[10,25,50,100]`)
EXAMPLE
@ -2067,7 +2122,7 @@ def tabulator_widget(
# List of params that will be add only if not default value
list_params = [("isStriped", isStriped, True),("filters", filters, None),("actionsButtons", actionsButtons, None),("layout", layout, "fitDataTable"),("rowHeight", rowHeight, 0),("colMinWidth", colMinWidth, 150),("colMaxWidth", colMaxWidth, 0),("isPagination", isPagination, True),("itemsBeforePagination", itemsBeforePagination, 10),("paginationSize", paginationSize, 10),("paginationInitialPage", paginationInitialPage, 1),("paginationSizeSelector", paginationSizeSelector, [10,25,50,100])]
list_params = [("isStriped", isStriped, True),("filters", filters, None),("actionsButtons", actionsButtons, None),("layout", layout, "fitDataTable"),("rowHeight", rowHeight, 0),("colMinWidth", colMinWidth, 150),("colMaxWidth", colMaxWidth, 0),("isPagination", isPagination, True),("itemsBeforePagination", itemsBeforePagination, 10),("paginationSize", paginationSize, 10),("paginationInitialPage", paginationInitialPage, 1),("paginationButtonCount", paginationButtonCount, 3),("paginationSizeSelector", paginationSizeSelector, [10,25,50,100])]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -2077,7 +2132,8 @@ def tabulator_widget(
def templates_widget(
templates: dict,
operation: str = "edit",
oldServerName: str = ""
oldServerName: str = "",
display: Optional[list] = None
):
"""
This component is used to create a complete settings form with all modes (advanced, raw, easy).
@ -2087,6 +2143,7 @@ def templates_widget(
- `templates` **Object** List of advanced templates that contains settings. Must be a dict with mode as key, then the template name as key with a list of data (different for each modes).
- `operation` **String** Operation type (edit, new, delete). (optional, default `"edit"`)
- `oldServerName` **String** Old server name. This is a server name before any changes. (optional, default `""`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
EXAMPLE
@ -2109,7 +2166,7 @@ def templates_widget(
# List of params that will be add only if not default value
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, "")]
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, ""),("display", display, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -2119,7 +2176,10 @@ def templates_widget(
def text_widget(
text: str,
textClass: str = "",
textIconContainerClass: str = "col-span-12 flex justify-center items-center",
color: str = "",
iconName: str = "",
iconColor: str = "",
bold: bool = False,
uppercase: bool = False,
tag: str = "p",
@ -2133,7 +2193,10 @@ def text_widget(
- `text` **String** The text value. Can be a translation key or by default raw text.
- `textClass` **String** Style of text. Can be replace by any class starting by 'text-' like 'text-stat'. (optional, default `""`)
- `textIconContainerClass` **String** Case we have icon with text, we wrap the text on a container with the icon. We can add a class to this container. (optional, default `"col-span-12 flex justify-center items-center"`)
- `color` **String** The color of the text between error, success, warning, info or tailwind color (optional, default `""`)
- `iconName` **String** The name of the icon to display before the text. (optional, default `""`)
- `iconColor` **String** The color of the icon. (optional, default `""`)
- `bold` **Boolean** If the text should be bold or not. (optional, default `false`)
- `uppercase` **Boolean** If the text should be uppercase or not. (optional, default `false`)
- `tag` **String** The tag of the text. Can be p, span, div, h1, h2, h3, h4, h5, h6 (optional, default `"p"`)
@ -2156,7 +2219,7 @@ def text_widget(
# List of params that will be add only if not default value
list_params = [("textClass", textClass, ""),("color", color, ""),("bold", bold, False),("uppercase", uppercase, False),("tag", tag, "p"),("icon", icon, False),("attrs", attrs, None)]
list_params = [("textClass", textClass, ""),("textIconContainerClass", textIconContainerClass, "col-span-12 flex justify-center items-center"),("color", color, ""),("iconName", iconName, ""),("iconColor", iconColor, ""),("bold", bold, False),("uppercase", uppercase, False),("tag", tag, "p"),("icon", icon, False),("attrs", attrs, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -2212,6 +2275,8 @@ def title_widget(
def unmatch_widget(
text: str,
iconName: str = "search",
iconColor: str = "",
unmatchClass: str = ""
):
"""
@ -2221,6 +2286,8 @@ def unmatch_widget(
PARAMETERS
- `text` **String** The text to display
- `iconName` **String** The icon to display (optional, default `"search"`)
- `iconColor` **String** The color of the icon (optional, default `""`)
- `unmatchClass` **String** The class to apply to the message. If not provided, the class will be based on the parent component. (optional, default `""`)
EXAMPLE
@ -2237,7 +2304,7 @@ def unmatch_widget(
# List of params that will be add only if not default value
list_params = [("unmatchClass", unmatchClass, "")]
list_params = [("iconName", iconName, "search"),("iconColor", iconColor, ""),("unmatchClass", unmatchClass, "")]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])

View file

@ -14,21 +14,19 @@ from pages.profile import profile_builder
# - last_update (last time update user info)
user = {
"profile": [
{
"profile_username": "username",
"profile_email": "email",
"profile_created_method": "created_method",
"role": "admin",
"role_description": "role_description",
"permissions": "read, write, admin",
"creation_date": "date",
"last_update": "date",
}
{"key": "profile_username", "value": "username"},
{"key": "profile_email", "value": "email"},
{"key": "profile_created_method", "value": "created_method"},
{"key": "profile_role", "value": "admin"},
{"key": "profile_role_description", "value": "role_description"},
{"key": "profile_permissions", "value": "read, write, admin"},
{"key": "profile_creation_date", "value": "date"},
{"key": "profile_last_update", "value": "date"},
],
"totp": {
"is_totp": False,
"totp_image": "", # base 64 that will be add in an img tag src
"totp_recovery_codes": [{"0": "code_0"}, {"1": "code_1"}, {"2": "code_2"}],
"totp_recovery_codes": [{"key": "0", "value": "code_0"}, {"key": "1", "value": "code_1"}, {"key": "2", "value": "code_2"}],
"is_recovery_refreshed": False,
"totp_secret": "totp_secret",
},

View file

@ -0,0 +1,153 @@
<script setup>
// Containers
import Alert from "@components/Widget/Alert.vue";
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Cell from "@components/Widget/Cell.vue";
import Container from "@components/Widget/Container.vue";
import Filter from "@components/Widget/Filter.vue";
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Icons from "@components/Widget/Icons.vue";
import Modal from "@components/Widget/Modal.vue";
import Popover from "@components/Widget/Popover.vue";
import PopoverGroup from "@components/Widget/PopoverGroup.vue";
import Stat from "@components/Widget/Stat.vue";
import Status from "@components/Widget/Status.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Table from "@components/Widget/Table.vue";
import Tabulator from "@components/Widget/Tabulator.vue";
import Text from "@components/Widget/Text.vue";
import Title from "@components/Widget/Title.vue";
import Regular from "@components/Form/Regular.vue";
import Advanced from "@components/Form/Advanced.vue";
import Raw from "@components/Form/Raw.vue";
import Easy from "@components/Form/Easy.vue";
import Fields from "@components/Form/Fields.vue";
import Pairs from "@components/List/Pairs.vue";
import Details from "@components/List/Details.vue";
import Templates from "@components/Form/Templates.vue";
import Unmatch from "@components/Message/Unmatch.vue";
import { useEqualStr } from "@utils/global.js";
/**
* @name Builder/Bans.vue
* @description This component is lightweight builder containing only the necessary components to create the bans page.
* @example
* [
* {
* type: "card",
* gridLayoutClass: "transparent",
* widgets: [
* { type: "Unmatch",
* data: { text: "bans_not_found" }
* },
* ],
* },
* ];
* @param {array} builder - Array of containers and widgets
*/
const props = defineProps({
builder: {
type: Array,
required: true,
},
});
</script>
<template>
<!-- top level grid (layout) -->
<GridLayout
v-for="(container, index) in props.builder"
:key="index"
:gridLayoutClass="container.containerClass"
:maxWidthScreen="container.maxWidthScreen"
:type="container.type"
:title="container.title"
:link="container.link"
:columns="container.containerColumns"
:id="container.id"
:display="container.display"
>
<!-- widget grid -->
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<Subtitle
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Unmatch
v-if="useEqualStr(widget.type, 'Unmatch')"
v-bind="widget.data"
/>
<Tabulator
v-if="useEqualStr(widget.type, 'Tabulator')"
v-bind="widget.data"
/>
<Button
v-if="useEqualStr(widget.type, 'Button')"
v-bind="widget.data"
/>
<Regular
v-if="useEqualStr(widget.type, 'Regular')"
v-bind="widget.data"
/>
<ButtonGroup
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
<
<Alert v-if="useEqualStr(widget.type, 'Alert')" v-bind="widget.data" />
<Cell v-if="useEqualStr(widget.type, 'Cell')" v-bind="widget.data" />
<Container
v-if="useEqualStr(widget.type, 'Container')"
v-bind="widget.data"
/>
<Filter
v-if="useEqualStr(widget.type, 'Filter')"
v-bind="widget.data"
/>
<Icons v-if="useEqualStr(widget.type, 'Icons')" v-bind="widget.data" />
<Modal v-if="useEqualStr(widget.type, 'Modal')" v-bind="widget.data" />
<Popover
v-if="useEqualStr(widget.type, 'Popover')"
v-bind="widget.data"
/>
<PopoverGroup
v-if="useEqualStr(widget.type, 'PopoverGroup')"
v-bind="widget.data"
/>
<Stat v-if="useEqualStr(widget.type, 'Stat')" v-bind="widget.data" />
<Status
v-if="useEqualStr(widget.type, 'Status')"
v-bind="widget.data"
/>
<Table v-if="useEqualStr(widget.type, 'Table')" v-bind="widget.data" />
<Text v-if="useEqualStr(widget.type, 'Text')" v-bind="widget.data" />
<Advanced
v-if="useEqualStr(widget.type, 'Advanced')"
v-bind="widget.data"
/>
<Raw v-if="useEqualStr(widget.type, 'Raw')" v-bind="widget.data" />
<Easy v-if="useEqualStr(widget.type, 'Easy')" v-bind="widget.data" />
<Fields
v-if="useEqualStr(widget.type, 'Fields')"
v-bind="widget.data"
/>
<Pairs v-if="useEqualStr(widget.type, 'Pairs')" v-bind="widget.data" />
<Details
v-if="useEqualStr(widget.type, 'Details')"
v-bind="widget.data"
/>
<Templates
v-if="useEqualStr(widget.type, 'Templates')"
v-bind="widget.data"
/>
</template>
</Grid>
</GridLayout>
</template>

View file

@ -0,0 +1,81 @@
<script setup>
// Containers
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Tabulator from "@components/Widget/Tabulator.vue";
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Regular from "@components/Form/Regular.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Text from "@components/Widget/Text.vue";
import Image from "@components/Widget/Image.vue";
import Unmatch from "@components/Message/Unmatch.vue";
import Pairs from "@components/List/Pairs.vue";
import { useEqualStr } from "@utils/global.js";
/**
* @name Builder/Profile.vue
* @description This component is lightweight builder containing only the necessary components to create the profile page.
** @example
** @param {array} builder - Array of containers and widgets
*/
const props = defineProps({
builder: {
type: Array,
required: true,
},
});
</script>
<template>
<!-- top level grid (layout) -->
<GridLayout
v-for="(container, index) in props.builder"
:key="index"
:gridLayoutClass="container.containerClass"
:maxWidthScreen="container.maxWidthScreen"
:type="container.type"
:title="container.title"
:link="container.link"
:columns="container.containerColumns"
:id="container.id"
:display="container.display"
>
<!-- widget grid -->
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<Image v-if="useEqualStr(widget.type, 'Image')" v-bind="widget.data" />
<Text v-if="useEqualStr(widget.type, 'Text')" v-bind="widget.data" />
<Subtitle
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Pairs v-if="useEqualStr(widget.type, 'Pairs')" v-bind="widget.data" />
<Unmatch
v-if="useEqualStr(widget.type, 'Unmatch')"
v-bind="widget.data"
/>
<Tabulator
v-if="useEqualStr(widget.type, 'Tabulator')"
v-bind="widget.data"
/>
<Button
v-if="useEqualStr(widget.type, 'Button')"
v-bind="widget.data"
/>
<Regular
v-if="useEqualStr(widget.type, 'Regular')"
v-bind="widget.data"
/>
<ButtonGroup
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
</template>
</Grid>
</GridLayout>
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { defineProps, reactive, onMounted, onUnmounted, computed } from "vue";
import { defineProps, reactive, onMounted, onUnmounted, watch } from "vue";
import Unmatch from "@components/Message/Unmatch.vue";
import Container from "@components/Widget/Container.vue";
import Fields from "@components/Form/Fields.vue";
@ -14,6 +14,7 @@ import { plugin_types } from "@utils/variables";
import { useAdvancedForm } from "@store/form.js";
import { useCheckPluginsValidity } from "@utils/global.js";
import { v4 as uuidv4 } from "uuid";
import { useDisplayStore } from "@store/global.js";
/**
* @name Form/Advanced.vue
@ -52,6 +53,7 @@ import { v4 as uuidv4 } from "uuid";
* @param {String} [method="POST"] - Http method to be used on form submit.
* @param {String} [oldServerName=""] - Old server name. This is a server name before any changes.
* @param {Object} [columns={ "pc": "12", "tablet": "12", "mobile": "12" }] - Columns object.
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
*/
const advancedForm = useAdvancedForm();
@ -93,8 +95,15 @@ const props = defineProps({
required: false,
default: { pc: 12, tablet: 12, mobile: 12 },
},
display: {
type: Array,
required: false,
default: [],
},
});
const displayStore = useDisplayStore();
const data = reactive({
currPlugin: "",
plugins: [],
@ -103,8 +112,31 @@ const data = reactive({
isReqErr: false,
settingErr: "",
pluginErr: "",
isDisplay: props.display.length
? displayStore.isCurrentDisplay(props.display[0], props.display[1])
: true,
});
/**
* @name checkDisplay
* @description Check if the current display value is matching the display store value.
* @returns {Void}
*/
function checkDisplay() {
if (!props.display.length) return;
data.isDisplay = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
}
// Case we have set a display group name and component id, the component id must match the current display id for the same group name to be displayed.
if (props.display.length) {
watch(displayStore.display, (val) => {
checkDisplay();
});
}
const comboboxPlugin = {
id: `advanced-combobox-${uuidv4()}`,
name: `advanced-combobox-${uuidv4()}`,
@ -313,6 +345,7 @@ onUnmounted(() => {
<template>
<Container
v-if="data.isDisplay"
data-advanced-form
data-is="form"
:tag="'form'"

View file

@ -11,13 +11,13 @@ import Container from "@components/Widget/Container.vue";
import Fields from "@components/Form/Fields.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Text from "@components/Widget/Text.vue";
import GroupMultiple from "@components/Forms/Group/Multiple.vue";
import Unmatch from "@components/Message/Unmatch.vue";
import { v4 as uuidv4 } from "uuid";
import { useCheckPluginsValidity } from "@utils/global.js";
import { useEasyForm } from "@store/form.js";
import { useDisplayStore } from "@store/global.js";
/**
* @name Form/Easy.vue
@ -56,6 +56,7 @@ import { useEasyForm } from "@store/form.js";
* @param {String} [method="POST"] - Http method to be used on form submit.
* @param {String} [oldServerName=""] - Old server name. This is a server name before any changes.
* @param {Object} [columns={ "pc": "12", "tablet": "12", "mobile": "12" }] - Columns object.
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
*/
const easyForm = useEasyForm();
@ -97,8 +98,15 @@ const props = defineProps({
required: false,
default: { pc: 12, tablet: 12, mobile: 12 },
},
display: {
type: Array,
required: false,
default: [],
},
});
const displayStore = useDisplayStore();
const data = reactive({
base: JSON.parse(JSON.stringify(props.template)),
currStep: 0,
@ -109,8 +117,30 @@ const data = reactive({
isReqErr: false,
settingErr: "",
stepErr: "",
isDisplay: props.display.length
? displayStore.isCurrentDisplay(props.display[0], props.display[1])
: true,
});
/**
* @name checkDisplay
* @description Check if the current display value is matching the display store value.
* @returns {Void}
*/
function checkDisplay() {
if (!props.display.length) return;
data.isDisplay = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
}
// Case we have set a display group name and component id, the component id must match the current display id for the same group name to be displayed.
if (props.display.length) {
watch(displayStore.display, (val) => {
checkDisplay();
});
}
watch(
() => props.template,
() => {
@ -202,6 +232,7 @@ onUnmounted(() => {
<template>
<Container
v-if="data.isDisplay"
data-easy-form
data-is="form"
:tag="'form'"

View file

@ -8,6 +8,7 @@ import Button from "@components/Widget/Button.vue";
import Text from "@components/Widget/Text.vue";
import { v4 as uuidv4 } from "uuid";
import { useRawForm } from "@store/form.js";
import { useDisplayStore } from "@store/global.js";
/**
* @name Form/Raw.vue
@ -27,6 +28,7 @@ import { useRawForm } from "@store/form.js";
* @param {String} [endpoint=""] - Form endpoint. Case none, will be ignored.
* @param {String} [method="POST"] - Http method to be used on form submit.
* @param {Object} [columns={ "pc": "12", "tablet": "12", "mobile": "12" }] - Columns object.
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
*/
const rawForm = useRawForm();
@ -68,14 +70,44 @@ const props = defineProps({
required: false,
default: { pc: 12, tablet: 12, mobile: 12 },
},
display: {
type: Array,
required: false,
default: [],
},
});
const displayStore = useDisplayStore();
const data = reactive({
isValid: true,
// This will be use to unmount and remount the editor (create a new editor instance because it is vanilla js)
isRender: true,
isDisplay: props.display.length
? displayStore.isCurrentDisplay(props.display[0], props.display[1])
: true,
});
/**
* @name checkDisplay
* @description Check if the current display value is matching the display store value.
* @returns {Void}
*/
function checkDisplay() {
if (!props.display.length) return;
data.isDisplay = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
}
// Case we have set a display group name and component id, the component id must match the current display id for the same group name to be displayed.
if (props.display.length) {
watch(displayStore.display, (val) => {
checkDisplay();
});
}
watch(
() => props.template,
() => {
@ -205,7 +237,7 @@ onMounted(() => {
<template>
<Container
data-raw-
v-if="data.isDisplay"
data-is="form"
:tag="'form'"
method="POST"

View file

@ -1,9 +1,12 @@
<script setup>
import { defineProps, reactive, onMounted } from "vue";
import { defineProps, reactive, onMounted, watch } from "vue";
import Container from "@components/Widget/Container.vue";
import Fields from "@components/Form/Fields.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Text from "@components/Widget/Text.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import { useDisplayStore } from "@store/global.js";
/**
* @name Form/Regular.vue
@ -40,6 +43,7 @@ import Text from "@components/Widget/Text.vue";
* @param {String} [containerClass=""] - Container additional class
* @param {String} [endpoint=""] - Form endpoint. Case none, will be ignored.
* @param {String} [method="POST"] - Http method to be used on form submit.
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
* @param {String} [maxWidthScreen="lg"] - Max screen width for the settings based on the breakpoint (xs, sm, md, lg, xl, 2xl)
* @param {Object} [columns={ "pc": "12", "tablet": "12", "mobile": "12" }] - Columns object.
*/
@ -81,6 +85,11 @@ const props = defineProps({
required: false,
default: "POST",
},
display: {
type: Array,
required: false,
default: [],
},
containerClass: {
type: String,
required: false,
@ -93,6 +102,8 @@ const props = defineProps({
},
});
const displayStore = useDisplayStore();
const data = reactive({
base: props.fields,
endpoint: props.endpoint,
@ -101,8 +112,31 @@ const data = reactive({
settingErr: "",
pluginErr: "",
isUpdateData: false,
isDisplay: props.display.length
? displayStore.isCurrentDisplay(props.display[0], props.display[1])
: true,
});
/**
* @name checkDisplay
* @description Check if the current display value is matching the display store value.
* @returns {Void}
*/
function checkDisplay() {
if (!props.display.length) return;
data.isDisplay = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
}
// Case we have set a display group name and component id, the component id must match the current display id for the same group name to be displayed.
if (props.display.length) {
watch(displayStore.display, (val) => {
checkDisplay();
});
}
onMounted(() => {
data.endpoint =
window.location.origin + window.location.pathname + props.endpoint;
@ -111,7 +145,8 @@ onMounted(() => {
<template>
<Container
data-is="form"
v-if="data.isDisplay"
data-is="form-regular"
:tag="'form'"
:method="props.method"
:action="data.endpoint"
@ -119,14 +154,19 @@ onMounted(() => {
:containerClass="`form-regular-container`"
>
<div class="form-regular-wrap">
<Title v-if="props.title" type="card" :title="props.title" />
<Subtitle v-if="props.subtitle" type="card" :subtitle="props.subtitle" />
<div
:class="[
'layout-settings-regular',
`max-w-screen-${props.maxWidthScreen}`,
]"
>
<Title v-if="props.title" type="card" :title="props.title" />
<Subtitle
v-if="props.subtitle"
type="card"
:subtitle="props.subtitle"
/>
<template v-for="(field, key) in props.fields">
<Fields :setting="field.setting" />
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { reactive, defineProps, computed, onBeforeMount } from "vue";
import { reactive, defineProps, computed, onBeforeMount, watch } from "vue";
import Container from "@components/Widget/Container.vue";
import Grid from "@components/Widget/Grid.vue";
import Select from "@components/Forms/Field/Select.vue";
@ -8,6 +8,7 @@ import Advanced from "@components/Form/Advanced.vue";
import Raw from "@components/Form/Raw.vue";
import Easy from "@components/Form/Easy.vue";
import { v4 as uuidv4 } from "uuid";
import { useDisplayStore } from "@store/global.js";
/**
* @name Form/Templates.vue
@ -26,6 +27,7 @@ import { v4 as uuidv4 } from "uuid";
* @param {Object} templates - List of advanced templates that contains settings. Must be a dict with mode as key, then the template name as key with a list of data (different for each modes).
* @param {String} [operation="edit"] - Operation type (edit, new, delete).
* @param {String} [oldServerName=""] - Old server name. This is a server name before any changes.
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
*/
const props = defineProps({
@ -44,8 +46,15 @@ const props = defineProps({
required: false,
default: "",
},
display: {
type: Array,
required: false,
default: [],
},
});
const displayStore = useDisplayStore();
const comboboxTemplate = {
id: `combobox-template-${uuidv4()}`,
name: `combobox-template-${uuidv4()}`,
@ -88,8 +97,30 @@ const data = reactive({
if (!data.currModeName) return [];
return Object.keys(props.templates[data.currModeName]);
}),
isDisplay: props.display.length
? displayStore.isCurrentDisplay(props.display[0], props.display[1])
: true,
});
/**
* @name checkDisplay
* @description Check if the current display value is matching the display store value.
* @returns {Void}
*/
function checkDisplay() {
if (!props.display.length) return;
data.isDisplay = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
}
// Case we have set a display group name and component id, the component id must match the current display id for the same group name to be displayed.
if (props.display.length) {
watch(displayStore.display, (val) => {
checkDisplay();
});
}
/**
* @name getFirstTemplateName
* @description Get the first template name from the first mode.
@ -118,7 +149,7 @@ onBeforeMount(() => {
<template>
<Container
v-if="data.currModeName && data.currTemplateName"
v-if="data.isDisplay && data.currModeName && data.currTemplateName"
:containerClass="`col-span-12 w-full ${
data.templates.length > 1 ? '' : 'mb-3'
}`"

View file

@ -35,7 +35,7 @@ import { onMounted, reactive, ref } from "vue";
* ],
* }
* @param {Array} buttons - List of buttons to display. Button component is used.
* @param {String} [boutonGroupClass=""] - Additional class for the flex container
* @param {String} [buttonGroupClass=""] - Additional class for the flex container
*/
const props = defineProps({
@ -44,7 +44,7 @@ const props = defineProps({
required: true,
default: [],
},
boutonGroupClass: {
buttonGroupClass: {
type: String,
required: false,
default: "",
@ -59,7 +59,7 @@ const groupEl = ref(null);
onMounted(() => {
group.class =
props.boutonGroupClass || groupEl?.value?.closest("[data-is]")
props.buttonGroupClass || groupEl?.value?.closest("[data-is]")
? `button-group-${groupEl.value
.closest("[data-is]")
.getAttribute("data-is")}`
@ -79,7 +79,7 @@ onMounted(() => {
<template>
<div
ref="groupEl"
:class="[group.class, props.boutonGroupClass]"
:class="[group.class, props.buttonGroupClass]"
v-if="props.buttons.length > 0"
>
<Button

View file

@ -26,7 +26,7 @@ import { useDisplayStore } from "@store/global.js";
* @param {String} [gridLayoutClass="items-start"] - Additional class
* @param {Array} [display=[]] - Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef.
* @param {String} [tabId=contentIndex] - Case the container is converted to an anchor with a link, we can define the tabId, by default it is the contentIndex
* @param {String} [maxWidthScreen="2xl"] - Max screen width for the settings based on the breakpoint (xs, sm, md, lg, xl, 2xl, 3xl)
* @param {String} [maxWidthScreen="2xl"] - Max screen width for the settings based on the breakpoint (xs, sm, md, lg, xl, 2xl, 3xl)
*/
const props = defineProps({

View file

@ -0,0 +1,56 @@
<script setup>
import { onMounted, reactive, ref } from "vue";
/**
* @name Widget/Image.vue
* @description This component is used for regular paragraph.
* @example
* {
* src: "https://via.placeholder.com/150",
* alt: "My image",
* attrs: { id: "paragraph" },
* }
* @param {String} src - The src value of the image.
* @param {String} [alt=""] - The alt value of the image. Can be a translation key or by default raw text.
* @param {String} [imgClass=""]
* @param {String} [imgContainerClass=""]
* @param {Object} [attrs={}] - List of attributes to add to the image.
*/
const props = defineProps({
src: {
type: [String, Number],
required: true,
},
alt: {
type: String,
required: false,
default: "",
},
imgClass: {
type: String,
required: false,
default: "h-full w-full",
},
imgContainerClass: {
type: String,
required: false,
default: "flex justify-center items-center h-48 w-48",
},
attrs: {
type: Object,
required: false,
default: {},
},
});
</script>
<template>
<div v-if="props.src" :class="[props.imgContainerClass]">
<img
:alt="$t(props.alt, $t('dashboard_placeholder', props.alt))"
v-bind="props.attrs"
:class="[props.imgClass]"
/>
</div>
</template>

View file

@ -13,7 +13,10 @@ import { onMounted, reactive, ref } from "vue";
* }
* @param {String} text - The text value. Can be a translation key or by default raw text.
* @param {String} [textClass=""] - Style of text. Can be replace by any class starting by 'text-' like 'text-stat'.
* @param {String} [textIconContainerClass="col-span-12 flex justify-center items-center"] - Case we have icon with text, we wrap the text on a container with the icon. We can add a class to this container.
* @param {String} [color=""] - The color of the text between error, success, warning, info or tailwind color
* @param {String} [iconName=""] - The name of the icon to display before the text.
* @param {String} [iconColor=""] - The color of the icon.
* @param {Boolean} [bold=false] - If the text should be bold or not.
* @param {Boolean} [uppercase=false] - If the text should be uppercase or not.
* @param {String} [tag="p"] - The tag of the text. Can be p, span, div, h1, h2, h3, h4, h5, h6
@ -31,6 +34,11 @@ const props = defineProps({
required: false,
default: "",
},
textIconContainerClass: {
type: String,
required: false,
default: "col-span-12 flex justify-center items-center",
},
color: {
type: String,
required: false,
@ -51,10 +59,15 @@ const props = defineProps({
required: false,
default: "p",
},
icon: {
type: [Boolean, Object],
iconName: {
type: String,
required: false,
default: false,
default: "",
},
iconColor: {
type: String,
required: false,
default: "",
},
attrs: {
type: Object,
@ -75,15 +88,17 @@ onMounted(() => {
// Check if next sibling is a
const renderEl = textEl.value || textIconEl.value || null;
text.class =
props.textClass || renderEl.closest("[data-is]")
? `text-${renderEl.closest("[data-is]").getAttribute("data-is")}`
props.textClass || renderEl.closest("[data-is]:not([data-is='text'])")
? `text-${renderEl
.closest("[data-is]:not([data-is='text'])")
.getAttribute("data-is")}`
: "text-card";
});
</script>
<template>
<component
v-if="!props.icon"
v-if="!props.iconName"
:is="props.tag"
v-bind="props.attrs"
ref="textEl"
@ -98,8 +113,12 @@ onMounted(() => {
{{ $t(props.text, $t("dashboard_placeholder", props.text)) }}
</component>
<div :class="['flex justify-center items-center']" v-if="props.icon">
<Icons v-if="props.icon" v-bind="props.icon" />
<div
:class="[props.textIconContainerClass]"
v-if="props.iconName"
data-is="text"
>
<Icons v-bind="{ iconName: props.iconName, color: props.iconColor }" />
<component
ref="textIconEl"
:is="props.tag"

View file

@ -414,5 +414,53 @@
"configs_tab_add": "Add config",
"configs_missing_types": "Impossible to retrieve config types",
"configs_missing_services": "Impossible to retrieve services",
"configs_not_found": "No configs found"
"configs_not_found": "No configs found",
"profile_info_not_found": "Impossible to retrieve profile information",
"profile_info_title": "Information",
"profile_info_subtitle": "Details about your account.",
"profile_account_title": "Account",
"profile_account_subtitle": "Manage some settings.",
"profile_email": "Email",
"profile_email_placeholder": "john.doe{'@'}gmail.com",
"profile_email_desc": "Email is optional but recommended for password recovery or others features.",
"profile_new_password": "New password",
"profile_new_password_placeholder": "P{'@'}ssw0rd",
"profile_new_password_desc": "Optional. Leave empty with confirm password to unchanged.",
"profile_new_password_confirm": "New password confirm",
"profile_new_password_confirm_placeholder": "P{'@'}ssw0rd",
"profile_new_password_confirm_desc": "Optional. Leave empty with new password to unchanged.",
"profile_current_password": "Current password",
"profile_current_password_placeholder": "P{'@'}ssw0rd",
"profile_current_password_desc": "This ensure you are the owner of the account.",
"profile_recovery_codes_refresh_but_not_found": "Refresh done but no recovery codes found",
"profile_refresh_recovery_codes": "Refresh recovery codes",
"profile_totp_title": "TOTP",
"profile_totp_subtitle": "Two-factor authentication.",
"profile_totp_enable_state": "TOTP is currently enabled.",
"profile_totp_disable_form_title": "Disable TOTP",
"profile_totp_code": "TOTP code",
"profile_totp_code_placeholder": "123456",
"profile_totp_code_desc": "TOTP code is given by your authenticator app.",
"profile_totp_disable_state": "TOTP is currently disabled.",
"profile_totp_qr_code": "QR code to scan with your authenticator app.",
"profile_totp_secret": "Secret key",
"profile_totp_secret_placeholder": "secret",
"profile_totp_secret_desc": "Another way to add TOTP to your authenticator app.",
"profile_tab_profile": "Profile",
"profile_tab_account": "Account",
"profile_tab_totp": "TOTP",
"profile_user_not_found": "Impossible to retrieve user information",
"profile_totp_data_missing": "Impossible to retrieve TOTP data",
"profile_username": "Username",
"profile_created_method": "Created method",
"profile_role": "Role",
"profile_role_description": "Role description",
"profile_permissions": "Permissions",
"profile_creation_date": "Creation date",
"profile_last_update": "Last update",
"profile_totp_enable_form_title": "Enable TOTP",
"profile_account_tab_email": "Email",
"profile_account_tab_password": "Password",
"profile_account_form_email_title": "Update email",
"profile_account_form_password_title": "Update password"
}

View file

@ -0,0 +1,45 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderProfile from "@components/Builder/Profile.vue";
import { useGlobal } from "@utils/global.js";
import { useDisplayStore } from "@store/global.js";
/**
* @name Page/Profile.vue
* @description This component is the profile page.
This page displays current profile and allows to manage them.
We are using displayStore and setting ["main", 1] to display the profile list first.
*/
// Set default store
const displayStore = useDisplayStore();
displayStore.setDisplay("main", 0);
displayStore.setDisplay("account", 0);
const profile = reactive({
builder: "",
});
onBeforeMount(() => {
// Get builder data
const dataAtt = "data-server-builder";
const dataEl = document.querySelector(`[${dataAtt}]`);
const data =
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
: {};
profile.builder = data;
});
onMounted(() => {
// Set the page title
useGlobal();
});
</script>
<template>
<DashboardLayout>
<BuilderProfile v-if="profile.builder" :builder="profile.builder" />
</DashboardLayout>
</template>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import Profile from "./Profile.vue";
const pinia = createPinia();
createApp(Profile)
.use(pinia)
.use(getI18n(["dashboard", "action", "inp", "icons", "profile"]))
.mount("#app");

View file

@ -616,7 +616,7 @@ body {
}
.layout-settings-regular {
@apply py-4 gap-y-4 sm:gap-x-4 md:gap-x-6 col-span-12 grid grid-cols-12 w-full relative;
@apply pt-2 pb-4 gap-y-4 sm:gap-x-4 md:gap-x-6 col-span-12 grid grid-cols-12 w-full relative;
}
.layout-settings-table {
@ -644,6 +644,10 @@ body {
@apply col-span-12 w-full mx-2 my-4;
}
.msg-unmatch-form-regular {
@apply col-span-12 w-full mx-2 my-4;
}
.msg-unmatch-base {
@apply col-span-12 w-full mx-2 my-4;
}
@ -714,6 +718,10 @@ body {
@apply block w-8 h-8 pointer-events-none;
}
.icon-text {
@apply block w-6 h-6 pointer-events-none;
}
.icon-menu {
@apply h-6 w-6 relative pointer-events-none;
}
@ -722,10 +730,14 @@ body {
@apply h-6 w-6 relative pointer-events-none;
}
.icon-button {
.icon-form-regular {
@apply h-6 w-6 relative pointer-events-none;
}
.icon-button {
@apply h-6 w-6 relative pointer-events-none -translate-y-0.4;
}
.icon-table-content {
@apply h-6.5 w-6.5 relative pointer-events-none;
}
@ -1192,7 +1204,7 @@ body {
}
.text-card {
@apply col-span-12 text-lg mb-0 dark:text-gray-300 break-word;
@apply col-span-12 text-base mb-0 dark:text-gray-300 break-word;
}
.text-unmatch {
@ -1229,6 +1241,10 @@ body {
@apply col-span-12 flex justify-center items-center;
}
.button-group-form-regular {
@apply col-span-12 flex justify-center items-center;
}
.button-group-tabs {
@apply col-span-12 flex flex-col sm:flex-row justify-center items-center gap-2;
}
@ -1259,7 +1275,7 @@ body {
/* LIST COMPONENT */
.list-pairs-container {
@apply col-span-12 grid grid-cols-12 gap-2 my-4;
@apply col-span-12 grid grid-cols-12 gap-3 my-4;
}
.list-pairs-item {
@ -1324,6 +1340,10 @@ body {
@apply break-word w-full max-w-[80%] sm:max-w-[800px] col-span-12 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl text-[#344767] tracking-normal;
}
.title-form-regular {
@apply break-word w-full flex justify-center max-w-[80%] sm:max-w-[800px] col-span-12 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl text-[#344767] tracking-normal;
}
.title-content {
@apply break-word w-full max-w-[80%] sm:max-w-[700px] col-span-12 font-bold dark:text-white/90 transition duration-300 ease-in-out text-lg text-[#344767];
}
@ -1356,11 +1376,16 @@ body {
@apply mb-2;
}
.no-subtitle.title-form-regular {
@apply mb-0;
}
.no-subtitle.title-stat {
@apply mb-0;
}
.is-subtitle.title-form,
.is-subtitle.title-form-regular,
.is-subtitle.title-card,
.is-subtitle.title-void,
.is-subtitle.title-modal,
@ -1388,6 +1413,10 @@ body {
@apply break-word dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[800px] leading-normal text-[1.05rem] mb-0;
}
.subtitle-form-regular {
@apply break-word dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[800px] leading-normal text-[1.05rem] mb-0;
}
.subtitle-content {
@apply break-word dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[700px] leading-normal text-[0.975rem] mb-0;
}
@ -1485,7 +1514,7 @@ body {
/* BTN */
.btn {
@apply w-full flex justify-center items-end tracking-wide dark:brightness-90 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0;
@apply w-full flex justify-center items-center tracking-wide dark:brightness-90 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0;
}
.btn.active:not([disabled]) {

File diff suppressed because one or more lines are too long

View file

@ -300,6 +300,18 @@ export default {
"sm:mr-4",
"md:mr-4",
"lg:mr-4",
"my-0",
"sm:my-0",
"md:my-0",
"lg:my-0",
"my-2",
"sm:my-2",
"md:my-2",
"lg:my-2",
"my-4",
"sm:my-4",
"md:my-4",
"lg:my-4",
"pl-2",
"sm:pl-2",
"md:pl-2",
@ -324,6 +336,18 @@ export default {
"sm:px-2",
"md:px-2",
"lg:px-2",
"px-4",
"sm:px-4",
"md:px-4",
"lg:px-4",
"py-2",
"sm:py-2",
"md:py-2",
"lg:py-2",
"py-4",
"sm:py-4",
"md:py-4",
"lg:py-4",
"scale-90",
"scale-95",
"w-fit",