diff --git a/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py b/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py new file mode 100644 index 0000000000..049ca14c82 --- /dev/null +++ b/backend/capellacollab/alembic/versions/ca9ce61491a7_make_announcements_dismissible.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Make announcements dismissible + +Revision ID: ca9ce61491a7 +Revises: 8731ac0b284e +Create Date: 2025-02-07 19:42:01.723097 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ca9ce61491a7" +down_revision = "8731ac0b284e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.rename_table("notices", "announcements") + + op.add_column( + "announcements", sa.Column("dismissible", sa.Boolean(), nullable=True) + ) + op.execute("UPDATE announcements SET dismissible = false") + op.alter_column("announcements", "dismissible", nullable=False) diff --git a/backend/capellacollab/notices/__init__.py b/backend/capellacollab/announcements/__init__.py similarity index 100% rename from backend/capellacollab/notices/__init__.py rename to backend/capellacollab/announcements/__init__.py diff --git a/backend/capellacollab/announcements/crud.py b/backend/capellacollab/announcements/crud.py new file mode 100644 index 0000000000..dae68c4396 --- /dev/null +++ b/backend/capellacollab/announcements/crud.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +from collections import abc + +import sqlalchemy as sa +from sqlalchemy import orm + +from capellacollab.announcements import models + + +def get_announcements( + db: orm.Session, +) -> abc.Sequence[models.DatabaseAnnouncement]: + return db.execute(sa.select(models.DatabaseAnnouncement)).scalars().all() + + +def get_announcement_by_id( + db: orm.Session, announcement_id: int +) -> models.DatabaseAnnouncement | None: + return db.execute( + sa.select(models.DatabaseAnnouncement).where( + models.DatabaseAnnouncement.id == announcement_id + ) + ).scalar_one_or_none() + + +def create_announcement( + db: orm.Session, body: models.CreateAnnouncementRequest +) -> models.DatabaseAnnouncement: + announcement = models.DatabaseAnnouncement(**body.model_dump()) + db.add(announcement) + db.commit() + return announcement + + +def update_announcement( + db: orm.Session, + announcement: models.DatabaseAnnouncement, + body: models.CreateAnnouncementRequest, +) -> models.DatabaseAnnouncement: + for field, value in body.model_dump().items(): + setattr(announcement, field, value) + db.commit() + return announcement + + +def delete_announcement( + db: orm.Session, announcement: models.DatabaseAnnouncement +) -> None: + db.delete(announcement) + db.commit() diff --git a/backend/capellacollab/notices/exceptions.py b/backend/capellacollab/announcements/exceptions.py similarity index 80% rename from backend/capellacollab/notices/exceptions.py rename to backend/capellacollab/announcements/exceptions.py index 2c278ff657..2e6b229149 100644 --- a/backend/capellacollab/notices/exceptions.py +++ b/backend/capellacollab/announcements/exceptions.py @@ -7,12 +7,12 @@ class AnnouncementNotFoundError(core_exceptions.BaseError): - def __init__(self, notice_id: int): + def __init__(self, announcement_id: int): super().__init__( status_code=status.HTTP_404_NOT_FOUND, err_code="ANNOUNCEMENT_NOT_FOUND", title="Announcement not found", - reason=f"The announcement with ID {notice_id} doesn't exist", + reason=f"The announcement with ID {announcement_id} doesn't exist", ) @classmethod diff --git a/backend/capellacollab/announcements/injectables.py b/backend/capellacollab/announcements/injectables.py new file mode 100644 index 0000000000..05261fc717 --- /dev/null +++ b/backend/capellacollab/announcements/injectables.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import fastapi +from sqlalchemy import orm + +from capellacollab.announcements import crud, models +from capellacollab.core import database + +from . import exceptions + + +def get_existing_announcement( + announcement_id: int, + db: orm.Session = fastapi.Depends(database.get_db), +) -> models.DatabaseAnnouncement: + if announcement := crud.get_announcement_by_id(db, announcement_id): + return announcement + + raise exceptions.AnnouncementNotFoundError(announcement_id) diff --git a/backend/capellacollab/notices/models.py b/backend/capellacollab/announcements/models.py similarity index 61% rename from backend/capellacollab/notices/models.py rename to backend/capellacollab/announcements/models.py index ae0c20fdf2..d2fa8414d2 100644 --- a/backend/capellacollab/notices/models.py +++ b/backend/capellacollab/announcements/models.py @@ -9,7 +9,7 @@ from capellacollab.core import pydantic as core_pydantic -class NoticeLevel(enum.Enum): +class AnnouncementLevel(enum.Enum): PRIMARY = "primary" SECONDARY = "secondary" SUCCESS = "success" @@ -19,22 +19,24 @@ class NoticeLevel(enum.Enum): ALERT = "alert" -class CreateNoticeRequest(core_pydantic.BaseModel): - level: NoticeLevel +class CreateAnnouncementRequest(core_pydantic.BaseModel): + level: AnnouncementLevel title: str message: str + dismissible: bool -class NoticeResponse(CreateNoticeRequest): +class AnnouncementResponse(CreateAnnouncementRequest): id: int -class DatabaseNotice(database.Base): - __tablename__ = "notices" +class DatabaseAnnouncement(database.Base): + __tablename__ = "announcements" id: orm.Mapped[int] = orm.mapped_column( init=False, primary_key=True, index=True ) title: orm.Mapped[str] message: orm.Mapped[str] - level: orm.Mapped[NoticeLevel] + level: orm.Mapped[AnnouncementLevel] + dismissible: orm.Mapped[bool] = orm.mapped_column(default=False) diff --git a/backend/capellacollab/announcements/routes.py b/backend/capellacollab/announcements/routes.py new file mode 100644 index 0000000000..eabcf7df7c --- /dev/null +++ b/backend/capellacollab/announcements/routes.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import fastapi +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.permissions import injectables as permissions_injectables +from capellacollab.permissions import models as permissions_models + +from . import crud, injectables, models + +router = fastapi.APIRouter() + + +@router.get( + "", + response_model=list[models.AnnouncementResponse], +) +def get_announcements(db: orm.Session = fastapi.Depends(database.get_db)): + return crud.get_announcements(db) + + +@router.get("/{announcement_id}") +def get_announcement_by_id( + announcement: models.DatabaseAnnouncement = fastapi.Depends( + injectables.get_existing_announcement + ), +): + return announcement + + +@router.post( + "", + dependencies=[ + fastapi.Depends( + permissions_injectables.PermissionValidation( + required_scope=permissions_models.GlobalScopes( + admin=permissions_models.AdminScopes( + announcements={permissions_models.UserTokenVerb.CREATE} + ) + ) + ), + ) + ], +) +def create_announcement( + post_announcement: models.CreateAnnouncementRequest, + db: orm.Session = fastapi.Depends(database.get_db), +): + return crud.create_announcement(db, post_announcement) + + +@router.patch( + "/{announcement_id}", + dependencies=[ + fastapi.Depends( + permissions_injectables.PermissionValidation( + required_scope=permissions_models.GlobalScopes( + admin=permissions_models.AdminScopes( + announcements={permissions_models.UserTokenVerb.UPDATE} + ) + ) + ), + ) + ], +) +def update_announcement( + post_announcement: models.CreateAnnouncementRequest, + announcement: models.DatabaseAnnouncement = fastapi.Depends( + injectables.get_existing_announcement + ), + db: orm.Session = fastapi.Depends(database.get_db), +): + return crud.update_announcement(db, announcement, post_announcement) + + +@router.delete( + "/{announcement_id}", + status_code=204, + dependencies=[ + fastapi.Depends( + permissions_injectables.PermissionValidation( + required_scope=permissions_models.GlobalScopes( + admin=permissions_models.AdminScopes( + announcements={permissions_models.UserTokenVerb.DELETE} + ) + ) + ), + ) + ], +) +def delete_announcement( + announcement: models.DatabaseAnnouncement = fastapi.Depends( + injectables.get_existing_announcement + ), + db: orm.Session = fastapi.Depends(database.get_db), +): + crud.delete_announcement(db, announcement) diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py index d566430518..75d8032ba9 100644 --- a/backend/capellacollab/core/database/migration.py +++ b/backend/capellacollab/core/database/migration.py @@ -11,6 +11,8 @@ from sqlalchemy import orm from capellacollab import core +from capellacollab.announcements import crud as announcements_crud +from capellacollab.announcements import models as announcements_models from capellacollab.configuration.app import config from capellacollab.core import database from capellacollab.events import crud as events_crud @@ -82,6 +84,7 @@ def migrate_db(engine, database_url: str): LOGGER.info("Database structure creation successful") command.stamp(alembic_cfg, "head") initialize_admin_user(session) + create_welcome_announcement(session) create_tools(session) initialize_capellambse_test_project(session) @@ -105,6 +108,19 @@ def initialize_admin_user(db: orm.Session): LOGGER.info("Initialized admin user %s", config.initial.admin) +def create_welcome_announcement(db: orm.Session): + welcome_announcement = announcements_crud.create_announcement( + db, + announcements_models.CreateAnnouncementRequest( + title="Welcome to the Capella Collaboration Manager", + message="Make sure to check out our documentation to learn more", + level=announcements_models.AnnouncementLevel.PRIMARY, + dismissible=True, + ), + ) + LOGGER.info("Initialized welcome announcement %s", welcome_announcement.id) + + def initialize_capellambse_test_project(db: orm.Session): project = projects_crud.create_project( db=db, diff --git a/backend/capellacollab/core/database/models.py b/backend/capellacollab/core/database/models.py index dff0b2c4b5..dd53c28fb9 100644 --- a/backend/capellacollab/core/database/models.py +++ b/backend/capellacollab/core/database/models.py @@ -4,10 +4,10 @@ # pylint: disable=unused-import # These import statements of the models are required and should not be removed! (SQLAlchemy will not load the models otherwise) +import capellacollab.announcements.models import capellacollab.configuration.models import capellacollab.events.models import capellacollab.feedback.models -import capellacollab.notices.models import capellacollab.projects.models import capellacollab.projects.permissions.models import capellacollab.projects.toolmodels.backups.models diff --git a/backend/capellacollab/notices/crud.py b/backend/capellacollab/notices/crud.py deleted file mode 100644 index 4a678186d9..0000000000 --- a/backend/capellacollab/notices/crud.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - - -from collections import abc - -import sqlalchemy as sa -from sqlalchemy import orm - -from capellacollab.notices import models - - -def get_notices(db: orm.Session) -> abc.Sequence[models.DatabaseNotice]: - return db.execute(sa.select(models.DatabaseNotice)).scalars().all() - - -def get_notice_by_id( - db: orm.Session, notice_id: int -) -> models.DatabaseNotice | None: - return db.execute( - sa.select(models.DatabaseNotice).where( - models.DatabaseNotice.id == notice_id - ) - ).scalar_one_or_none() - - -def create_notice( - db: orm.Session, body: models.CreateNoticeRequest -) -> models.DatabaseNotice: - notice = models.DatabaseNotice(**body.model_dump()) - db.add(notice) - db.commit() - return notice - - -def delete_notice(db: orm.Session, notice: models.DatabaseNotice) -> None: - db.delete(notice) - db.commit() diff --git a/backend/capellacollab/notices/injectables.py b/backend/capellacollab/notices/injectables.py deleted file mode 100644 index 75ed721d2a..0000000000 --- a/backend/capellacollab/notices/injectables.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -import fastapi -from sqlalchemy import orm - -from capellacollab.core import database -from capellacollab.notices import crud, models - -from . import exceptions - - -def get_existing_notice( - notice_id: int, - db: orm.Session = fastapi.Depends(database.get_db), -) -> models.DatabaseNotice: - if notice := crud.get_notice_by_id(db, notice_id): - return notice - - raise exceptions.AnnouncementNotFoundError(notice_id) diff --git a/backend/capellacollab/notices/routes.py b/backend/capellacollab/notices/routes.py deleted file mode 100644 index fb3a0b7286..0000000000 --- a/backend/capellacollab/notices/routes.py +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -import fastapi -from sqlalchemy import orm - -import capellacollab.notices.crud as notices -from capellacollab.core import database -from capellacollab.notices.injectables import get_existing_notice -from capellacollab.notices.models import ( - CreateNoticeRequest, - DatabaseNotice, - NoticeResponse, -) -from capellacollab.permissions import injectables as permissions_injectables -from capellacollab.permissions import models as permissions_models - -router = fastapi.APIRouter() - - -@router.get( - "", - response_model=list[NoticeResponse], -) -def get_notices(db: orm.Session = fastapi.Depends(database.get_db)): - return notices.get_notices(db) - - -@router.get("/{notice_id}") -def get_notice_by_id( - notice: DatabaseNotice = fastapi.Depends(get_existing_notice), -): - return notice - - -@router.post( - "", - dependencies=[ - fastapi.Depends( - permissions_injectables.PermissionValidation( - required_scope=permissions_models.GlobalScopes( - admin=permissions_models.AdminScopes( - announcements={permissions_models.UserTokenVerb.CREATE} - ) - ) - ), - ) - ], -) -def create_notice( - post_notice: CreateNoticeRequest, - db: orm.Session = fastapi.Depends(database.get_db), -): - return notices.create_notice(db, post_notice) - - -@router.delete( - "/{notice_id}", - status_code=204, - dependencies=[ - fastapi.Depends( - permissions_injectables.PermissionValidation( - required_scope=permissions_models.GlobalScopes( - admin=permissions_models.AdminScopes( - announcements={permissions_models.UserTokenVerb.DELETE} - ) - ) - ), - ) - ], -) -def delete_notice( - notice: DatabaseNotice = fastapi.Depends(get_existing_notice), - db: orm.Session = fastapi.Depends(database.get_db), -): - notices.delete_notice(db, notice) diff --git a/backend/capellacollab/permissions/models.py b/backend/capellacollab/permissions/models.py index 81922559f2..5a2d7a1d8c 100644 --- a/backend/capellacollab/permissions/models.py +++ b/backend/capellacollab/permissions/models.py @@ -101,7 +101,9 @@ class AdminScopes(core_pydantic.BaseModelStrict): description="Manage all tools, including its versions and natures", ) announcements: set[ - t.Literal[UserTokenVerb.CREATE, UserTokenVerb.DELETE] + t.Literal[ + UserTokenVerb.CREATE, UserTokenVerb.UPDATE, UserTokenVerb.DELETE + ] ] = pydantic.Field( default_factory=set, title="Announcements", diff --git a/backend/capellacollab/routes.py b/backend/capellacollab/routes.py index 4b6ee07f4a..a3a823f17b 100644 --- a/backend/capellacollab/routes.py +++ b/backend/capellacollab/routes.py @@ -6,12 +6,12 @@ import fastapi +from capellacollab.announcements import routes as announcements_routes from capellacollab.configuration import routes as configuration_routes from capellacollab.core.authentication import routes as authentication_routes from capellacollab.events import routes as events_router from capellacollab.feedback import routes as feedback_routes from capellacollab.health import routes as health_routes -from capellacollab.notices import routes as notices_routes from capellacollab.permissions import routes as permissions_routes from capellacollab.projects import routes as projects_routes from capellacollab.sessions import routes as sessions_routes @@ -60,7 +60,9 @@ tags=["Events"], ) router.include_router( - notices_routes.router, prefix="/notices", tags=["Notices"] + announcements_routes.router, + prefix="/announcements", + tags=["Announcements"], ) router.include_router( settings_routes.router, diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index 0c95e6d4f9..dbd3270cc6 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -70,6 +70,7 @@ class Role(str, enum.Enum): }, announcements={ models.UserTokenVerb.CREATE, + models.UserTokenVerb.UPDATE, models.UserTokenVerb.DELETE, }, monitoring={ diff --git a/backend/tests/settings/test_alerts.py b/backend/tests/settings/test_alerts.py deleted file mode 100644 index 92ec26160d..0000000000 --- a/backend/tests/settings/test_alerts.py +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from collections import abc - -from fastapi.testclient import TestClient -from sqlalchemy import orm - -from capellacollab.notices import crud as notices_crud -from capellacollab.notices import models as notices_models -from capellacollab.users import crud as users_crud -from capellacollab.users import models as users_models - - -def test_get_alerts(client: TestClient, db: orm.Session, executor_name: str): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.USER - ) - notice = notices_crud.create_notice( - db, - notices_models.CreateNoticeRequest( - level=notices_models.NoticeLevel.INFO, - title="test title", - message="test message", - ), - ) - - response = client.get("/api/v1/notices") - - assert response.status_code == 200 - assert len(response.json()) == 1 - assert { - "level": "info", - "title": "test title", - "message": "test message", - }.items() <= response.json()[0].items() - - single_response = client.get(f"/api/v1/notices/{notice.id}") - assert single_response.status_code == 200 - assert { - "level": "info", - "title": "test title", - "message": "test message", - }.items() <= single_response.json().items() - - -def test_get_missing_alert( - client: TestClient, db: orm.Session, executor_name: str -): - response = client.get("/api/v1/notices/1") - - assert response.status_code == 404 - assert response.json()["detail"]["err_code"] == "ANNOUNCEMENT_NOT_FOUND" - - -def test_create_alert2( - client: TestClient, db: orm.Session, executor_name: str -): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - - response = client.post( - "/api/v1/notices", - json={"title": "test", "message": "test", "level": "success"}, - ) - - assert response.status_code == 200 - - notices: abc.Sequence[notices_models.DatabaseNotice] = ( - notices_crud.get_notices(db) - ) - assert len(notices) == 1 - assert notices[0].title == "test" - assert notices[0].message == "test" - assert notices[0].level == notices_models.NoticeLevel.SUCCESS - - -def test_delete_alert(client: TestClient, db: orm.Session, executor_name: str): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - alert = notices_crud.create_notice( - db, - notices_models.CreateNoticeRequest( - level=notices_models.NoticeLevel.INFO, - title="test title", - message="test message", - ), - ) - - response = client.delete(f"/api/v1/notices/{alert.id}") - - assert response.status_code == 204 - assert not notices_crud.get_notices(db) diff --git a/backend/tests/settings/test_announcements.py b/backend/tests/settings/test_announcements.py new file mode 100644 index 0000000000..8d3e2fb0a8 --- /dev/null +++ b/backend/tests/settings/test_announcements.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from fastapi import testclient +from sqlalchemy import orm + +from capellacollab.announcements import crud as announcements_crud +from capellacollab.announcements import models as announcements_models +from capellacollab.users import models as users_models + + +def test_get_announcements( + client: testclient.TestClient, + db: orm.Session, +): + announcement = announcements_crud.create_announcement( + db, + announcements_models.CreateAnnouncementRequest( + level=announcements_models.AnnouncementLevel.INFO, + title="test title", + message="test message", + dismissible=True, + ), + ) + + response = client.get("/api/v1/announcements") + + assert response.status_code == 200 + assert len(response.json()) == 2 + assert { + "level": "info", + "title": "test title", + "message": "test message", + "dismissible": True, + }.items() <= response.json()[1].items() + + single_response = client.get(f"/api/v1/announcements/{announcement.id}") + assert single_response.status_code == 200 + assert { + "level": "info", + "title": "test title", + "message": "test message", + "dismissible": True, + }.items() <= single_response.json().items() + + +def test_get_missing_announcement( + client: testclient.TestClient, + db: orm.Session, +): + response = client.get("/api/v1/announcements/99") + + assert response.status_code == 404 + assert response.json()["detail"]["err_code"] == "ANNOUNCEMENT_NOT_FOUND" + + +def test_create_announcement( + client: testclient.TestClient, + admin: users_models.DatabaseUser, + db: orm.Session, +): + response = client.post( + "/api/v1/announcements", + json={ + "title": "test", + "message": "test", + "level": "success", + "dismissible": True, + }, + ) + + assert response.status_code == 200 + + announcements = announcements_crud.get_announcements(db) + + assert len(announcements) == 2 + assert announcements[1].title == "test" + assert announcements[1].message == "test" + assert ( + announcements[1].level + == announcements_models.AnnouncementLevel.SUCCESS + ) + + +def test_update_announcement( + client: testclient.TestClient, + admin: users_models.DatabaseUser, + db: orm.Session, +): + announcement = announcements_crud.create_announcement( + db, + announcements_models.CreateAnnouncementRequest( + level=announcements_models.AnnouncementLevel.INFO, + title="original title", + message="original message", + dismissible=True, + ), + ) + + update_data = { + "title": "updated title", + "message": "updated message", + "level": "warning", + "dismissible": False, + } + + response = client.patch( + f"/api/v1/announcements/{announcement.id}", json=update_data + ) + + assert response.status_code == 200 + + updated_announcement = announcements_crud.get_announcement_by_id( + db, announcement.id + ) + + assert updated_announcement.title == "updated title" + assert updated_announcement.message == "updated message" + assert ( + updated_announcement.level + == announcements_models.AnnouncementLevel.WARNING + ) + assert updated_announcement.dismissible is False + + +def test_delete_announcement( + client: testclient.TestClient, + admin: users_models.DatabaseUser, + db: orm.Session, +): + announcement = announcements_crud.create_announcement( + db, + announcements_models.CreateAnnouncementRequest( + level=announcements_models.AnnouncementLevel.INFO, + title="test title", + message="test message", + dismissible=True, + ), + ) + + response = client.delete(f"/api/v1/announcements/{announcement.id}") + + assert response.status_code == 204 + + announcements = announcements_crud.get_announcements(db) + assert len(announcements) == 1 diff --git a/docs/docs/admin/alerts/create.md b/docs/docs/admin/alerts/create.md deleted file mode 100644 index 84fb9c104e..0000000000 --- a/docs/docs/admin/alerts/create.md +++ /dev/null @@ -1,41 +0,0 @@ - - -Alerts can be used to inform users about changes, news or maintenance work. The -alerts are displayed to each user. - -1. Navigate to `Menu` → `Settings` -2. Fill in all required fields in the `Create an alert` form. - ![Create an alert](create.png) - - !!! Question "What does the alert level mean?" - - The alert level specifies - the background color of the alert. You can choose one of the following - options:
- - :material-checkbox-blank-circle:{ style="color: #004085 " } `primary`
- :material-checkbox-blank-circle:{ style="color: #383d41 " } `secondary`
- :material-checkbox-blank-circle:{ style="color: #155724 " } `success`
- :material-checkbox-blank-circle:{ style="color: #721c24 " } `danger`
- :material-checkbox-blank-circle:{ style="color: #fff3cd " } `warning`
- :material-checkbox-blank-circle:{ style="color: #d1ecf1 " } `info`
- - !!! Question "Which scopes are available?" - - Currently, there is only one scope. - Please enter `t4c` in the scope field. - - !!! hint - - Simple HTML tags can be used in the alerts description. For - example, a link can be created with: - - ``` html - Link description - ``` - -3. The alert is now created and is displayed to all users: - ![Success alert](success_alert.png) diff --git a/docs/docs/admin/alerts/create.png b/docs/docs/admin/alerts/create.png deleted file mode 100644 index 87fb4de4a9..0000000000 Binary files a/docs/docs/admin/alerts/create.png and /dev/null differ diff --git a/docs/docs/admin/alerts/success_alert.png b/docs/docs/admin/alerts/success_alert.png deleted file mode 100644 index acd083dac5..0000000000 Binary files a/docs/docs/admin/alerts/success_alert.png and /dev/null differ diff --git a/docs/docs/admin/announcements/create.md b/docs/docs/admin/announcements/create.md new file mode 100644 index 0000000000..c90dea9c37 --- /dev/null +++ b/docs/docs/admin/announcements/create.md @@ -0,0 +1,42 @@ + + +Announcements can be used to inform users about changes, news or maintenance +work. Announcements are displayed to all users, no matter their role. + +1. Navigate to `Menu` → `Settings` +2. Fill in all required fields in the `Create new announcement` form. + ![Create an Announcement](create.png) + + !!! Question "What does the announcement level mean?" + + The announcement level specifies + the background color of the announcement. You can choose one of the following + options:
+ + :material-checkbox-blank-circle:{ style="color: #004085 " } `primary`
+ :material-checkbox-blank-circle:{ style="color: #383d41 " } `secondary`
+ :material-checkbox-blank-circle:{ style="color: #155724 " } `success`
+ :material-checkbox-blank-circle:{ style="color: #721c24 " } `danger`
+ :material-checkbox-blank-circle:{ style="color: #fff3cd " } `warning`
+ :material-checkbox-blank-circle:{ style="color: #d1ecf1 " } `info`
+ + !!! hint + + The announcement description supports markdown syntax. + + ```markdown + You can use __bold__, *italic*, ~~strikethrough~~, and `code` text. + ``` + Check the [Markdown Guide](https://www.markdownguide.org/basic-syntax/) for more information. + + Additionally, you can use HTML tags in the announcement description. + + !!! hint + + By default, announcements can be dismissed by users. If you want to create an announcement that cannot be dismissed, you can toggle the `Dismissable` checkbox. + +3. The announcement is now created and is displayed to all users: + ![Success announcement](success_announcement.png) diff --git a/docs/docs/admin/announcements/create.png b/docs/docs/admin/announcements/create.png new file mode 100644 index 0000000000..cb93bebcbf Binary files /dev/null and b/docs/docs/admin/announcements/create.png differ diff --git a/docs/docs/admin/alerts/create.png.license b/docs/docs/admin/announcements/create.png.license similarity index 100% rename from docs/docs/admin/alerts/create.png.license rename to docs/docs/admin/announcements/create.png.license diff --git a/docs/docs/admin/announcements/success_announcement.png b/docs/docs/admin/announcements/success_announcement.png new file mode 100644 index 0000000000..a02ffc4e41 Binary files /dev/null and b/docs/docs/admin/announcements/success_announcement.png differ diff --git a/docs/docs/admin/alerts/success_alert.png.license b/docs/docs/admin/announcements/success_announcement.png.license similarity index 100% rename from docs/docs/admin/alerts/success_alert.png.license rename to docs/docs/admin/announcements/success_announcement.png.license diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 880aa69314..27c9ad30d5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -117,7 +117,7 @@ nav: - Tools: - General: admin/settings/tools/index.md - Configuration: admin/tools/configuration.md - - Alerts: admin/alerts/create.md + - Announcements: admin/announcements/create.md - CI templates: - Gitlab CI/CD: - Image builder: admin/ci-templates/gitlab/image-builder.md diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 57682eb0db..d7468abf67 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -42,7 +42,7 @@ import { ProjectOverviewComponent } from './projects/project-overview/project-ov import { ProjectWrapperComponent } from './projects/project-wrapper/project-wrapper.component'; import { SessionOverviewComponent } from './sessions/session-overview/session-overview.component'; import { SessionsComponent } from './sessions/sessions.component'; -import { AlertSettingsComponent } from './settings/core/alert-settings/alert-settings.component'; +import { AnnouncementSettingsComponent } from './settings/core/announcement-settings/announcement-settings.component'; import { ToolDetailsComponent } from './settings/core/tools-settings/tool-details/tool-details.component'; import { ToolsSettingsComponent } from './settings/core/tools-settings/tools-settings.component'; import { PureVariantsComponent } from './settings/integrations/pure-variants/pure-variants.component'; @@ -361,9 +361,9 @@ export const routes: Routes = [ component: UserSettingsComponent, }, { - path: 'alerts', - data: { breadcrumb: 'Alerts' }, - component: AlertSettingsComponent, + path: 'announcements', + data: { breadcrumb: 'Announcements' }, + component: AnnouncementSettingsComponent, }, { path: 'pipelines', diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 43c30bea4a..e5b75454e2 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -25,9 +25,9 @@ > @if ( (fullscreenService.isFullscreen$ | async) === false && - pageLayoutService.showNotice + pageLayoutService.showAnnouncement ) { - + } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7ec0c0cc10..9382527613 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -13,10 +13,10 @@ import { import { RouterOutlet } from '@angular/router'; import slugify from 'slugify'; import { NavBarService } from 'src/app/general/nav-bar/nav-bar.service'; +import { AnnouncementListComponent } from './general/announcement/announcement-list.component'; import { FooterComponent } from './general/footer/footer.component'; import { HeaderComponent } from './general/header/header.component'; import { NavBarMenuComponent } from './general/nav-bar-menu/nav-bar-menu.component'; -import { NoticeComponent } from './general/notice/notice.component'; import { PageLayoutService } from './page-layout/page-layout.service'; import { FeedbackWrapperService } from './sessions/feedback/feedback.service'; import { FullscreenService } from './sessions/service/fullscreen.service'; @@ -31,7 +31,7 @@ import { FullscreenService } from './sessions/service/fullscreen.service'; MatDrawerContent, HeaderComponent, NgClass, - NoticeComponent, + AnnouncementListComponent, RouterOutlet, FooterComponent, AsyncPipe, diff --git a/frontend/src/app/general/announcement/announcement-list.component.html b/frontend/src/app/general/announcement/announcement-list.component.html new file mode 100644 index 0000000000..2fe5ebaa08 --- /dev/null +++ b/frontend/src/app/general/announcement/announcement-list.component.html @@ -0,0 +1,13 @@ + + +
+ @for ( + announcement of announcementWrapperService.visibleAnnouncements(); + track announcement.id + ) { + + } +
diff --git a/frontend/src/app/general/announcement/announcement-list.component.ts b/frontend/src/app/general/announcement/announcement-list.component.ts new file mode 100644 index 0000000000..d57ad526ed --- /dev/null +++ b/frontend/src/app/general/announcement/announcement-list.component.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Component } from '@angular/core'; +import { AnnouncementWrapperService } from 'src/app/general/announcement/announcement.service'; +import { AnnouncementComponent } from './announcement/announcement.component'; + +@Component({ + selector: 'app-announcement-list', + templateUrl: './announcement-list.component.html', + imports: [AnnouncementComponent], +}) +export class AnnouncementListComponent { + constructor(public announcementWrapperService: AnnouncementWrapperService) {} +} diff --git a/frontend/src/app/general/announcement/announcement.service.ts b/frontend/src/app/general/announcement/announcement.service.ts new file mode 100644 index 0000000000..89a0bf7285 --- /dev/null +++ b/frontend/src/app/general/announcement/announcement.service.ts @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { computed, inject, Injectable, signal } from '@angular/core'; +import { AnnouncementResponse, AnnouncementsService } from 'src/app/openapi'; +import { z } from 'zod'; + +const dismissedAnnouncementsSchema = z.array( + z.object({ + id: z.number(), + date: z.coerce.date(), + }), +); + +export type DismissedAnnouncements = z.infer< + typeof dismissedAnnouncementsSchema +>; + +@Injectable({ + providedIn: 'root', +}) +export class AnnouncementWrapperService { + LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY = 'dismissedAnnouncements'; + + public announcements = signal(undefined); + public dismissedAnnouncements = signal([]); + private announcementsService: AnnouncementsService = + inject(AnnouncementsService); + public visibleAnnouncements = computed(() => { + const announcements = this.announcements(); + if (!announcements) return []; + + return announcements.filter( + (announcement) => + !this.dismissedAnnouncements().find((dismissedAnnouncement) => { + return ( + dismissedAnnouncement.id === announcement.id && + announcement.dismissible + ); + }), + ); + }); + + constructor() { + this.refreshAnnouncements(); + } + + dismissAnnouncement(announcementId: number): void { + this.dismissedAnnouncements.set([ + ...this.dismissedAnnouncements(), + { id: announcementId, date: new Date() }, + ]); + localStorage.setItem( + this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY, + JSON.stringify(this.dismissedAnnouncements()), + ); + } + + resetDismissedAnnouncements(): void { + this.dismissedAnnouncements.set([]); + localStorage.removeItem(this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY); + } + + refreshAnnouncements(): void { + const localDismissedAnnouncements = localStorage.getItem( + this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY, + ); + try { + const result = dismissedAnnouncementsSchema.safeParse( + JSON.parse(localDismissedAnnouncements ?? '[]'), + ); + if (result.success) { + this.dismissedAnnouncements.set(result.data); + } + } catch (e) { + console.error(e); + localStorage.removeItem(this.LOCAL_STORAGE_DISMISSED_ANNOUNCEMENT_KEY); + } + this.announcementsService.getAnnouncements().subscribe((res) => { + this.announcements.set(res); + }); + } +} diff --git a/frontend/src/app/general/announcement/announcement.stories.ts b/frontend/src/app/general/announcement/announcement.stories.ts new file mode 100644 index 0000000000..f5edd877ab --- /dev/null +++ b/frontend/src/app/general/announcement/announcement.stories.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { userEvent, within } from '@storybook/test'; +import { AnnouncementLevel } from 'src/app/openapi'; +import { + mockAnnouncement, + mockAnnouncementWrapperServiceProvider, +} from 'src/storybook/announcements'; +import { AnnouncementListComponent } from './announcement-list.component'; + +const meta: Meta = { + title: 'General Components/Announcements', + component: AnnouncementListComponent, + parameters: { + screenshot: { + delay: 1000, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const AllLevels: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + Object.values(AnnouncementLevel).map((level, i) => ({ + ...mockAnnouncement, + title: 'This is an example announcement with level ' + level, + level, + id: i, + })), + [], + ), + ], + }), + ], +}; + +export const DismissedAnnouncements: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + [ + mockAnnouncement, + { + ...mockAnnouncement, + message: 'This announcement is not visible', + id: 2, + }, + { + ...mockAnnouncement, + message: + "This announcement is visible despite being dismissed, because it's not dismissible", + dismissible: false, + id: 3, + }, + ], + [{ id: 3, date: new Date() }], + ), + ], + }), + ], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const dismissButton = canvas.getByTestId('announcement-2'); + await userEvent.click(dismissButton); + }, +}; diff --git a/frontend/src/app/general/notice/notice.component.css b/frontend/src/app/general/announcement/announcement/announcement.component.css similarity index 100% rename from frontend/src/app/general/notice/notice.component.css rename to frontend/src/app/general/announcement/announcement/announcement.component.css diff --git a/frontend/src/app/general/announcement/announcement/announcement.component.html b/frontend/src/app/general/announcement/announcement/announcement.component.html new file mode 100644 index 0000000000..1717397a54 --- /dev/null +++ b/frontend/src/app/general/announcement/announcement/announcement.component.html @@ -0,0 +1,30 @@ + + +
+
+

+ +
+ @if (announcement().dismissible) { + + } +
diff --git a/frontend/src/app/general/announcement/announcement/announcement.component.ts b/frontend/src/app/general/announcement/announcement/announcement.component.ts new file mode 100644 index 0000000000..bb9678fad1 --- /dev/null +++ b/frontend/src/app/general/announcement/announcement/announcement.component.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + animate, + state, + style, + transition, + trigger, +} from '@angular/animations'; +import { Component, inject, input } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MarkdownComponent, provideMarkdown } from 'ngx-markdown'; +import { AnnouncementResponse } from '../../../openapi'; +import { AnnouncementWrapperService } from '../announcement.service'; + +@Component({ + selector: 'app-announcement', + imports: [MatIconButton, MatIconModule, MarkdownComponent], + templateUrl: './announcement.component.html', + styleUrls: ['./announcement.component.css'], + providers: [provideMarkdown()], + animations: [ + trigger('announcementAnimation', [ + state('void', style({ height: '0' })), + state('*', style({ height: '*' })), + transition('void <=> *', animate('150ms ease-in-out')), + ]), + ], +}) +export class AnnouncementComponent { + announcement = input.required(); + isFake = input(false); + announcementWrapperService: AnnouncementWrapperService = inject( + AnnouncementWrapperService, + ); + + dismissAnnouncement() { + if (!this.isFake()) { + this.announcementWrapperService.dismissAnnouncement( + this.announcement().id, + ); + } + } +} diff --git a/frontend/src/app/general/auth/auth/auth.component.html b/frontend/src/app/general/auth/auth/auth.component.html index 2897553366..f95c19c935 100644 --- a/frontend/src/app/general/auth/auth/auth.component.html +++ b/frontend/src/app/general/auth/auth/auth.component.html @@ -4,10 +4,6 @@ -->
-
- -
- @if (params["error"]) {

diff --git a/frontend/src/app/general/auth/auth/auth.component.ts b/frontend/src/app/general/auth/auth/auth.component.ts index 25df7016bb..21b5586ce3 100644 --- a/frontend/src/app/general/auth/auth/auth.component.ts +++ b/frontend/src/app/general/auth/auth/auth.component.ts @@ -12,18 +12,11 @@ import { DOCS_URL } from 'src/app/environment'; import { MetadataService } from 'src/app/general/metadata/metadata.service'; import { PageLayoutService } from 'src/app/page-layout/page-layout.service'; import { AuthenticationWrapperService } from 'src/app/services/auth/auth.service'; -import { WelcomeComponent } from '../../welcome/welcome.component'; @Component({ selector: 'app-auth', templateUrl: './auth.component.html', - imports: [ - MatButton, - AsyncPipe, - WelcomeComponent, - MatIconModule, - MatButtonModule, - ], + imports: [MatButton, AsyncPipe, MatIconModule, MatButtonModule], }) export class AuthComponent implements OnInit { public params = {} as Params; diff --git a/frontend/src/app/general/notice/notice.component.html b/frontend/src/app/general/notice/notice.component.html deleted file mode 100644 index 5f29a6ad83..0000000000 --- a/frontend/src/app/general/notice/notice.component.html +++ /dev/null @@ -1,19 +0,0 @@ - - -

- @for (notice of noticesWrapperService.notices$ | async; track notice.id) { -
-

-

-
- } -
diff --git a/frontend/src/app/general/notice/notice.component.ts b/frontend/src/app/general/notice/notice.component.ts deleted file mode 100644 index 9f8963304a..0000000000 --- a/frontend/src/app/general/notice/notice.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { AsyncPipe, NgClass } from '@angular/common'; -import { Component } from '@angular/core'; -import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; - -@Component({ - selector: 'app-notice', - templateUrl: './notice.component.html', - styleUrls: ['./notice.component.css'], - imports: [NgClass, AsyncPipe], -}) -export class NoticeComponent { - constructor(public noticesWrapperService: NoticeWrapperService) {} -} diff --git a/frontend/src/app/general/notice/notice.service.ts b/frontend/src/app/general/notice/notice.service.ts deleted file mode 100644 index 9c0012a936..0000000000 --- a/frontend/src/app/general/notice/notice.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { NoticeResponse, NoticesService } from 'src/app/openapi'; - -@Injectable({ - providedIn: 'root', -}) -export class NoticeWrapperService { - private _notices = new BehaviorSubject( - undefined, - ); - public readonly notices$ = this._notices.asObservable(); - - constructor(private noticesService: NoticesService) { - this.refreshNotices(); - } - - refreshNotices(): void { - this._notices.next(undefined); - this.noticesService.getNotices().subscribe((res) => { - this._notices.next(res); - }); - } -} diff --git a/frontend/src/app/general/notice/notice.stories.ts b/frontend/src/app/general/notice/notice.stories.ts deleted file mode 100644 index 6b278e873a..0000000000 --- a/frontend/src/app/general/notice/notice.stories.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { NoticeLevel } from 'src/app/openapi'; -import { - mockNotice, - mockNoticeWrapperServiceProvider, -} from 'src/storybook/notices'; -import { NoticeComponent } from './notice.component'; - -const meta: Meta = { - title: 'General Components/Notices', - component: NoticeComponent, -}; - -export default meta; -type Story = StoryObj; - -export const AllLevels: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - mockNoticeWrapperServiceProvider( - Object.values(NoticeLevel).map((level) => ({ - ...mockNotice, - title: 'This is an example notice with level ' + level, - level, - })), - ), - ], - }), - ], -}; diff --git a/frontend/src/app/general/welcome/welcome.component.html b/frontend/src/app/general/welcome/welcome.component.html deleted file mode 100644 index f5e7820f88..0000000000 --- a/frontend/src/app/general/welcome/welcome.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- Welcome to the -

Capella Collaboration Manager

-
diff --git a/frontend/src/app/general/welcome/welcome.component.ts b/frontend/src/app/general/welcome/welcome.component.ts deleted file mode 100644 index 161f8ff2e3..0000000000 --- a/frontend/src/app/general/welcome/welcome.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -@Component({ - selector: 'app-welcome', - imports: [CommonModule], - templateUrl: './welcome.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class WelcomeComponent {} diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES index da2c904078..4a87844101 100644 --- a/frontend/src/app/openapi/.openapi-generator/FILES +++ b/frontend/src/app/openapi/.openapi-generator/FILES @@ -1,4 +1,5 @@ api.module.ts +api/announcements.service.ts api/api.ts api/authentication.service.ts api/configuration.service.ts @@ -6,7 +7,6 @@ api/events.service.ts api/feedback.service.ts api/health.service.ts api/integrations-pure-variants.service.ts -api/notices.service.ts api/permissions.service.ts api/projects-events.service.ts api/projects-models-backups.service.ts @@ -36,6 +36,8 @@ encoder.ts index.ts model/admin-scopes-input.ts model/admin-scopes-output.ts +model/announcement-level.ts +model/announcement-response.ts model/anonymized-session.ts model/authorization-response.ts model/backup-pipeline-run.ts @@ -50,8 +52,8 @@ model/built-in-link-item.ts model/built-in-navbar-link.ts model/cpu-resources-input.ts model/cpu-resources-output.ts +model/create-announcement-request.ts model/create-backup.ts -model/create-notice-request.ts model/create-t4-c-instance.ts model/create-t4-c-repository.ts model/create-tool-input.ts @@ -112,8 +114,6 @@ model/models.ts model/navbar-configuration-input-external-links-inner.ts model/navbar-configuration-input.ts model/navbar-configuration-output.ts -model/notice-level.ts -model/notice-response.ts model/page-history-event.ts model/page-pipeline-run.ts model/patch-project-user.ts diff --git a/frontend/src/app/openapi/api/notices.service.ts b/frontend/src/app/openapi/api/announcements.service.ts similarity index 59% rename from frontend/src/app/openapi/api/notices.service.ts rename to frontend/src/app/openapi/api/announcements.service.ts index c614dbcc62..9a64769c69 100644 --- a/frontend/src/app/openapi/api/notices.service.ts +++ b/frontend/src/app/openapi/api/announcements.service.ts @@ -19,11 +19,11 @@ import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; // @ts-ignore -import { CreateNoticeRequest } from '../model/create-notice-request'; +import { AnnouncementResponse } from '../model/announcement-response'; // @ts-ignore -import { HTTPValidationError } from '../model/http-validation-error'; +import { CreateAnnouncementRequest } from '../model/create-announcement-request'; // @ts-ignore -import { NoticeResponse } from '../model/notice-response'; +import { HTTPValidationError } from '../model/http-validation-error'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -34,7 +34,7 @@ import { Configuration } from '../configurat @Injectable({ providedIn: 'root' }) -export class NoticesService { +export class AnnouncementsService { protected basePath = 'http://localhost'; public defaultHeaders = new HttpHeaders(); @@ -97,18 +97,18 @@ export class NoticesService { } /** - * Create Notice + * Create Announcement * This route requires the following permissions: `admin.announcements:create` - * @param createNoticeRequest + * @param createAnnouncementRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createNotice(createNoticeRequest: CreateNoticeRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createNotice(createNoticeRequest: CreateNoticeRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { - if (createNoticeRequest === null || createNoticeRequest === undefined) { - throw new Error('Required parameter createNoticeRequest was null or undefined when calling createNotice.'); + public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createAnnouncement(createAnnouncementRequest: CreateAnnouncementRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (createAnnouncementRequest === null || createAnnouncementRequest === undefined) { + throw new Error('Required parameter createAnnouncementRequest was null or undefined when calling createAnnouncement.'); } let localVarHeaders = this.defaultHeaders; @@ -168,11 +168,11 @@ export class NoticesService { } } - let localVarPath = `/api/v1/notices`; + let localVarPath = `/api/v1/announcements`; return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, - body: createNoticeRequest, + body: createAnnouncementRequest, responseType: responseType_, withCredentials: this.configuration.withCredentials, headers: localVarHeaders, @@ -184,18 +184,18 @@ export class NoticesService { } /** - * Delete Notice + * Delete Announcement * This route requires the following permissions: `admin.announcements:delete` - * @param noticeId + * @param announcementId * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public deleteNotice(noticeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public deleteNotice(noticeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public deleteNotice(noticeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public deleteNotice(noticeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { - if (noticeId === null || noticeId === undefined) { - throw new Error('Required parameter noticeId was null or undefined when calling deleteNotice.'); + public deleteAnnouncement(announcementId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteAnnouncement(announcementId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteAnnouncement(announcementId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteAnnouncement(announcementId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (announcementId === null || announcementId === undefined) { + throw new Error('Required parameter announcementId was null or undefined when calling deleteAnnouncement.'); } let localVarHeaders = this.defaultHeaders; @@ -246,7 +246,7 @@ export class NoticesService { } } - let localVarPath = `/api/v1/notices/${this.configuration.encodeParam({name: "noticeId", value: noticeId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, @@ -261,17 +261,17 @@ export class NoticesService { } /** - * Get Notice By Id - * @param noticeId + * Get Announcement By Id + * @param announcementId * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getNoticeById(noticeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public getNoticeById(noticeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getNoticeById(noticeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getNoticeById(noticeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { - if (noticeId === null || noticeId === undefined) { - throw new Error('Required parameter noticeId was null or undefined when calling getNoticeById.'); + public getAnnouncementById(announcementId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getAnnouncementById(announcementId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAnnouncementById(announcementId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAnnouncementById(announcementId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (announcementId === null || announcementId === undefined) { + throw new Error('Required parameter announcementId was null or undefined when calling getAnnouncementById.'); } let localVarHeaders = this.defaultHeaders; @@ -310,7 +310,7 @@ export class NoticesService { } } - let localVarPath = `/api/v1/notices/${this.configuration.encodeParam({name: "noticeId", value: noticeId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, @@ -325,17 +325,98 @@ export class NoticesService { } /** - * Get Notices + * Get Announcements + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getAnnouncements(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAnnouncements(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAnnouncements(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getAnnouncements(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/announcements`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Update Announcement + * This route requires the following permissions: `admin.announcements:update` + * @param announcementId + * @param createAnnouncementRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getNotices(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getNotices(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getNotices(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getNotices(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateAnnouncement(announcementId: number, createAnnouncementRequest: CreateAnnouncementRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (announcementId === null || announcementId === undefined) { + throw new Error('Required parameter announcementId was null or undefined when calling updateAnnouncement.'); + } + if (createAnnouncementRequest === null || createAnnouncementRequest === undefined) { + throw new Error('Required parameter createAnnouncementRequest was null or undefined when calling updateAnnouncement.'); + } let localVarHeaders = this.defaultHeaders; + let localVarCredential: string | undefined; + // authentication (Cookie) required + localVarCredential = this.configuration.lookupCredential('Cookie'); + if (localVarCredential) { + } + + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; if (localVarHttpHeaderAcceptSelected === undefined) { // to determine the Accept header @@ -359,6 +440,15 @@ export class NoticesService { } + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + let responseType_: 'text' | 'json' | 'blob' = 'json'; if (localVarHttpHeaderAcceptSelected) { if (localVarHttpHeaderAcceptSelected.startsWith('text')) { @@ -370,10 +460,11 @@ export class NoticesService { } } - let localVarPath = `/api/v1/notices`; - return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + let localVarPath = `/api/v1/announcements/${this.configuration.encodeParam({name: "announcementId", value: announcementId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, + body: createAnnouncementRequest, responseType: responseType_, withCredentials: this.configuration.withCredentials, headers: localVarHeaders, diff --git a/frontend/src/app/openapi/api/api.ts b/frontend/src/app/openapi/api/api.ts index e8d473e35a..fff564883e 100644 --- a/frontend/src/app/openapi/api/api.ts +++ b/frontend/src/app/openapi/api/api.ts @@ -9,6 +9,8 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ +export * from './announcements.service'; +import { AnnouncementsService } from './announcements.service'; export * from './authentication.service'; import { AuthenticationService } from './authentication.service'; export * from './configuration.service'; @@ -21,8 +23,6 @@ export * from './health.service'; import { HealthService } from './health.service'; export * from './integrations-pure-variants.service'; import { IntegrationsPureVariantsService } from './integrations-pure-variants.service'; -export * from './notices.service'; -import { NoticesService } from './notices.service'; export * from './permissions.service'; import { PermissionsService } from './permissions.service'; export * from './projects.service'; @@ -71,4 +71,4 @@ export * from './users-token.service'; import { UsersTokenService } from './users-token.service'; export * from './users-workspaces.service'; import { UsersWorkspacesService } from './users-workspaces.service'; -export const APIS = [AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, NoticesService, PermissionsService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsProvisioningService, ProjectsModelsREADMEService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, ProjectsPermissionsService, ProjectsToolsService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersPermissionsService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; +export const APIS = [AnnouncementsService, AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, PermissionsService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsProvisioningService, ProjectsModelsREADMEService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, ProjectsPermissionsService, ProjectsToolsService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersPermissionsService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; diff --git a/frontend/src/app/openapi/model/admin-scopes-input.ts b/frontend/src/app/openapi/model/admin-scopes-input.ts index 15e9e05e86..e8199f8105 100644 --- a/frontend/src/app/openapi/model/admin-scopes-input.ts +++ b/frontend/src/app/openapi/model/admin-scopes-input.ts @@ -87,9 +87,10 @@ export namespace AdminScopesInput { Update: 'UPDATE' as ToolsEnum, Delete: 'DELETE' as ToolsEnum }; - export type AnnouncementsEnum = 'CREATE' | 'DELETE'; + export type AnnouncementsEnum = 'CREATE' | 'UPDATE' | 'DELETE'; export const AnnouncementsEnum = { Create: 'CREATE' as AnnouncementsEnum, + Update: 'UPDATE' as AnnouncementsEnum, Delete: 'DELETE' as AnnouncementsEnum }; export type MonitoringEnum = 'GET'; diff --git a/frontend/src/app/openapi/model/admin-scopes-output.ts b/frontend/src/app/openapi/model/admin-scopes-output.ts index 30cc99fade..faf6d23476 100644 --- a/frontend/src/app/openapi/model/admin-scopes-output.ts +++ b/frontend/src/app/openapi/model/admin-scopes-output.ts @@ -87,9 +87,10 @@ export namespace AdminScopesOutput { Update: 'UPDATE' as ToolsEnum, Delete: 'DELETE' as ToolsEnum }; - export type AnnouncementsEnum = 'CREATE' | 'DELETE'; + export type AnnouncementsEnum = 'CREATE' | 'UPDATE' | 'DELETE'; export const AnnouncementsEnum = { Create: 'CREATE' as AnnouncementsEnum, + Update: 'UPDATE' as AnnouncementsEnum, Delete: 'DELETE' as AnnouncementsEnum }; export type MonitoringEnum = 'GET'; diff --git a/frontend/src/app/openapi/model/announcement-level.ts b/frontend/src/app/openapi/model/announcement-level.ts new file mode 100644 index 0000000000..c136a98802 --- /dev/null +++ b/frontend/src/app/openapi/model/announcement-level.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration Manager API + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export type AnnouncementLevel = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'alert'; + +export const AnnouncementLevel = { + Primary: 'primary' as AnnouncementLevel, + Secondary: 'secondary' as AnnouncementLevel, + Success: 'success' as AnnouncementLevel, + Danger: 'danger' as AnnouncementLevel, + Warning: 'warning' as AnnouncementLevel, + Info: 'info' as AnnouncementLevel, + Alert: 'alert' as AnnouncementLevel +}; + diff --git a/frontend/src/app/openapi/model/notice-response.ts b/frontend/src/app/openapi/model/announcement-response.ts similarity index 69% rename from frontend/src/app/openapi/model/notice-response.ts rename to frontend/src/app/openapi/model/announcement-response.ts index 4581bbb5c3..5812dd670b 100644 --- a/frontend/src/app/openapi/model/notice-response.ts +++ b/frontend/src/app/openapi/model/announcement-response.ts @@ -9,16 +9,17 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ -import { NoticeLevel } from './notice-level'; +import { AnnouncementLevel } from './announcement-level'; -export interface NoticeResponse { - level: NoticeLevel; +export interface AnnouncementResponse { + level: AnnouncementLevel; title: string; message: string; + dismissible: boolean; id: number; } -export namespace NoticeResponse { +export namespace AnnouncementResponse { } diff --git a/frontend/src/app/openapi/model/create-notice-request.ts b/frontend/src/app/openapi/model/create-announcement-request.ts similarity index 67% rename from frontend/src/app/openapi/model/create-notice-request.ts rename to frontend/src/app/openapi/model/create-announcement-request.ts index c664facc31..7eac869675 100644 --- a/frontend/src/app/openapi/model/create-notice-request.ts +++ b/frontend/src/app/openapi/model/create-announcement-request.ts @@ -9,15 +9,16 @@ + To generate a new version, run `make openapi` in the root directory of this repository. */ -import { NoticeLevel } from './notice-level'; +import { AnnouncementLevel } from './announcement-level'; -export interface CreateNoticeRequest { - level: NoticeLevel; +export interface CreateAnnouncementRequest { + level: AnnouncementLevel; title: string; message: string; + dismissible: boolean; } -export namespace CreateNoticeRequest { +export namespace CreateAnnouncementRequest { } diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts index 0975d4bbb2..4169527ee1 100644 --- a/frontend/src/app/openapi/model/models.ts +++ b/frontend/src/app/openapi/model/models.ts @@ -11,6 +11,8 @@ export * from './admin-scopes-input'; export * from './admin-scopes-output'; +export * from './announcement-level'; +export * from './announcement-response'; export * from './anonymized-session'; export * from './authorization-response'; export * from './backup'; @@ -25,8 +27,8 @@ export * from './built-in-link-item'; export * from './built-in-navbar-link'; export * from './cpu-resources-input'; export * from './cpu-resources-output'; +export * from './create-announcement-request'; export * from './create-backup'; -export * from './create-notice-request'; export * from './create-t4-c-instance'; export * from './create-t4-c-repository'; export * from './create-tool-input'; @@ -86,8 +88,6 @@ export * from './model-provisioning'; export * from './navbar-configuration-input'; export * from './navbar-configuration-input-external-links-inner'; export * from './navbar-configuration-output'; -export * from './notice-level'; -export * from './notice-response'; export * from './page-history-event'; export * from './page-pipeline-run'; export * from './patch-project'; diff --git a/frontend/src/app/openapi/model/notice-level.ts b/frontend/src/app/openapi/model/notice-level.ts deleted file mode 100644 index 7f82486485..0000000000 --- a/frontend/src/app/openapi/model/notice-level.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - * - * Capella Collaboration Manager API - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * Do not edit the class manually. - + To generate a new version, run `make openapi` in the root directory of this repository. - */ - - - -export type NoticeLevel = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'alert'; - -export const NoticeLevel = { - Primary: 'primary' as NoticeLevel, - Secondary: 'secondary' as NoticeLevel, - Success: 'success' as NoticeLevel, - Danger: 'danger' as NoticeLevel, - Warning: 'warning' as NoticeLevel, - Info: 'info' as NoticeLevel, - Alert: 'alert' as NoticeLevel -}; - diff --git a/frontend/src/app/page-layout/page-layout.service.ts b/frontend/src/app/page-layout/page-layout.service.ts index 645366fb88..be05e6a3f7 100644 --- a/frontend/src/app/page-layout/page-layout.service.ts +++ b/frontend/src/app/page-layout/page-layout.service.ts @@ -10,15 +10,19 @@ import { Injectable } from '@angular/core'; export class PageLayoutService { showNavbar = true; showFooter = true; - showNotice = true; + showAnnouncement = true; hideNavbar() { this.showNavbar = false; } + setShowAnnouncement(show: boolean) { + this.showAnnouncement = show; + } + enableAll() { this.showNavbar = true; this.showFooter = true; - this.showNotice = true; + this.showAnnouncement = true; } } diff --git a/frontend/src/app/sessions/sessions.component.html b/frontend/src/app/sessions/sessions.component.html index 7460f08f83..742acd7a35 100644 --- a/frontend/src/app/sessions/sessions.component.html +++ b/frontend/src/app/sessions/sessions.component.html @@ -3,8 +3,6 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - -
diff --git a/frontend/src/app/sessions/sessions.component.ts b/frontend/src/app/sessions/sessions.component.ts index decbc56520..eb47086c6c 100644 --- a/frontend/src/app/sessions/sessions.component.ts +++ b/frontend/src/app/sessions/sessions.component.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ import { Component } from '@angular/core'; -import { WelcomeComponent } from '../general/welcome/welcome.component'; import { ActiveSessionsComponent } from './user-sessions-wrapper/active-sessions/active-sessions.component'; import { CreatePersistentSessionComponent } from './user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component'; import { UserSessionsWrapperComponent } from './user-sessions-wrapper/user-sessions-wrapper.component'; @@ -15,7 +14,6 @@ import { UserSessionsWrapperComponent } from './user-sessions-wrapper/user-sessi UserSessionsWrapperComponent, CreatePersistentSessionComponent, ActiveSessionsComponent, - WelcomeComponent, ], }) export class SessionsComponent {} diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html b/frontend/src/app/settings/core/alert-settings/alert-settings.component.html deleted file mode 100644 index d5733041a2..0000000000 --- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html +++ /dev/null @@ -1,91 +0,0 @@ - - -
-
-

Create new Alert

-
-
-
- - Title - - -
- - Level - - @for (noticeLevel of noticeLevels; track noticeLevel) { - - {{ noticeLevel }} - - } - - Please select a level! - -
- - Description - - @if (message.getError("titleOrDescriptionAvailable")) { - Please enter a description or title! - }
- -
-
-
-

Handle alerts

- @if ((noticeWrapperService.notices$ | async) === undefined) { - @for (_ of [0, 1, 2]; track $index) { - - } - } @else { - - @for ( - notice of noticeWrapperService.notices$ | async; - track notice.id - ) { - - - {{ notice.title }} - - {{ notice.level }} - - -
-

{{ notice.message }}

- -
-
- } @empty { - There are no existing alerts. - } -
- } -
-
diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts deleted file mode 100644 index 7c2ec4ec81..0000000000 --- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { AsyncPipe } from '@angular/common'; -import { Component } from '@angular/core'; -import { - AbstractControl, - FormControl, - FormGroup, - ValidationErrors, - ValidatorFn, - Validators, - FormsModule, - ReactiveFormsModule, -} from '@angular/forms'; -import { MatButton } from '@angular/material/button'; -import { MatOption } from '@angular/material/core'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatError, MatFormFieldModule } from '@angular/material/form-field'; -import { MatInput } from '@angular/material/input'; -import { MatSelect } from '@angular/material/select'; -import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; -import { - CreateNoticeRequest, - NoticeLevel, - NoticesService, -} from 'src/app/openapi'; - -@Component({ - selector: 'app-alert-settings', - templateUrl: './alert-settings.component.html', - imports: [ - FormsModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInput, - MatSelect, - MatOption, - MatError, - MatButton, - MatExpansionModule, - AsyncPipe, - NgxSkeletonLoaderModule, - ], -}) -export class AlertSettingsComponent { - createAlertForm = new FormGroup( - { - title: new FormControl(''), - message: new FormControl(''), - level: new FormControl('', Validators.required), - }, - this.titleOrDescriptionRequired(), - ); - - get message(): FormControl { - return this.createAlertForm.get('message') as FormControl; - } - - get noticeLevels(): string[] { - return Object.values(NoticeLevel); - } - - constructor( - public noticeWrapperService: NoticeWrapperService, - private noticeService: NoticesService, - ) {} - - titleOrDescriptionRequired(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (control.get('title')?.value || control.get('message')?.value) { - control.get('message')?.setErrors(null); - return null; - } - control.get('message')?.setErrors({ titleOrDescriptionAvailable: true }); - return { titleOrDescriptionAvailable: true }; - }; - } - - createNotice(): void { - if (this.createAlertForm.valid) { - this.noticeService - .createNotice(this.createAlertForm.value as CreateNoticeRequest) - .subscribe({ - next: () => { - this.noticeWrapperService.refreshNotices(); - }, - }); - } - } - - deleteNotice(id: number): void { - this.noticeService.deleteNotice(id).subscribe({ - next: () => { - this.noticeWrapperService.refreshNotices(); - }, - }); - } -} diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts deleted file mode 100644 index c4032fcb65..0000000000 --- a/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { userEvent, within } from '@storybook/test'; -import { - mockNotice, - mockNoticeWrapperServiceProvider, -} from 'src/storybook/notices'; -import { AlertSettingsComponent } from './alert-settings.component'; - -const meta: Meta = { - title: 'Settings Components/Alert Settings', - component: AlertSettingsComponent, -}; - -export default meta; -type Story = StoryObj; - -export const Loading: Story = { - args: {}, -}; - -export const NoAlerts: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [mockNoticeWrapperServiceProvider([])], - }), - ], -}; - -export const SomeAlerts: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [ - mockNoticeWrapperServiceProvider([ - mockNotice, - { ...mockNotice, id: 2 }, - ]), - ], - }), - ], -}; - -export const AlertExpanded: Story = { - args: {}, - decorators: [ - moduleMetadata({ - providers: [mockNoticeWrapperServiceProvider([mockNotice])], - }), - ], - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const alert = canvas.getByTestId('alert-1'); - await userEvent.click(alert); - }, -}; diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html new file mode 100644 index 0000000000..2c93ce07ea --- /dev/null +++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.html @@ -0,0 +1,36 @@ + + +
+

Create new Announcement

+ + +

Edit existing Announcements

+ @if (announcementWrapperService.announcements() === undefined) { + @for (_ of [0, 1, 2]; track $index) { + + } + } @else { + @for ( + announcement of announcementWrapperService.announcements(); + track announcement.id + ) { + + } @empty { +

No announcements found

+ } + } +
diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts new file mode 100644 index 0000000000..74456114d1 --- /dev/null +++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.component.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { AnnouncementWrapperService } from 'src/app/general/announcement/announcement.service'; +import { PageLayoutService } from '../../../page-layout/page-layout.service'; +import { EditAnnouncementComponent } from './edit-announcement/edit-announcement/edit-announcement.component'; + +@Component({ + selector: 'app-announcement-settings', + templateUrl: './announcement-settings.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatExpansionModule, + NgxSkeletonLoaderModule, + EditAnnouncementComponent, + ], +}) +export class AnnouncementSettingsComponent implements OnInit, OnDestroy { + public announcementWrapperService: AnnouncementWrapperService = inject( + AnnouncementWrapperService, + ); + public pageLayoutService: PageLayoutService = inject(PageLayoutService); + + ngOnInit(): void { + this.pageLayoutService.setShowAnnouncement(false); + } + + ngOnDestroy(): void { + this.pageLayoutService.setShowAnnouncement(true); + } +} diff --git a/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts b/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts new file mode 100644 index 0000000000..9b427d4d32 --- /dev/null +++ b/frontend/src/app/settings/core/announcement-settings/announcement-settings.stories.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { + mockAnnouncement, + mockAnnouncementWrapperServiceProvider, +} from 'src/storybook/announcements'; +import { AnnouncementSettingsComponent } from './announcement-settings.component'; + +const meta: Meta = { + title: 'Settings Components/Announcement Settings', + component: AnnouncementSettingsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const NoAnnouncements: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [mockAnnouncementWrapperServiceProvider([], [])], + }), + ], +}; + +export const SomeAnnouncements: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + [mockAnnouncement, { ...mockAnnouncement, id: 2 }], + [], + ), + ], + }), + ], +}; diff --git a/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html new file mode 100644 index 0000000000..76f727d86a --- /dev/null +++ b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.html @@ -0,0 +1,62 @@ + + +
+ + Title + + + + Description + + @if ( + announcementForm.get("message")?.getError("titleOrDescriptionAvailable") + ) { + Please enter a description or title! + } + + +
+ + Level + + @for ( + announcementLevel of announcementLevels(); + track announcementLevel + ) { + + {{ announcementLevel }} + + } + + Please select a level! + + + Dismissible + +
+ +
+ +
+ +
+ @if (existingAnnouncement()) { + + + } @else { + + } +
+
diff --git a/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts new file mode 100644 index 0000000000..1d93fc7e3e --- /dev/null +++ b/frontend/src/app/settings/core/announcement-settings/edit-announcement/edit-announcement/edit-announcement.component.ts @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Component, computed, effect, inject, input } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + AbstractControl, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { MatOption } from '@angular/material/autocomplete'; +import { MatButton } from '@angular/material/button'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { MatSelect } from '@angular/material/select'; +import { AnnouncementWrapperService } from '../../../../../general/announcement/announcement.service'; +import { AnnouncementComponent } from '../../../../../general/announcement/announcement/announcement.component'; +import { ToastService } from '../../../../../helpers/toast/toast.service'; +import { + AnnouncementLevel, + AnnouncementResponse, + AnnouncementsService, + CreateAnnouncementRequest, +} from '../../../../../openapi'; + +@Component({ + selector: 'app-edit-announcement', + imports: [ + MatError, + MatFormField, + MatInput, + MatLabel, + MatOption, + MatSelect, + ReactiveFormsModule, + AnnouncementComponent, + MatButton, + MatCheckbox, + ], + templateUrl: './edit-announcement.component.html', +}) +export class EditAnnouncementComponent { + public existingAnnouncement = input(); + announcementService: AnnouncementsService = inject(AnnouncementsService); + announcementWrapperService: AnnouncementWrapperService = inject( + AnnouncementWrapperService, + ); + private toastService: ToastService = inject(ToastService); + + announcementForm = new FormGroup( + { + title: new FormControl(''), + message: new FormControl(''), + level: new FormControl('', Validators.required), + dismissible: new FormControl(true), + }, + this.titleOrDescriptionRequired(), + ); + title = toSignal(this.announcementForm.controls.title.valueChanges); + message = toSignal(this.announcementForm.controls.message.valueChanges); + level = toSignal(this.announcementForm.controls.level.valueChanges); + dismissible = toSignal( + this.announcementForm.controls.dismissible.valueChanges, + ); + + displayedAnnouncement = computed(() => { + return { + id: this.existingAnnouncement()?.id || 0, + title: this.title() ?? '', + message: + this.message() ?? + (!this.message() && !this.title() + ? 'Write some text above to preview your announcement' + : ''), + level: (this.level() as AnnouncementLevel) ?? AnnouncementLevel.Info, + dismissible: this.dismissible() ?? true, + }; + }); + + constructor() { + effect(() => { + if (this.existingAnnouncement()) { + this.announcementForm.patchValue(this.existingAnnouncement()!); + } + }); + } + + announcementLevels = computed(() => { + return Object.values(AnnouncementLevel); + }); + + titleOrDescriptionRequired(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.get('title')?.value || control.get('message')?.value) { + control.get('message')?.setErrors(null); + return null; + } + control.get('message')?.setErrors({ titleOrDescriptionAvailable: true }); + return { titleOrDescriptionAvailable: true }; + }; + } + + createAnnouncement(): void { + if (this.announcementForm.valid && !this.existingAnnouncement()) { + this.announcementService + .createAnnouncement( + this.announcementForm.value as CreateAnnouncementRequest, + ) + .subscribe({ + next: () => { + this.toastService.showSuccess( + 'Announcement created', + 'The announcement has been created successfully', + ); + this.announcementWrapperService.refreshAnnouncements(); + this.announcementForm.reset({ + title: '', + message: '', + level: '', + dismissible: true, + }); + }, + }); + } + } + + updateAnnouncement(): void { + if (this.announcementForm.valid && this.existingAnnouncement()) { + this.announcementService + .updateAnnouncement( + this.existingAnnouncement()!.id, + this.announcementForm.value as CreateAnnouncementRequest, + ) + .subscribe({ + next: () => { + this.toastService.showSuccess( + 'Announcement updated', + 'The announcement has been updated successfully', + ); + this.announcementWrapperService.refreshAnnouncements(); + }, + }); + } + } + + deleteAnnouncement(): void { + if (this.existingAnnouncement()) { + this.announcementService + .deleteAnnouncement(this.existingAnnouncement()!.id) + .subscribe({ + next: () => { + this.toastService.showSuccess( + 'Announcement deleted', + 'The announcement has been deleted successfully', + ); + this.announcementWrapperService.refreshAnnouncements(); + }, + }); + } + } +} diff --git a/frontend/src/app/settings/settings.component.html b/frontend/src/app/settings/settings.component.html index 3cf7a3fcce..9533e52b3e 100644 --- a/frontend/src/app/settings/settings.component.html +++ b/frontend/src/app/settings/settings.component.html @@ -11,7 +11,7 @@ supervised_user_circle
- +
Announcements
add_alert diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html new file mode 100644 index 0000000000..5f59bea62c --- /dev/null +++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.html @@ -0,0 +1,23 @@ + + +@if (hiddenAnnouncements().length > 0) { +
+

Reset hidden announcements

+

+ You currently have {{ hiddenAnnouncements().length }} hidden + {{ + hiddenAnnouncements().length === 1 ? "announcement" : "announcements" + }}. To reset the hidden announcements, click the button below. +

+ +
+} diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts new file mode 100644 index 0000000000..a35bbd342b --- /dev/null +++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.component.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Component, computed, inject } from '@angular/core'; +import { MatButton } from '@angular/material/button'; +import { AnnouncementWrapperService } from '../../../general/announcement/announcement.service'; + +@Component({ + selector: 'app-reset-hidden-announcements', + imports: [MatButton], + templateUrl: './reset-hidden-announcements.component.html', +}) +export class ResetHiddenAnnouncementsComponent { + announcementWrapperService: AnnouncementWrapperService = inject( + AnnouncementWrapperService, + ); + + hiddenAnnouncements = computed(() => { + return this.announcementWrapperService + .dismissedAnnouncements() + .filter((announcement) => { + return this.announcementWrapperService + .announcements() + ?.find((visibleAnnouncement) => { + return visibleAnnouncement.id === announcement.id; + }); + }); + }); +} diff --git a/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts new file mode 100644 index 0000000000..3cc71a5c48 --- /dev/null +++ b/frontend/src/app/users/users-profile/reset-hidden-announcements/reset-hidden-announcements.stories.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { + mockAnnouncement, + mockAnnouncementWrapperServiceProvider, +} from '../../../../storybook/announcements'; +import { ResetHiddenAnnouncementsComponent } from './reset-hidden-announcements.component'; + +const meta: Meta = { + title: 'Settings Components/Users Profile/Reset Hidden Announcements', + component: ResetHiddenAnnouncementsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const NoDismissedAnnouncements: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider([mockAnnouncement], []), + ], + }), + ], +}; + +export const DismissedAnnouncementThatIsNoLongerVisible: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + [mockAnnouncement], + [{ id: -1, date: new Date() }], + ), + ], + }), + ], +}; + +export const OneDismissedAnnouncement: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + [mockAnnouncement], + [{ id: 1, date: new Date() }], + ), + ], + }), + ], +}; + +export const TwoDismissedAnnouncements: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + mockAnnouncementWrapperServiceProvider( + [mockAnnouncement, { ...mockAnnouncement, id: 2 }], + [ + { id: 1, date: new Date() }, + { id: 2, date: new Date() }, + ], + ), + ], + }), + ], +}; diff --git a/frontend/src/app/users/users-profile/users-profile.component.html b/frontend/src/app/users/users-profile/users-profile.component.html index 12b5737131..9e567e06b3 100644 --- a/frontend/src/app/users/users-profile/users-profile.component.html +++ b/frontend/src/app/users/users-profile/users-profile.component.html @@ -34,6 +34,10 @@

Profile of {{ user?.name }}

} } + @if (ownUserService.user?.id === (userWrapperService.user$ | async)?.id) { + + } + @if (ownUserService.user?.role === "administrator") { diff --git a/frontend/src/app/users/users-profile/users-profile.component.ts b/frontend/src/app/users/users-profile/users-profile.component.ts index 12ba1a6869..55c4b7a7c9 100644 --- a/frontend/src/app/users/users-profile/users-profile.component.ts +++ b/frontend/src/app/users/users-profile/users-profile.component.ts @@ -10,6 +10,7 @@ import { UserWrapperService } from 'src/app/users/user-wrapper/user-wrapper.serv import { UnifiedConfigWrapperService } from '../../services/unified-config-wrapper/unified-config-wrapper.service'; import { BetaTestingComponent } from './beta-testing/beta-testing.component'; import { CommonProjectsComponent } from './common-projects/common-projects.component'; +import { ResetHiddenAnnouncementsComponent } from './reset-hidden-announcements/reset-hidden-announcements.component'; import { UserInformationComponent } from './user-information/user-information.component'; import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.component'; @@ -24,6 +25,7 @@ import { UserWorkspacesComponent } from './user-workspaces/user-workspaces.compo UserWorkspacesComponent, AsyncPipe, BetaTestingComponent, + ResetHiddenAnnouncementsComponent, ], }) export class UsersProfileComponent { diff --git a/frontend/src/storybook/announcements.ts b/frontend/src/storybook/announcements.ts new file mode 100644 index 0000000000..595b9c9e56 --- /dev/null +++ b/frontend/src/storybook/announcements.ts @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { computed, signal } from '@angular/core'; +import { + AnnouncementWrapperService, + DismissedAnnouncements, +} from 'src/app/general/announcement/announcement.service'; +import { AnnouncementLevel, AnnouncementResponse } from 'src/app/openapi'; + +export const mockAnnouncement: AnnouncementResponse = { + id: 1, + title: 'Title of the announcement', + message: + 'This is the message / content of an announcement. It can also contain simple HTML like links: ' + + "
example.com", + level: AnnouncementLevel.Info, + dismissible: true, +}; + +class MockAnnouncementWrapperService + implements Partial +{ + public announcements = signal([]); + public dismissedAnnouncements = signal([]); + public visibleAnnouncements = computed(() => { + return this.announcements().filter( + (announcement) => + !this.dismissedAnnouncements().find((dismissedAnnouncement) => { + return ( + dismissedAnnouncement.id === announcement.id && + announcement.dismissible + ); + }), + ); + }); + dismissAnnouncement(announcementId: number): void { + this.dismissedAnnouncements.set([ + ...this.dismissedAnnouncements(), + { id: announcementId, date: new Date() }, + ]); + } + + resetDismissedAnnouncements(): void { + this.dismissedAnnouncements.set([]); + } + + constructor( + notices: AnnouncementResponse[], + dismissedAnnouncements: DismissedAnnouncements, + ) { + this.announcements.set(notices); + this.dismissedAnnouncements.set(dismissedAnnouncements); + } +} + +export const mockAnnouncementWrapperServiceProvider = ( + announcements: AnnouncementResponse[], + dismissedAnnouncements: DismissedAnnouncements, +) => { + return { + provide: AnnouncementWrapperService, + useValue: new MockAnnouncementWrapperService( + announcements, + dismissedAnnouncements, + ), + }; +}; diff --git a/frontend/src/storybook/notices.ts b/frontend/src/storybook/notices.ts deleted file mode 100644 index 26cd6c3fe8..0000000000 --- a/frontend/src/storybook/notices.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { BehaviorSubject } from 'rxjs'; -import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; -import { NoticeLevel, NoticeResponse } from 'src/app/openapi'; - -export const mockNotice: NoticeResponse = { - id: 1, - title: 'Title of the notice', - message: - 'This is the message / content of a notice. It can also contain simple HTML like links: ' + - "example.com", - level: NoticeLevel.Info, -}; - -class MockNoticeWrapperService implements Partial { - private _notices = new BehaviorSubject( - undefined, - ); - public readonly notices$ = this._notices.asObservable(); - - constructor(notices: NoticeResponse[]) { - this._notices.next(notices); - } -} - -export const mockNoticeWrapperServiceProvider = (notices: NoticeResponse[]) => { - return { - provide: NoticeWrapperService, - useValue: new MockNoticeWrapperService(notices), - }; -};