diff --git a/Tekst-API/openapi.json b/Tekst-API/openapi.json index dcc0de49..a8ca916b 100644 --- a/Tekst-API/openapi.json +++ b/Tekst-API/openapi.json @@ -785,6 +785,194 @@ } } }, + "/layers/{id}/propose": { + "post": { + "tags": [ + "layers" + ], + "summary": "Propose layer", + "operationId": "proposeLayer", + "security": [ + { + "APIKeyCookie": [] + }, + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "5eb7cf5a86d9755df3a6c593", + "title": "Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "404": { + "description": "Not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/layers/{id}/unpropose": { + "post": { + "tags": [ + "layers" + ], + "summary": "Unpropose layer", + "operationId": "unproposeLayer", + "security": [ + { + "APIKeyCookie": [] + }, + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "5eb7cf5a86d9755df3a6c593", + "title": "Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "404": { + "description": "Not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/layers/{id}/publish": { + "post": { + "tags": [ + "layers" + ], + "summary": "Publish layer", + "operationId": "publishLayer", + "security": [ + { + "APIKeyCookie": [] + }, + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "5eb7cf5a86d9755df3a6c593", + "title": "Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "404": { + "description": "Not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/layers/{id}/unpublish": { + "post": { + "tags": [ + "layers" + ], + "summary": "Unpublish layer", + "operationId": "unpublishLayer", + "security": [ + { + "APIKeyCookie": [] + }, + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "5eb7cf5a86d9755df3a6c593", + "title": "Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "404": { + "description": "Not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/nodes": { "post": { "tags": [ diff --git a/Tekst-API/tekst/config.py b/Tekst-API/tekst/config.py index a9b7d332..0658d83f 100644 --- a/Tekst-API/tekst/config.py +++ b/Tekst-API/tekst/config.py @@ -5,7 +5,7 @@ from typing import Annotated from urllib.parse import quote -from pydantic import EmailStr, Field, StringConstraints, field_validator +from pydantic import EmailStr, Field, StringConstraints, computed_field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict from tekst import pkg_meta @@ -155,23 +155,18 @@ class TekstConfig(BaseSettings): tekst_license: str = pkg_meta["license"] tekst_license_url: CustomHttpUrl = pkg_meta["license_url"] - @field_validator("db_host", "db_password", mode="after") - @classmethod - def url_quote(cls, v: str) -> str: - return quote(str(v).encode("utf8"), safe="") - @field_validator("db_name", mode="after") @classmethod def generate_db_name(cls, v: str) -> str: return safe_name(v) - def db_get_uri(self) -> str: - creds = ( - f"{self.db_user}:{self.db_password}@" - if self.db_user and self.db_password - else "" - ) - return f"{self.db_protocol}://{creds}{self.db_host}:{str(self.db_port)}" + @computed_field + @property + def db_uri(self) -> str: + db_host = quote(str(self.db_host).encode("utf8"), safe="") + db_password = quote(str(self.db_password).encode("utf8"), safe="") + creds = f"{self.db_user}:{db_password}@" if self.db_user and db_password else "" + return f"{self.db_protocol}://{creds}{db_host}:{str(self.db_port)}" @field_validator( "cors_allow_origins", "cors_allow_methods", "cors_allow_headers", mode="before" diff --git a/Tekst-API/tekst/db.py b/Tekst-API/tekst/db.py index 15fae596..791a3dbb 100644 --- a/Tekst-API/tekst/db.py +++ b/Tekst-API/tekst/db.py @@ -24,7 +24,7 @@ def _init_client(db_uri: str = None) -> None: global _db_client if _db_client is None: log.info("Initializing database client...") - _db_client = DatabaseClient(db_uri or _cfg.db_get_uri()) + _db_client = DatabaseClient(db_uri or _cfg.db_uri) def get_client(db_uri: str) -> DatabaseClient: @@ -35,7 +35,7 @@ def get_client(db_uri: str) -> DatabaseClient: async def reset_db(): """Drops the database""" - await get_client(_cfg.db_get_uri()).drop_database(_cfg.db_name) + await get_client(_cfg.db_uri).drop_database(_cfg.db_name) async def init_odm(db: Database) -> None: diff --git a/Tekst-API/tekst/dependencies.py b/Tekst-API/tekst/dependencies.py index ed0a7dee..a1b6391b 100644 --- a/Tekst-API/tekst/dependencies.py +++ b/Tekst-API/tekst/dependencies.py @@ -12,7 +12,7 @@ def get_cfg() -> TekstConfig: def get_db_client(cfg: TekstConfig = get_config()) -> DatabaseClient: - return get_client(cfg.db_get_uri()) + return get_client(cfg.db_uri) def get_db( diff --git a/Tekst-API/tekst/routers/layers.py b/Tekst-API/tekst/routers/layers.py index 3209842f..0a9f5608 100644 --- a/Tekst-API/tekst/routers/layers.py +++ b/Tekst-API/tekst/routers/layers.py @@ -3,7 +3,7 @@ from beanie import PydanticObjectId from fastapi import APIRouter, HTTPException, Path, Query, status -from tekst.auth import OptionalUserDep, UserDep +from tekst.auth import OptionalUserDep, SuperuserDep, UserDep from tekst.layer_types import layer_type_manager from tekst.models.layer import AnyLayerRead, LayerBase, LayerBaseDocument from tekst.models.text import TextDocument @@ -17,10 +17,10 @@ async def get_layer( id: PydanticObjectId, user: OptionalUserDep ) -> layer_read_model: """A generic route for reading a layer definition from the database""" - layer_doc = await layer_document_model.find( + layer_doc = await layer_document_model.find_one( layer_document_model.id == id, await layer_document_model.allowed_to_read(user), - ).first_or_none() + ) if not layer_doc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -64,16 +64,17 @@ def _generate_update_endpoint( async def update_layer( id: PydanticObjectId, updates: layer_update_model, user: UserDep ) -> layer_read_model: - layer_doc: layer_document_model = ( - await layer_document_model.find(layer_document_model.id == id) - .find(layer_document_model.allowed_to_write(user)) - .first_or_none() + layer_doc: layer_document_model = await layer_document_model.find_one( + layer_document_model.id == id, layer_document_model.allowed_to_write(user) ) if not layer_doc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Layer {id} doesn't exist or requires extra permissions", ) + # force keep certain fields + for field in ("public", "proposed", "text_id"): + setattr(updates, field, getattr(layer_doc, field)) await layer_doc.apply(updates.model_dump(exclude_unset=True)) return layer_doc @@ -314,11 +315,11 @@ async def get_generic_layer_data_by_id( ), ] = False, ) -> dict: - layer_doc = await LayerBaseDocument.find( + layer_doc = await LayerBaseDocument.find_one( LayerBaseDocument.id == layer_id, await LayerBaseDocument.allowed_to_read(user), with_children=True, - ).first_or_none() + ) if not layer_doc: raise HTTPException( status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}" @@ -326,3 +327,116 @@ async def get_generic_layer_data_by_id( return await _process_layer_results( layer_doc, user, include_owners, include_writable ) + + +@router.post("/{id}/propose", status_code=status.HTTP_204_NO_CONTENT) +async def propose_layer( + user: UserDep, layer_id: Annotated[PydanticObjectId, Path(alias="id")] +) -> None: + layer_doc = await LayerBaseDocument.get(layer_id, with_children=True) + if not layer_doc: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}" + ) + if not user.is_superuser and user.id != layer_doc.owner_id: + raise HTTPException(status.HTTP_401_UNAUTHORIZED) + if layer_doc.public: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is already public", + ) + if layer_doc.proposed: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is already proposed for publication", + ) + # all fine, propose layer + await LayerBaseDocument.find_one( + LayerBaseDocument.id == layer_id, + with_children=True, + ).set({LayerBaseDocument.proposed: True}) # noqa: E712 + + +@router.post("/{id}/unpropose", status_code=status.HTTP_204_NO_CONTENT) +async def unpropose_layer( + user: UserDep, layer_id: Annotated[PydanticObjectId, Path(alias="id")] +) -> None: + layer_doc = await LayerBaseDocument.get(layer_id, with_children=True) + if not layer_doc: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}" + ) + if not user.is_superuser and user.id != layer_doc.owner_id: + raise HTTPException(status.HTTP_401_UNAUTHORIZED) + if not layer_doc.proposed: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is not proposed for publication", + ) + # all fine, unpropose layer + await LayerBaseDocument.find_one( + LayerBaseDocument.id == layer_id, + with_children=True, + ).set( + { + LayerBaseDocument.proposed: False, # noqa: E712 + LayerBaseDocument.public: False, # noqa: E712 + } + ) + + +@router.post("/{id}/publish", status_code=status.HTTP_204_NO_CONTENT) +async def publish_layer( + user: SuperuserDep, layer_id: Annotated[PydanticObjectId, Path(alias="id")] +) -> None: + layer_doc = await LayerBaseDocument.get(layer_id, with_children=True) + if not layer_doc: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}" + ) + if layer_doc.public: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is already public", + ) + if not layer_doc.proposed: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is not proposed for publication", + ) + # all fine, publish layer + await LayerBaseDocument.find_one( + LayerBaseDocument.id == layer_id, + with_children=True, + ).set( + { + LayerBaseDocument.public: True, # noqa: E712 + LayerBaseDocument.proposed: False, # noqa: E712 + } + ) + + +@router.post("/{id}/unpublish", status_code=status.HTTP_204_NO_CONTENT) +async def unpublish_layer( + user: SuperuserDep, layer_id: Annotated[PydanticObjectId, Path(alias="id")] +) -> None: + layer_doc = await LayerBaseDocument.get(layer_id, with_children=True) + if not layer_doc: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}" + ) + if not layer_doc.public: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Layer with ID {layer_id} is not public", + ) + # all fine, unpublish layer + await LayerBaseDocument.find_one( + LayerBaseDocument.id == layer_id, + with_children=True, + ).set( + { + LayerBaseDocument.public: False, # noqa: E712 + LayerBaseDocument.proposed: False, # noqa: E712 + } + ) diff --git a/Tekst-API/tests/conftest.py b/Tekst-API/tests/conftest.py index 1044396e..fb8e93b5 100644 --- a/Tekst-API/tests/conftest.py +++ b/Tekst-API/tests/conftest.py @@ -84,7 +84,7 @@ def _get_sample_data(rel_path: str, for_http: bool = False) -> Any: @pytest.fixture(scope="session") async def get_db_client_override(config) -> DatabaseClient: """Dependency override for the database client dependency""" - db_client: DatabaseClient = DatabaseClient(config.db_get_uri()) + db_client: DatabaseClient = DatabaseClient(config.db_uri) yield db_client # close db connection db_client.close() @@ -138,28 +138,36 @@ async def _insert_sample_data(*collections: str) -> dict[str, list[str]]: @pytest.fixture -def new_user_data() -> dict: - return dict( - email="foo@bar.de", - username="test_user", - password="poiPOI098", - first_name="Foo", - last_name="Bar", - affiliation="Some Institution", - ) +def get_fake_user() -> Callable: + def _get_fake_user(alternative: bool = False): + return dict( + email="foo@bar.de" if not alternative else "bar@foo.de", + username="test_user" if not alternative else "test_user2", + password="poiPOI098", + first_name="Foo", + last_name="Bar", + affiliation="Some Institution", + ) + + return _get_fake_user @pytest.fixture -async def register_test_user(new_user_data) -> Callable: +async def register_test_user(get_fake_user) -> Callable: async def _register_test_user( - *, is_active: bool = True, is_verified: bool = True, is_superuser: bool = False + *, + is_active: bool = True, + is_verified: bool = True, + is_superuser: bool = False, + alternative: bool = False, ) -> dict: - user = UserCreate(**new_user_data) + user_data = get_fake_user(alternative=alternative) + user = UserCreate(**user_data) user.is_active = is_active user.is_verified = is_verified user.is_superuser = is_superuser created_user = await _create_user(user) - return {"id": str(created_user.id), **new_user_data} + return {"id": str(created_user.id), **user_data} return _register_test_user diff --git a/Tekst-API/tests/integration/test_api_auth.py b/Tekst-API/tests/integration/test_api_auth.py index 83a1f388..1af7b27a 100644 --- a/Tekst-API/tests/integration/test_api_auth.py +++ b/Tekst-API/tests/integration/test_api_auth.py @@ -5,9 +5,9 @@ @pytest.mark.anyio async def test_register( - api_path, test_client: AsyncClient, new_user_data, status_fail_msg + api_path, test_client: AsyncClient, get_fake_user, status_fail_msg ): - payload = new_user_data + payload = get_fake_user() resp = await test_client.post("/auth/register", json=payload) assert resp.status_code == 201, status_fail_msg(201, resp) assert "id" in resp.json() @@ -15,9 +15,9 @@ async def test_register( @pytest.mark.anyio async def test_register_invalid_pw( - api_path, reset_db, test_client: AsyncClient, new_user_data, status_fail_msg + api_path, reset_db, test_client: AsyncClient, get_fake_user, status_fail_msg ): - payload = new_user_data + payload = get_fake_user() payload["username"] = "uuuuhhh" payload["password"] = "foo" @@ -46,9 +46,9 @@ async def test_register_invalid_pw( @pytest.mark.anyio async def test_register_username_exists( - api_path, reset_db, test_client: AsyncClient, new_user_data, status_fail_msg + api_path, reset_db, test_client: AsyncClient, get_fake_user, status_fail_msg ): - payload = new_user_data + payload = get_fake_user() payload["username"] = "someuser" resp = await test_client.post("/auth/register", json=payload) @@ -61,9 +61,9 @@ async def test_register_username_exists( @pytest.mark.anyio async def test_register_email_exists( - api_path, reset_db, test_client: AsyncClient, new_user_data, status_fail_msg + api_path, reset_db, test_client: AsyncClient, get_fake_user, status_fail_msg ): - payload = new_user_data + payload = get_fake_user() payload["email"] = "first@test.com" payload["username"] = "first" @@ -85,8 +85,8 @@ async def test_login( test_client: AsyncClient, status_fail_msg, ): - await register_test_user() - payload = {"username": "foo@bar.de", "password": "poiPOI098"} + user_data = await register_test_user() + payload = {"username": user_data["email"], "password": user_data["password"]} resp = await test_client.post( "/auth/cookie/login", data=payload, @@ -104,8 +104,8 @@ async def test_login_fail_bad_pw( test_client: AsyncClient, status_fail_msg, ): - await register_test_user() - payload = {"username": "foo@bar.de", "password": "wrongpassword"} + user_data = await register_test_user() + payload = {"username": user_data["username"], "password": "XoiPOI09871"} resp = await test_client.post( "/auth/cookie/login", data=payload, @@ -123,8 +123,8 @@ async def test_login_fail_unverified( test_client: AsyncClient, status_fail_msg, ): - await register_test_user(is_verified=False) - payload = {"username": "foo@bar.de", "password": "poiPOI098"} + user_data = await register_test_user(is_verified=False) + payload = {"username": user_data["email"], "password": user_data["password"]} resp = await test_client.post( "/auth/cookie/login", data=payload, @@ -138,42 +138,27 @@ async def test_user_updates_self( config, reset_db, register_test_user, + get_session_cookie, api_path, test_client: AsyncClient, status_fail_msg, ): - await register_test_user() - # login - payload = {"username": "foo@bar.de", "password": "poiPOI098"} - resp = await test_client.post( - "/auth/cookie/login", - data=payload, - ) - assert resp.status_code == 204, status_fail_msg(204, resp) - - # save auth cookie - assert resp.cookies.get(config.security_auth_cookie_name) - auth_token = resp.cookies.get(config.security_auth_cookie_name) - + user_data = await register_test_user() + session_cookie = await get_session_cookie(user_data) # get user data from /users/me resp = await test_client.get( "/users/me", - cookies={ - config.security_auth_cookie_name: auth_token, - }, + cookies=session_cookie, ) assert resp.status_code == 200, status_fail_msg(200, resp) assert "id" in resp.json() - # update own first name user_id = resp.json()["id"] updates = {"firstName": "Bird Person"} resp = await test_client.patch( "/users/me", json=updates, - cookies={ - config.security_auth_cookie_name: auth_token, - }, + cookies=session_cookie, ) assert resp.status_code == 200, status_fail_msg(200, resp) assert resp.json()["id"] == user_id diff --git a/Tekst-API/tests/integration/test_api_layer.py b/Tekst-API/tests/integration/test_api_layer.py index a2885796..76a85340 100644 --- a/Tekst-API/tests/integration/test_api_layer.py +++ b/Tekst-API/tests/integration/test_api_layer.py @@ -179,13 +179,11 @@ async def test_access_private_layer( # register test superuser user_data = await register_test_user(is_superuser=True) session_cookie = await get_session_cookie(user_data) - # set layer to inactive - resp = await test_client.patch( - f"/layers/plaintext/{layer_id}", json={"public": False}, cookies=session_cookie + # unpublish + resp = await test_client.post( + f"/layers/{layer_id}/unpublish", cookies=session_cookie ) - assert resp.status_code == 200, status_fail_msg(200, resp) - assert isinstance(resp.json(), dict) - assert resp.json()["public"] is False + assert resp.status_code == 204, status_fail_msg(204, resp) # logout resp = await test_client.post("/auth/cookie/logout") assert resp.status_code == 204, status_fail_msg(204, resp) @@ -222,6 +220,93 @@ async def test_get_layers( assert resp.status_code == 422, status_fail_msg(422, resp) +@pytest.mark.anyio +async def test_propose_unpropose_publish_unpublish_layer( + api_path, + test_client: AsyncClient, + insert_sample_data, + status_fail_msg, + register_test_user, + get_session_cookie, +): + text_id = (await insert_sample_data("texts", "nodes", "layers"))["texts"][0] + user_data = await register_test_user() + session_cookie = await get_session_cookie(user_data) + # create new layer (because only owner can update(write)) + payload = { + "title": "Foo Bar Baz", + "textId": text_id, + "level": 0, + "layerType": "plaintext", + "ownerId": user_data.get("id"), + } + resp = await test_client.post( + "/layers/plaintext", json=payload, cookies=session_cookie + ) + assert resp.status_code == 201, status_fail_msg(201, resp) + layer_data = resp.json() + assert "id" in layer_data + assert "ownerId" in layer_data + # become superuser + user_data = await register_test_user(is_superuser=True, alternative=True) + session_cookie = await get_session_cookie(user_data) + # publish unproposed layer + resp = await test_client.post( + f"/layers/{layer_data['id']}/publish", + cookies=session_cookie, + ) + assert resp.status_code == 400, status_fail_msg(400, resp) + # propose layer + resp = await test_client.post( + f"/layers/{layer_data['id']}/propose", + cookies=session_cookie, + ) + assert resp.status_code == 204, status_fail_msg(204, resp) + # get all accessible layers, check if ours is proposed + resp = await test_client.get("/layers", params={"textId": text_id}) + assert resp.status_code == 200, status_fail_msg(200, resp) + assert isinstance(resp.json(), list) + for layer in resp.json(): + if layer["id"] == layer_data["id"]: + assert layer["proposed"] + # propose layer again + resp = await test_client.post( + f"/layers/{layer_data['id']}/propose", + cookies=session_cookie, + ) + assert resp.status_code == 400, status_fail_msg(400, resp) + # publish layer + resp = await test_client.post( + f"/layers/{layer_data['id']}/publish", + cookies=session_cookie, + ) + assert resp.status_code == 204, status_fail_msg(204, resp) + # unpublish layer + resp = await test_client.post( + f"/layers/{layer_data['id']}/unpublish", + cookies=session_cookie, + ) + assert resp.status_code == 204, status_fail_msg(204, resp) + # unpublish layer again + resp = await test_client.post( + f"/layers/{layer_data['id']}/unpublish", + cookies=session_cookie, + ) + assert resp.status_code == 400, status_fail_msg(400, resp) + # propose layer again + resp = await test_client.post( + f"/layers/{layer_data['id']}/propose", + cookies=session_cookie, + ) + assert resp.status_code == 204, status_fail_msg(204, resp) + # unpropose layer + resp = await test_client.post( + f"/layers/{layer_data['id']}/unpropose", + cookies=session_cookie, + ) + assert resp.status_code == 204, status_fail_msg(204, resp) + + # @pytest.mark.anyio # async def test_get_layer_template( # api_path, test_client: AsyncClient, insert_sample_data diff --git a/Tekst-Web/src/api/schema.d.ts b/Tekst-Web/src/api/schema.d.ts index 41ea9066..95bea83b 100644 --- a/Tekst-Web/src/api/schema.d.ts +++ b/Tekst-Web/src/api/schema.d.ts @@ -93,6 +93,22 @@ export interface paths { /** Get generic layer data by id */ get: operations['getGenericLayerDataById']; }; + '/layers/{id}/propose': { + /** Propose layer */ + post: operations['proposeLayer']; + }; + '/layers/{id}/unpropose': { + /** Unpropose layer */ + post: operations['unproposeLayer']; + }; + '/layers/{id}/publish': { + /** Publish layer */ + post: operations['publishLayer']; + }; + '/layers/{id}/unpublish': { + /** Unpublish layer */ + post: operations['unpublishLayer']; + }; '/nodes': { /** Find nodes */ get: operations['findNodes']; @@ -1850,6 +1866,102 @@ export interface operations { }; }; }; + /** Propose layer */ + proposeLayer: { + parameters: { + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 204: { + content: never; + }; + /** @description Not found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + /** Unpropose layer */ + unproposeLayer: { + parameters: { + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 204: { + content: never; + }; + /** @description Not found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + /** Publish layer */ + publishLayer: { + parameters: { + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 204: { + content: never; + }; + /** @description Not found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + /** Unpublish layer */ + unpublishLayer: { + parameters: { + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 204: { + content: never; + }; + /** @description Not found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; /** Find nodes */ findNodes: { parameters: { diff --git a/Tekst-Web/src/components/LayerListItem.vue b/Tekst-Web/src/components/LayerListItem.vue index 2ed31d38..f3cd3cb1 100644 --- a/Tekst-Web/src/components/LayerListItem.vue +++ b/Tekst-Web/src/components/LayerListItem.vue @@ -8,6 +8,7 @@ import LayerPublicationStatus from '@/components/LayerPublicationStatus.vue'; import DeleteFilled from '@vicons/material/DeleteFilled'; import ModeEditFilled from '@vicons/material/ModeEditFilled'; import StarHalfOutlined from '@vicons/material/StarHalfOutlined'; +import StarBorderOutlined from '@vicons/material/StarBorderOutlined'; import PublicFilled from '@vicons/material/PublicFilled'; import PublicOffFilled from '@vicons/material/PublicOffFilled'; @@ -16,7 +17,14 @@ const props = defineProps<{ currentUser?: UserRead; }>(); -defineEmits(['deleteClick']); +defineEmits([ + 'proposeClick', + 'unproposeClick', + 'publishClick', + 'unpublishClick', + 'editClick', + 'deleteClick', +]); const canDelete = computed( () => @@ -28,9 +36,14 @@ const canPropose = computed( () => props.currentUser && (props.currentUser.isSuperuser || props.currentUser.id === props.targetLayer.ownerId) && - !props.targetLayer.public && - !props.targetLayer.proposed + !props.targetLayer.public ); + +const actionButtonProps = { + quaternary: true, + circle: true, + focusable: false, +};